diff --git a/.gitmodules b/.gitmodules index d62bfd9..9ad52c8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,7 @@ url = https://github.com/HyperloopUPV-H8/ST-LIB [submodule "Core/Inc/Communications/JSON_ADE"] path= Core/Inc/Communications/JSON_ADE - url = https://github.com/HyperloopUPV-H8/JSON_ADE \ No newline at end of file + url = https://github.com/HyperloopUPV-H8/JSON_ADE +[submodule "Tests/VirtualMCU"] + path = Tests/VirtualMCU + url = https://github.com/HyperloopUPV-H8/VirtualMCU diff --git a/Core/Src/main.cpp b/Core/Src/main.cpp index 15c27b2..0f791f0 100644 --- a/Core/Src/main.cpp +++ b/Core/Src/main.cpp @@ -7,8 +7,15 @@ #include "ST-LIB.hpp" int main(void) { +#ifdef SIM_ON + SharedMemory::start(); +#endif + + DigitalOutput led_on(PA1); STLIB::start(); + Time::register_low_precision_alarm(100, [&]() { led_on.toggle(); }); + while (1) { STLIB::update(); } diff --git a/Tests/VirtualMCU b/Tests/VirtualMCU new file mode 160000 index 0000000..fac66f1 --- /dev/null +++ b/Tests/VirtualMCU @@ -0,0 +1 @@ +Subproject commit fac66f1bfa86e65c0a7845f31b36dec31b61ccbf diff --git a/Tests/runner.py b/Tests/runner.py new file mode 100644 index 0000000..ea4ce90 --- /dev/null +++ b/Tests/runner.py @@ -0,0 +1,138 @@ +import subprocess +from argparse import ArgumentParser +import time + + +class DuplicatedTestError(Exception): + def __init__(self, name): + self._name = name + + def __str__(self): + return f"Test {self._name} is duplicated" + + +class UnitUnderTest: + def __init__(self, executable): + self._executable = executable + + def __enter__(self): + self._process = subprocess.Popen( + self._executable, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + def __exit__(self, *args): + try: + out, err = self._process.communicate(timeout=1) + except Exception: + self._process.kill() + try: + out, err = self._process.communicate(timeout=1) + except Exception: + out = "Error recovering stdout" + err = "Error recovering stderr" + + if out: + print(f" * UUT stdout:\n{out}") + if err: + print(f" * UUT stderr:\n{err}") + + +class Test: + def __init__(self, func): + self._func = func + self._prepare = None + self._cleanup = None + + def __call__(self, *args, **kwargs): + return self._func(*args, **kwargs) + + def prepare(self): + def decorator(prepare_func): + nonlocal self + self._prepare = prepare_func + return prepare_func + + return decorator + + def cleanup(self): + def decorator(cleanup_func): + nonlocal self + self._cleanup = cleanup_func + return cleanup_func + + return decorator + + def run_prepare(self): + if self._prepare is not None: + self._prepare() + + def run_cleanup(self): + if self._cleanup is not None: + self._cleanup() + +class TestRunner: + + def __init__(self, uut_executable): + self._prepare = {} + self._tests = {} + self._cleanup = {} + self._uut = UnitUnderTest(uut_executable) + + # Registers a new test in the runner, name is infered from the function name + def test(self): + def decorator(test_func): + nonlocal self + + if test_func.__name__ in self._tests: + raise DuplicatedTestError(test_func.__name__) + + self._tests[test_func.__name__] = Test(test_func) + + return self._tests[test_func.__name__] + + return decorator + + + # Runs all the registered tests, cleaning up after each test + def run(self): + for name, test in self._tests.items(): + try: + test.run_prepare() + + with self._uut: + time.sleep(0.1) + try: + print(f"[{name}] Running...") + result = test() + print(f"[{name}] Succesfull!") + if result is not None: + print(f" * Result: {result}") + except Exception as reason: + print(f"[{name}] Failed!") + print(f" * Reason: {reason}") + + test.run_cleanup() + except KeyboardInterrupt: + print(f"[{name}] Keyboard Interrupt. Aborted.") + + + +parser = ArgumentParser( + prog="test", + description="run multiple simulator tests on a target executable" +) + +parser.add_argument( + "-uut", + "--executable", + required=True, + help="full path to the target executable" +) + +args = parser.parse_args() + +runner = TestRunner(args.executable) diff --git a/Tests/test.py b/Tests/test.py new file mode 100644 index 0000000..aa6cdfc --- /dev/null +++ b/Tests/test.py @@ -0,0 +1,50 @@ +import sys, os +sys.path.append(os.path.join(os.path.dirname(__file__), "VirtualMCU", "src")) + +from runner import runner +from vmcu.shared_memory import SharedMemory +from vmcu.pin import Pinout, DigitalOut +from vmcu.services.digital_out import DigitalOutService +from vmcu.assertions import * + + +@runner.test() +def led_toggle(): + TOGGLE_PERIOD = milliseconds(100 * 2) + ALLOWED_SLACK = milliseconds(5) + + shm = SharedMemory("gpio__blinking_led", "state_machine__blinking_led") + led = DigitalOutService(shm, Pinout.PA1) + + def led_turns_on(): + nonlocal led + return led.get_pin_state() is DigitalOut.State.High + + def led_turns_off(): + nonlocal led + return led.get_pin_state() is DigitalOut.State.Low + + #sync with board + completes( + wait_until_true(led_turns_on), + before=(TOGGLE_PERIOD / 2) + ALLOWED_SLACK, + msg="Sync fails" + ) + + for i in range(150): + completes( + wait_until_true(led_turns_off), + before=(TOGGLE_PERIOD / 2) + ALLOWED_SLACK, + after=(TOGGLE_PERIOD / 2) - ALLOWED_SLACK, + msg="turns off" + ) + completes( + wait_until_true(led_turns_on), + before=(TOGGLE_PERIOD / 2) + ALLOWED_SLACK, + after=(TOGGLE_PERIOD / 2) - ALLOWED_SLACK, + msg="turns on" + ) + print("toggle", i) + + +runner.run() # Runs the tests, do not delete!