#!/usr/bin/env python3 # # Copyright (C) 2017, 2018 Jaguar Land Rover # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # Authors: Shane Fagan - shane.fagan@collabora.com # # Authors: # * Gustavo Noronha <gustavo.noronha@collabora.com> # * Travis Reitter <travis.reitter@collabora.co.uk> # * Shane Fagan <shane.fagan@collabora.com> # * Luis Araujo <luis.araujo@collabora.co.uk> # * Guillaume Tucker <guillaume.tucker@collabora.com> import os import unittest from subprocess import Popen, PIPE, TimeoutExpired import vsmlib.utils import zmq import ipc.zeromq import ipc.stream RULES_PATH = os.path.abspath(os.path.join('.', 'sample_rules')) LOGS_PATH = os.path.abspath(os.path.join('.', 'sample_logs')) SIGNAL_NUMBER_PATH = os.path.abspath(os.path.join('.', 'signal_number_maps')) SIGNAL_FORMAT = '{},{},\'{}\'\n' VSM_LOG_FILE = 'vsm-tests.log' SIGNAL_NUM_FILE = 'samples.vsi' SIGNUM_DEFAULT = "[SIGNUM]" def format_ipc_input(data): if not data: return [] return [ (x.strip(), y.strip()) for x, y in \ [ elm.split('=') for elm in data.split('\n') ] ] def _remove_timestamp(output_string): # strip any prepended timestamp, if it exists output = '' for line in output_string.splitlines(): try: timestamp, remainder = line.split(',', 1) output += remainder except ValueError: output += line # this re-adds a trailing newline output += '\n' return output def _signal_format_safe(signal_to_num, signal, value): string = '' signum = None if signal in signal_to_num: signum = signal_to_num[signal] elif signal != '': signum = SIGNUM_DEFAULT if signum: string = SIGNAL_FORMAT.format(signal, signum, value) return string class TestVSMDebug(object): module = None quit_command = "\nquit" def close(self): pass def _run_vsm(self, cmd, input_data, sig_num_path, wait_time_ms): data = (input_data + self.quit_command).encode('utf8') timeout_s = 2 if wait_time_ms > 0: timeout_s = wait_time_ms / 1000 process = Popen(cmd, stdin=PIPE, stdout=PIPE) try: output, _ = process.communicate(data, timeout_s) except TimeoutExpired: process.kill() return None cmd_output = output.decode() return _remove_timestamp(cmd_output) class NoneSignalIPC(ipc.stream.StdioIPC): def receive(self): return super(ipc.stream.StdioIPC, self).receive() def _readline(self): line = super(NoneSignalIPC, self)._readline() if line == 'not-acceptable': return None return line class TestVSMNoneSignal(TestVSMDebug): module = 'tests.NoneSignalIPC' quit_command = "\nquit=''" class TestVSMZeroMQ(object): module = 'ipc.zeromq.ZeromqIPC' def __init__(self): self._zmq_addr = ipc.zeromq.SOCKET_ADDR context = zmq.Context() self._zmq_socket = context.socket(zmq.PAIR) self._zmq_socket.connect(self._zmq_addr) # set maximum wait on receiving (in ms) self._zmq_socket.RCVTIMEO = 200 def close(self): self._zmq_socket.close() def _send(self, signal, value): self._zmq_socket.send_pyobj((signal, value)) def _receive(self): return self._zmq_socket.recv_pyobj() def _receive_all(self, signal_to_num): process_output = '' # keep receiving output, one line at a time, until empty (defined as # a timeout of self._zmq_socket.RCVTIMEO ms -- see where that is set # for more information) while True: try: sig, val = self._receive() process_output += _signal_format_safe(signal_to_num, sig, val) except zmq.error.Again: # timed out on receive (which happens when we've received # all output) break return process_output def _run_vsm(self, cmd, input_data, sig_num_path, wait_time_ms): signal_to_num, _ = vsmlib.utils.parse_signal_num_file(sig_num_path) process = Popen(cmd) process_output = self._receive_all(signal_to_num) for signal, value in format_ipc_input(input_data): self._send(signal, value) # Record sent signal directly from the test. process_output += _signal_format_safe(signal_to_num, signal, value) # fetch any pending output so send and receive output maintain # chronological ordering process_output += self._receive_all(signal_to_num) self._send('quit', '') process.wait() process_output += self._receive_all(signal_to_num) return process_output class TestVSM(unittest.TestCase): ipc_class = None def setUp(self): self.ipc = self.ipc_class() def tearDown(self): self.ipc.close() def run_vsm(self, name, input_data, expected_output, use_initial=True, replay_case=None, wait_time_ms=0): conf = os.path.join(RULES_PATH, name + '.yaml') initial_state = os.path.join(RULES_PATH, name + '.initial.yaml') cmd = ['./vsm.py' ] sig_num_path = os.path.join(SIGNAL_NUMBER_PATH, SIGNAL_NUM_FILE) cmd += [ '--signal-number-file={}'.format(sig_num_path) ] # Direct verbose output (including state dumps) to log file so the tests # can parse them. cmd += [ '--log-file={}'.format(VSM_LOG_FILE) ] if use_initial and os.path.exists(initial_state): cmd += ['--initial-state={}'.format(initial_state)] cmd += [conf] if replay_case: replay_file = os.path.join(LOGS_PATH, replay_case + '.log') if os.path.exists(replay_file): cmd += ['--replay-log-file={}'.format(replay_file)] if self.ipc.module: cmd += ['--ipc-modules={}'.format(self.ipc.module)] process_output = self.ipc._run_vsm(cmd, input_data, sig_num_path, wait_time_ms) if process_output is None: self.fail("VSM process failed") # Read state dump from log file. with open(VSM_LOG_FILE) as f: state_output = f.read() log_output = _remove_timestamp(state_output) output_final = log_output + process_output self.assertEqual(output_final , expected_output) class VSMTestCases(TestVSM): def test_simple0(self): input_data = 'transmission.gear = "reverse"' expected_output = ''' transmission.gear,9,'reverse' State = { transmission.gear = reverse } condition: (transmission.gear == 'reverse') => True car.backup,3,'True' State = { car.backup = True transmission.gear = reverse } transmission.gear,9,'"reverse"' car.backup,3,'True' ''' self.run_vsm('simple0', input_data, expected_output.strip() + '\n') def test_simple0_delayed(self): input_data = 'transmission.gear = "reverse"' expected_output = ''' transmission.gear,9,'reverse' State = { transmission.gear = reverse } condition: (transmission.gear == 'reverse') => True car.backup,3,'True' State = { car.backup = True transmission.gear = reverse } transmission.gear,9,'"reverse"' car.backup,3,'True' ''' self.run_vsm('simple0_delay', input_data, expected_output.strip() + '\n') def test_simple0_uninteresting(self): ''' A test case where conditions to emit another signal are never triggered ''' input_data = 'phone.call = "inactive"' expected_output = ''' phone.call,7,'inactive' State = { phone.call = inactive } condition: (phone.call == 'active') => False phone.call,7,'"inactive"' ''' self.run_vsm('simple0', input_data, expected_output.strip() + '\n') def test_simple2_initial(self): input_data = 'damage = True' expected_output = ''' damage,5,True State = { damage = True moving = False } condition: (moving != True and damage == True) => True car.stop,4,'True' State = { car.stop = True damage = True moving = False } damage,5,'True' car.stop,4,'True' ''' self.run_vsm('simple2', input_data, expected_output.strip() + '\n') def test_simple2_initial_uninteresting(self): ''' A test case where conditions to emit another signal are never triggered ''' input_data = 'moving = False' expected_output = ''' moving,6,False State = { moving = False } moving,6,'False' ''' self.run_vsm('simple2', input_data, expected_output.strip() + '\n') def test_simple2_modify_uninteresting(self): ''' A test case where conditions to emit another signal are never triggered ''' input_data = 'moving = True\ndamage = True' expected_output = ''' moving,6,True State = { moving = True } condition: (moving != True and damage == True) => False damage,5,True State = { damage = True moving = True } condition: (moving != True and damage == True) => False moving,6,'True' damage,5,'True' ''' self.run_vsm('simple2', input_data, expected_output.strip() + '\n') def test_simple2_multiple_signals(self): input_data = 'moving = False\ndamage = True' expected_output = ''' moving,6,False State = { moving = False } damage,5,True State = { damage = True moving = False } condition: (moving != True and damage == True) => True car.stop,4,'True' State = { car.stop = True damage = True moving = False } moving,6,'False' damage,5,'True' car.stop,4,'True' ''' self.run_vsm('simple2', input_data, expected_output.strip() + '\n', False) def test_simple0_log_replay(self): ''' A test of the log replay functionality ''' # replay output is not currently forwarded to IPC modules if self.ipc.module: self.skipTest("test not compatible with IPC module") input_data = '' expected_output = ''' phone.call,7,'active' State = { phone.call = active } car.stop,4,'True' State = { car.stop = True phone.call = active } phone.call,7,'active' car.stop,4,'True' ''' self.run_vsm('simple0', input_data, expected_output.strip() + '\n', replay_case='simple0-replay', wait_time_ms=5000) def test_unconditional_emit_log_replay(self): ''' Regression test to ensure we don't issue duplicate unconditional emits when replaying. ''' input_data = '' expected_output = ''' lock.state,13,'true' State = { lock.state = true } lock.state,13,'true' ''' self.run_vsm('unconditional_emit', input_data, expected_output.strip() + '\n', replay_case='unconditional_emit', wait_time_ms=500) def test_simple3_xor_condition(self): input_data = 'phone.call = "active"\nspeed.value = 5.0' expected_output = ''' phone.call,7,'active' State = { phone.call = active } speed.value,8,5.0 State = { phone.call = active speed.value = 5.0 } condition: (phone.call == 'active' ^^ speed.value > 50.90) => True car.stop,4,'True' State = { car.stop = True phone.call = active speed.value = 5.0 } phone.call,7,'"active"' speed.value,8,'5.0' car.stop,4,'True' ''' self.run_vsm('simple3', input_data, expected_output.strip() + '\n') def test_monitored_condition_satisfied(self): ''' This test case sets up the monitor for the subcondition and satisfies the subcondition before the 'stop' timeout (and thus omits the error message in the expected output). ''' input_data = 'transmission.gear = "forward"\n' \ 'transmission.gear = "reverse"\n' \ 'camera.backup.active = True' expected_output = ''' transmission.gear,9,'reverse' State = { transmission.gear = reverse } transmission.gear,9,'forward' State = { transmission.gear = forward } condition: (transmission.gear == 'reverse') => False transmission.gear,9,'reverse' State = { transmission.gear = reverse } condition: (transmission.gear == 'reverse') => True lights.external.backup,14,'True' State = { lights.external.backup = True transmission.gear = reverse } camera.backup.active,15,True State = { camera.backup.active = True lights.external.backup = True transmission.gear = reverse } parent condition: transmission.gear == reverse condition: (camera.backup.active == True) => True transmission.gear,9,'reverse' transmission.gear,9,'"forward"' transmission.gear,9,'"reverse"' lights.external.backup,14,'True' camera.backup.active,15,'True' ''' self.run_vsm('monitored_condition', input_data, expected_output.strip() + '\n', wait_time_ms=2500) def test_monitored_condition_child_failure(self): ''' This test case sets up the monitor for the subcondition and intentionally allows it to fail by not satisfying the subcondition before the 'stop' timeout. ''' input_data = 'transmission.gear = "forward"\n' \ 'transmission.gear = "reverse"' expected_output = ''' transmission.gear,9,'reverse' State = { transmission.gear = reverse } transmission.gear,9,'forward' State = { transmission.gear = forward } condition: (transmission.gear == 'reverse') => False transmission.gear,9,'reverse' State = { transmission.gear = reverse } condition: (transmission.gear == 'reverse') => True lights.external.backup,14,'True' State = { lights.external.backup = True transmission.gear = reverse } condition not met by 'start' time of 1000ms transmission.gear,9,'reverse' transmission.gear,9,'"forward"' transmission.gear,9,'"reverse"' lights.external.backup,14,'True' ''' self.run_vsm('monitored_condition', input_data, expected_output.strip() + '\n', wait_time_ms=1500) def test_monitored_condition_parent_cancellation(self): ''' This test case sets up the monitor for the subcondition and changes the evaluation of the parent condition to cancel the monitor before the 'stop' timeout. ''' input_data = 'transmission.gear = "forward"\n' \ 'transmission.gear = "reverse" \n' \ 'transmission.gear = "forward"' expected_output = ''' transmission.gear,9,'reverse' State = { transmission.gear = reverse } transmission.gear,9,'forward' State = { transmission.gear = forward } condition: (transmission.gear == 'reverse') => False transmission.gear,9,'reverse' State = { transmission.gear = reverse } condition: (transmission.gear == 'reverse') => True lights.external.backup,14,'True' State = { lights.external.backup = True transmission.gear = reverse } transmission.gear,9,'forward' State = { lights.external.backup = True transmission.gear = forward } condition: (transmission.gear == 'reverse') => False transmission.gear,9,'reverse' transmission.gear,9,'"forward"' transmission.gear,9,'"reverse"' lights.external.backup,14,'True' transmission.gear,9,'"forward"' ''' self.run_vsm('monitored_condition', input_data, expected_output.strip() + '\n', wait_time_ms=1500) def test_nested_4_condition_satisfied(self): ''' This test case triggers the parent monitored condition and satisfies its three descendents to fully-satisfy a 4-deep nesting of conditions. ''' input_data = 'a = true\n' \ 'b = true\n' \ 'c = true\n' \ 'd = true' expected_output = ''' a,5040,True State = { a = True } condition: (a == True) => True b,5041,True State = { a = True b = True } parent condition: a == True condition: (b == True) => True c,5042,True State = { a = True b = True c = True } parent condition: b == True parent condition: a == True condition: (c == True) => True d,5043,True State = { a = True b = True c = True d = True } parent condition: c == True parent condition: b == True parent condition: a == True condition: (d == True) => True a,5040,'true' b,5041,'true' c,5042,'true' d,5043,'true' ''' self.run_vsm('nested_4', input_data, expected_output.strip() + '\n', wait_time_ms=2200) def test_nested_4_condition_child_failure(self): ''' This test case triggers the parent monitored condition and fails one of the middle conditions by the timeout. ''' input_data = 'a = true\n' \ 'b = true' expected_output = ''' a,5040,True State = { a = True } condition: (a == True) => True b,5041,True State = { a = True b = True } parent condition: a == True condition: (b == True) => True condition not met by 'start' time of 1000ms condition not met by 'start' time of 1500ms a,5040,'true' b,5041,'true' ''' self.run_vsm('nested_4', input_data, expected_output.strip() + '\n', wait_time_ms=2200) def test_parallel(self): input_data = 'transmission.gear = "reverse"\n'\ 'wipers = True' expected_output = ''' transmission.gear,9,'reverse' State = { transmission.gear = reverse } condition: (transmission.gear == 'reverse') => True reverse,16,'True' State = { reverse = True transmission.gear = reverse } wipers,17,True State = { reverse = True transmission.gear = reverse wipers = True } condition: (wipers == True) => True lights,18,'on' State = { lights = on reverse = True transmission.gear = reverse wipers = True } transmission.gear,9,'"reverse"' reverse,16,'True' wipers,17,'True' lights,18,'on' ''' self.run_vsm('parallel', input_data, expected_output.strip() + '\n', False) def test_sequence_in_order(self): input_data = 'transmission.gear = "park"\n' \ 'ignition = True' expected_output = ''' transmission.gear,9,'park' State = { transmission.gear = park } condition: (transmission.gear == 'park') => True parked,11,'True' State = { parked = True transmission.gear = park } ignition,10,True State = { ignition = True parked = True transmission.gear = park } condition: (ignition == True) => True ignited,12,'True' State = { ignited = True ignition = True parked = True transmission.gear = park } transmission.gear,9,'"park"' parked,11,'True' ignition,10,'True' ignited,12,'True' ''' self.run_vsm('sequence', input_data, expected_output.strip() + '\n') def test_sequence_out_then_in_order(self): input_data = 'ignition = True\n' \ 'transmission.gear = "park"\n' \ 'ignition = True' expected_output = ''' ignition,10,True State = { ignition = True } changed value for signal 'ignition' ignored because prior conditions in its sequence block have not been met transmission.gear,9,'park' State = { ignition = True transmission.gear = park } condition: (transmission.gear == 'park') => True parked,11,'True' State = { ignition = True parked = True transmission.gear = park } ignition,10,True State = { ignition = True parked = True transmission.gear = park } condition: (ignition == True) => True ignited,12,'True' State = { ignited = True ignition = True parked = True transmission.gear = park } ignition,10,'True' transmission.gear,9,'"park"' parked,11,'True' ignition,10,'True' ignited,12,'True' ''' self.run_vsm('sequence', input_data, expected_output.strip() + '\n') def test_unconditional_emit(self): input_data = '' expected_output = ''' lock.state,13,'True' State = { lock.state = True } lock.state,13,'True' ''' self.run_vsm('unconditional_emit', input_data, expected_output.strip() + '\n') def test_delay(self): input_data = 'wipers.front.on = True' expected_output = ''' wipers.front.on,5020,True State = { wipers.front.on = True } condition: (wipers.front.on == True) => True lights.external.headlights,19,'True' State = { lights.external.headlights = True wipers.front.on = True } wipers.front.on,5020,'True' lights.external.headlights,19,'True' ''' # NOTE: ideally, this would ensure the delay in output but, for # simplicity, that is handled in a manual test case. This simply ensures # the output is correct. self.run_vsm('delay', input_data, expected_output.strip() + '\n', False, wait_time_ms=2500) def test_subclauses_arithmetic_booleans(self): input_data = 'flux_capacitor.energy_generated = 1.1\nspeed.value = 140' expected_output = ''' flux_capacitor.energy_generated,5030,1.1 State = { flux_capacitor.energy_generated = 1.1 } condition: (flux_capacitor.energy_generated >= 1.21 * 0.9 and not (flux_capacitor.energy_generated >= 1.21) ) => True lights.external.time_travel_imminent,5032,'True' State = { flux_capacitor.energy_generated = 1.1 lights.external.time_travel_imminent = True } condition: (flux_capacitor.energy_generated >= 1.21 * 0.9 and not (flux_capacitor.energy_generated >= 1.21) ) => True lights.external.time_travel_imminent,5032,'True' State = { flux_capacitor.energy_generated = 1.1 lights.external.time_travel_imminent = True } speed.value,8,140 State = { flux_capacitor.energy_generated = 1.1 lights.external.time_travel_imminent = True speed.value = 140 } condition: (( speed.value >= (88 - 10) * 1.6 and speed.value < 88 * 1.6 ) or ( flux_capacitor.energy_generated >= 1.21 * 0.9 and flux_capacitor.energy_generated < 1.21 ) ) => True lights.internal.time_travel_imminent,5031,'True' State = { flux_capacitor.energy_generated = 1.1 lights.external.time_travel_imminent = True lights.internal.time_travel_imminent = True speed.value = 140 } condition: (( speed.value >= (88 - 10) * 1.6 and speed.value < 88 * 1.6 ) or ( flux_capacitor.energy_generated >= 1.21 * 0.9 and flux_capacitor.energy_generated < 1.21 ) ) => True lights.internal.time_travel_imminent,5031,'True' State = { flux_capacitor.energy_generated = 1.1 lights.external.time_travel_imminent = True lights.internal.time_travel_imminent = True speed.value = 140 } flux_capacitor.energy_generated,5030,'1.1' lights.external.time_travel_imminent,5032,'True' lights.external.time_travel_imminent,5032,'True' speed.value,8,'140' lights.internal.time_travel_imminent,5031,'True' lights.internal.time_travel_imminent,5031,'True' ''' self.run_vsm('subclauses_arithmetic_booleans', input_data, expected_output.strip() + '\n', False) def test_nested_child_before_parent(self): ''' Ensure that we can safely set a nested condition before its parent. Originally, this caused a crash. ''' input_data = 'horn = true' expected_output = ''' horn,20,True State = { horn = True } parent condition: parked == (unset) parent condition: car.stop == (unset) condition: (horn == True) => True horn,20,'true' ''' self.run_vsm('nested_simple', input_data, expected_output.strip() + '\n', wait_time_ms=1500) def test_start_0_child_unmet(self): ''' Ensure that we can use a start time of zero and meet its parent condition without crashing. ''' input_data = 'parked = true' expected_output = ''' parked,11,True State = { parked = True } condition not met by 'start' time of 0ms condition: (parked == True) => True parked,11,'true' ''' self.run_vsm('start_0', input_data, expected_output.strip() + '\n', wait_time_ms=1200) def test_start_0_child_met(self): ''' Ensure that we can use a start time of zero and meet the full chain of conditions without crashing. ''' input_data = 'horn = true\n' \ 'parked = true' expected_output = ''' horn,20,True State = { horn = True } parent condition: parked == (unset) condition: (horn == True) => True parked,11,True State = { horn = True parked = True } condition: (parked == True) => True horn,20,'true' parked,11,'true' ''' self.run_vsm('start_0', input_data, expected_output.strip() + '\n', wait_time_ms=1200) class VSMStdTests(VSMTestCases): ipc_class = TestVSMDebug class VSMZeroMQTests(VSMTestCases): ipc_class = TestVSMZeroMQ class VSMNoneSignalTests(TestVSM): ipc_class = TestVSMNoneSignal def test_none_signal(self): input_data = 'transmission.gear = "reverse"\nnot-acceptable' expected_output = ''' transmission.gear,9,'reverse' State = { transmission.gear = reverse } condition: (transmission.gear == 'reverse') => True car.backup,3,'True' State = { car.backup = True transmission.gear = reverse } skipping invalid message car.backup=True ''' self.run_vsm('simple0', input_data, expected_output.strip() + '\n') if __name__ == '__main__': for cls in [VSMStdTests, VSMZeroMQTests, VSMNoneSignalTests]: suite = unittest.TestLoader().loadTestsFromTestCase(cls) unittest.TextTestRunner(verbosity=2).run(suite)