From 34d6c8ddfa5c1f18f48ab33193da5843b8b0d087 Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Wed, 12 Aug 2026 10:55:58 -0400 Subject: [PATCH 01/10] Clean up test helpers --- README.md | 2 +- src/fpy/state.py | 24 +- src/fpy/test_helpers.py | 834 +++++++++++++------------------ test/fpy/test_compiler_config.py | 49 ++ 4 files changed, 414 insertions(+), 495 deletions(-) diff --git a/README.md b/README.md index 73444af..704ea15 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ prm_3: U8 = Ref.sendBuffComp.parameter3 A significant limitation of this is that it will only return the value most recently saved to the parameter database. This means you must command `_PRM_SAVE` before the sequence will see the new value. -> Note: If a telemetry channel and parameter have the same fully-qualified name, the fully-qualified name will get the value of the telemetry channel +> Note: If two dictionary items have the same fully-qualified name, the name resolves to the first match in this order: telemetry channel, parameter, enum constant, FPP constant. ## Conditionals Fpy supports comparison operators: diff --git a/src/fpy/state.py b/src/fpy/state.py index 18e8490..4e6faef 100644 --- a/src/fpy/state.py +++ b/src/fpy/state.py @@ -507,6 +507,19 @@ def _populate_type_defaults(typ: FpyType) -> None: typ.elem_defaults = tuple(array_defaults) +def is_seq_run_cmd(cmd) -> bool: + """Whether a command is a sequence-run command, detected by its signature: + (fileName: string, block: Svc.BlockState, args: Svc.SeqArgs). The user + provides the target sequence's arguments in place of the SeqArgs param.""" + args = cmd.arguments + return ( + len(args) == 3 + and args[0][2].is_string + and args[1][2].name == "Svc.BlockState" + and args[2][2].name == "Svc.SeqArgs" + ) + + def _make_type_ctor(name: str, typ: FpyType) -> TypeCtorSymbol | None: """Create a TypeCtorSymbol for a type, or return None if it has no callable ctor.""" if typ.kind == TypeKind.STRUCT: @@ -548,6 +561,7 @@ def _build_global_scopes(dictionary: str) -> tuple: _validate_and_replace_type( dict_type_name_dict, "Fw.TimeComparison", TIME_COMPARISON ) + _validate_and_replace_type(dict_type_name_dict, "Fw.LogSeverity", LOG_SEVERITY) _validate_and_replace_type(dict_type_name_dict, "Svc.BlockState", BLOCK_STATE) _update_seq_args_from_dict(dict_type_name_dict) @@ -563,7 +577,6 @@ def _build_global_scopes(dictionary: str) -> tuple: BOOL.name: BOOL, CHECK_STATE.name: CHECK_STATE, FLAGS_TYPE.name: FLAGS_TYPE, - LOG_SEVERITY.name: LOG_SEVERITY, } # Collect enum constants from the final type dict (after builtins and @@ -585,14 +598,7 @@ def _build_global_scopes(dictionary: str) -> tuple: for name, cmd in cmd_name_dict.items(): args = [(arg_name, arg_type, None) for arg_name, _, arg_type in cmd.arguments] - # Detect sequence-run commands by matching the 3-arg signature: - # (fileName: string, block: Svc.BlockState, args: Svc.SeqArgs) - if ( - len(args) == 3 - and args[0][1].is_string - and args[1][1].name == "Svc.BlockState" - and args[2][1].name == "Svc.SeqArgs" - ): + if is_seq_run_cmd(cmd): # Strip the SeqArgs param; user provides varargs instead fixed_args = args[:2] callable_name_dict[name] = CommandSymbol( diff --git a/src/fpy/test_helpers.py b/src/fpy/test_helpers.py index 8c42e6b..aa43b0a 100644 --- a/src/fpy/test_helpers.py +++ b/src/fpy/test_helpers.py @@ -1,26 +1,61 @@ +"""Helpers for fpy tests: compile sequences, run them on the test harnesses +(or a live GDS deployment with --use-gds), and assert success or failure.""" + from __future__ import annotations -from pathlib import Path + +import json +import re import tempfile +from pathlib import Path + import fpy.error -from fpy.harness import HarnessError, fpy_harness +from fpy.bytecode.assembler import serialize_directives from fpy.bytecode.directives import ( AllocateDirective, Directive, DirectiveErrorCode, + FwOpcodeType, GotoDirective, PushValDirective, ) from fpy.compiler import ( - text_to_ast, - analyze_ast, analysis_to_fpybc_directives, analysis_to_wasm, + analyze_ast, + text_to_ast, ) -from fpy.state import CompileState, get_base_compile_state -from fpy.bytecode.assembler import serialize_directives from fpy.dictionary import load_dictionary from fpy.error import WarningType -from fpy.types import FpyType, FpyValue +from fpy.harness import HarnessError, fpy_harness, wasm_harness +from fpy.state import CompileState, get_base_compile_state +from fpy.types import CmdDef, FpyType, FpyValue, TypeKind + +default_dictionary = str( + Path(__file__).parent.parent.parent / "test" / "fpy" / "RefTopologyDictionary.json" +) + +# Flipped to True by conftest's pytest_configure when --wasm is passed, routing +# the assert_* helpers through the LLVM/wasm backend (run on the real +# Svc::WasmSequencer through the wasm harness) instead of the bytecode VM. +USE_WASM = False + +# Fw.CmdResponse enum values. +CMD_RESPONSE_OK = 0 +CMD_RESPONSE_EXECUTION_ERROR = 4 + + +class CompilationFailed(Exception): + """Raised when compilation fails expectedly (parse error or semantic error).""" + + +class ValidationError(Exception): + """Raised when the sequencer rejects a sequence during validation (bad + file, bad CRC, argument size mismatch, ...), before running anything.""" + + +# --------------------------------------------------------------------------- +# Compiling +# --------------------------------------------------------------------------- # Every known warning type. Tests fail on ANY warning by default: the compile # helpers promote every warning to a hard error unless the caller declares it in @@ -49,32 +84,9 @@ def _assert_expected_emitted(state, expected_warnings): assert not missing, f"expected warnings not emitted: {missing} (got {emitted})" -default_dictionary = str( - Path(__file__).parent.parent.parent / "test" / "fpy" / "RefTopologyDictionary.json" -) - - -class CompilationFailed(Exception): - """Raised when compilation fails expectedly (parse error or semantic error).""" - - pass - - -class ValidationError(Exception): - """Raised when the sequencer rejects a sequence during validation (bad - file, bad CRC, argument size mismatch, ...), before running anything.""" - - pass - - -# Flipped to True by conftest's pytest_configure when --wasm is passed, routing -# the assert_* helpers through the LLVM/wasm backend (run on the real -# Svc::WasmSequencer through the wasm harness) instead of the bytecode VM. -USE_WASM = False - - -def compile_seq( +def _compile( seq: str, + to_wasm: bool, ground_binary_dir: str = None, ignored_warnings=None, error_warnings=None, @@ -82,8 +94,9 @@ def compile_seq( import_directories: list[str] | None = None, main_file_dir: str | None = None, main_file_path: str | None = None, -) -> tuple[CompileState, list[Directive], list[tuple[str, FpyType]]]: - """Compile a sequence string and return (state, directives, arg_types). +): + """Compile a sequence string and return (state, backend output): the wasm + binary bytes when *to_wasm*, else (directives, arg_types). By default every warning is a hard error; pass *expected_warnings* to allow (and still collect) specific ones.""" @@ -104,274 +117,92 @@ def compile_seq( try: body = text_to_ast(seq) state = analyze_ast(body, state) - directives, arg_types = analysis_to_fpybc_directives(state) + if to_wasm: + output, _ = analysis_to_wasm(state) + else: + output = analysis_to_fpybc_directives(state) except (fpy.error.CompileError, fpy.error.BackendError) as e: raise CompilationFailed(f"Compilation failed:\n{e}") _assert_expected_emitted(state, expected_warnings) - return state, directives, arg_types - + return state, output -def compile_seq_wasm( - seq: str, - ground_binary_dir: str = None, - import_directories: list[str] | None = None, - ignored_warnings=None, - error_warnings=None, - expected_warnings=None, - main_file_dir: str | None = None, -) -> bytes: - """Compile a sequence string to a runnable wasm binary (the LLVM backend). - - By default every warning is a hard error; pass *expected_warnings* to allow - (and still collect) specific ones.""" - fpy.error.file_name = "" - state = get_base_compile_state( - default_dictionary, - ground_binary_dir, - ignored_warnings=ignored_warnings, - error_warnings=_default_error_warnings( - error_warnings, ignored_warnings, expected_warnings - ), - import_directories=import_directories, - main_file_dir=main_file_dir, - ) +def compile_seq( + seq: str, **kwargs +) -> tuple[CompileState, list[Directive], list[tuple[str, FpyType]]]: + """Compile a sequence string to fpy bytecode. Returns + (state, directives, arg_types). See _compile for the keyword args.""" + state, (directives, arg_types) = _compile(seq, to_wasm=False, **kwargs) + return state, directives, arg_types - try: - body = text_to_ast(seq) - state = analyze_ast(body, state) - wasm, _ = analysis_to_wasm(state) - except (fpy.error.CompileError, fpy.error.BackendError) as e: - raise CompilationFailed(f"Compilation failed:\n{e}") - _assert_expected_emitted(state, expected_warnings) +def compile_seq_wasm(seq: str, **kwargs) -> bytes: + """Compile a sequence string to a runnable wasm binary (the LLVM backend). + See _compile for the keyword args.""" + _, wasm = _compile(seq, to_wasm=True, **kwargs) return wasm -def run_seq_wasm( - seq: str, - ground_binary_dir: str = None, - import_directories: list[str] | None = None, - expected_warnings=None, - main_file_dir: str | None = None, - failing_opcodes: set[int] = None, -) -> int: - """Compile *seq* to wasm and run it, returning the sequence's error code - (reported via the exit/panic host imports; 0 when the void entrypoint - falls off its end without failing). - - Runs the compiled module on a real Svc::WasmSequencer through the wasm - harness built by conftest.""" - code, _, _ = _run_seq_wasm( - seq, - ground_binary_dir, - import_directories=import_directories, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - failing_opcodes=failing_opcodes, - ) - return code - - -def run_seq_wasm_with_events( - seq: str, - ground_binary_dir: str = None, - import_directories: list[str] | None = None, - expected_warnings=None, - main_file_dir: str | None = None, -) -> tuple[int, list[tuple[int, str]]]: - """Like run_seq_wasm, but also returns the events the sequence reported - through the event host import (the log() builtin) as (severity, message) - pairs, in call order.""" - code, events, _ = _run_seq_wasm( - seq, - ground_binary_dir, - import_directories=import_directories, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - ) - return code, events - - -def run_seq_wasm_with_cmds( - seq: str, - ground_binary_dir: str = None, - import_directories: list[str] | None = None, - expected_warnings=None, - main_file_dir: str | None = None, - failing_opcodes: set[int] = None, - cmd_response: int = None, -) -> tuple[int, list[bytes]]: - """Like run_seq_wasm, but also returns the command buffers the sequence - dispatched through the cmd host import (the big-endian serialized - FwOpcodeType + arguments), in call order. Every command completes with - *cmd_response* (an Fw.CmdResponse value, default OK) unless its opcode is - in *failing_opcodes*, which makes it complete with EXECUTION_ERROR.""" - code, _, cmds = _run_seq_wasm( - seq, - ground_binary_dir, - import_directories=import_directories, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - failing_opcodes=failing_opcodes, - cmd_response=cmd_response, - ) - return code, cmds - +# --------------------------------------------------------------------------- +# Compiled sequence files +# --------------------------------------------------------------------------- -def _run_seq_wasm( - seq: str, - ground_binary_dir: str = None, - import_directories: list[str] | None = None, - expected_warnings=None, - main_file_dir: str | None = None, - failing_opcodes: set[int] = None, - cmd_response: int = None, -) -> tuple[int, list[tuple[int, str]], list[bytes]]: - """Compile *seq* to wasm, run it through the spacewasm runner harness, and - return (error code, reported events, dispatched command buffers). - - The commands that fail are *failing_opcodes* plus the RUN commands that - always fail when called from within a running sequence on the same - sequencer instance -- the same set the bytecode reference model uses.""" - wasm = compile_seq_wasm( - seq, - ground_binary_dir, - import_directories=import_directories, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - ) - return run_wasm(wasm, failing_opcodes=failing_opcodes, cmd_response=cmd_response) - - -def run_wasm( - wasm: bytes, - failing_opcodes: set[int] = None, - cmd_response: int = None, -) -> tuple[int, list[tuple[int, str]], list[bytes]]: - """Run an already-linked wasm module on a real Svc::WasmSequencer through - the wasm harness and return (error code, reported events, dispatched - command buffers). - - The commands that fail are *failing_opcodes* plus the RUN commands that - always fail when called from within a running sequence on the same - sequencer instance.""" - from fpy.harness import wasm_harness +# One scratch directory per test session for compiled sequence files. The +# harness runs with this as its working directory and gets the short relative +# file name, because the RUN command's file path argument is a command string, +# which F Prime silently caps at FW_CMD_STRING_MAX_SIZE (40) characters. +_scratch_dir: tempfile.TemporaryDirectory | None = None - d = load_dictionary(default_dictionary) - always_failing = {d["cmd_name_dict"]["Ref.cmdSeq0.RUN"].opcode} - seq_dir, seq_file = _write_wasm_for_harness(wasm) - request = { - "seqFile": seq_file, - "cwd": seq_dir, - "time": {"base": 0, "context": 0, "seconds": 0, "useconds": 0}, - "failOpcodes": sorted(always_failing | set(failing_opcodes or ())), - } - if cmd_response is not None: - request["cmdResponse"] = cmd_response +def _write_for_harness( + data: bytes, name: str, directory: str = None +) -> tuple[str, str]: + """Write *data* to / (the per-session scratch directory by + default) and return (directory, name).""" + global _scratch_dir + if directory is None: + if _scratch_dir is None: + _scratch_dir = tempfile.TemporaryDirectory(prefix="fpy-harness-") + directory = _scratch_dir.name + Path(directory, name).write_bytes(data) + return directory, name - result = wasm_harness().run(request) - if "error" in result: - raise HarnessError(result["error"]) - if "cmdResponse" not in result: - raise HarnessError(f"wasm harness gave no command response: {result}") +def _write_tmpfile(data: bytes, suffix: str) -> str: + """Write *data* to a temp file and return its path.""" + f = tempfile.NamedTemporaryFile(suffix=suffix, delete=False) + f.write(data) + f.close() + return f.name - # The guest-flagged events are the ones the sequence itself logged; the - # rest are the sequencer's own reporting. - events = [(e["severity"], e["text"]) for e in result["events"] if e.get("guest")] - cmds = [bytes.fromhex(c) for c in result["cmds"]] - if result["cmdResponse"] == CMD_RESPONSE_OK: - return 0, events, cmds - if "exitCode" in result: - # The code the sequence passed to the exit or panic host import, - # reported through the SequenceExitedWithError event. - return result["exitCode"], events, cmds - raise HarnessError( - "wasm sequence failed without an exit code (interpreter trap): " - + "; ".join(e["text"] for e in result["events"]) - ) - - -def lookup_type(fprime_test_api, type_name: str): - d = load_dictionary(default_dictionary) - return d["type_defs"][type_name] - - -def _write_wasm_to_tmpfile(wasm: bytes) -> str: - """Write a compiled wasm module to a temp .wasm file and return its path.""" - wasm_file = tempfile.NamedTemporaryFile(suffix=".wasm", delete=False) - wasm_file.write(wasm) - wasm_file.close() - return wasm_file.name - - -def _write_wasm_for_harness(wasm: bytes) -> tuple[str, str]: - """Write a compiled wasm module to /m0.wasm and return - (directory, file name); like sequence files, the module travels to the - sequencer as a short relative name because the RUN command's file path - argument is capped at FW_CMD_STRING_MAX_SIZE characters.""" - global _seq_scratch_dir - if _seq_scratch_dir is None: - _seq_scratch_dir = tempfile.TemporaryDirectory(prefix="fpy-harness-") - name = "m0.wasm" - Path(_seq_scratch_dir.name, name).write_bytes(wasm) - return _seq_scratch_dir.name, name - - -def _write_seq_to_tmpfile( - directives: list[Directive], arg_types: list[tuple[str, FpyType]] = None -) -> str: - """Serialize directives to a temp .bin file and return its path.""" +def _serialize_seq( + directives: list[Directive], arg_types: list[tuple[str, FpyType]] +) -> bytes: + """Serialize directives (with the sequence arg specs) to .bin file bytes.""" arg_specs = [(name, t.name, t.max_size) for name, t in (arg_types or [])] - seq_file = tempfile.NamedTemporaryFile(suffix=".bin", delete=False) - Path(seq_file.name).write_bytes( - serialize_directives(directives, arg_specs=arg_specs)[0] - ) - return seq_file.name + return serialize_directives(directives, arg_specs=arg_specs)[0] -def _build_seq_args_json(args: bytes) -> str: - """Build a JSON string for the Svc.SeqArgs struct expected by RUN_ARGS.""" - import json - - buf = list(args) + [0] * (255 - len(args)) - return json.dumps({"size": len(args), "buffer": buf}) - +def _serialize_args(args: list[FpyValue] | None) -> bytes | None: + """Serialize a list of sequence argument values to bytes.""" + if args is None: + return None + return b"".join(v.serialize() for v in args) -# Fw.CmdResponse enum values. -CMD_RESPONSE_OK = 0 -CMD_RESPONSE_EXECUTION_ERROR = 4 -# One scratch directory per test session for compiled sequence files. The -# harness runs with this as its working directory and gets the short relative -# file name, because the RUN command's file path argument is a command string, -# which F Prime silently caps at FW_CMD_STRING_MAX_SIZE (40) characters. -_seq_scratch_dir: tempfile.TemporaryDirectory | None = None +# --------------------------------------------------------------------------- +# Running +# --------------------------------------------------------------------------- -# FIXME again should be fpybc. it's all either wasm or fpybc -def _write_seq_for_harness( - directives: list[Directive], - arg_types: list[tuple[str, FpyType]] = None, - directory: str = None, -) -> tuple[str, str]: - """Serialize directives to /s0.bin (a per-session scratch - directory by default) and return (directory, file name).""" - global _seq_scratch_dir - if directory is None: - if _seq_scratch_dir is None: - _seq_scratch_dir = tempfile.TemporaryDirectory(prefix="fpy-harness-") - directory = _seq_scratch_dir.name - arg_specs = [(name, t.name, t.max_size) for name, t in (arg_types or [])] - name = "s0.bin" - Path(directory, name).write_bytes( - serialize_directives(directives, arg_specs=arg_specs)[0] - ) - return directory, name +def _always_failing_opcodes(failing_opcodes) -> set[int]: + """The opcodes the harness completes with EXECUTION_ERROR: the RUN + commands that always fail when called from within a running sequence on + the same sequencer instance, plus the caller's *failing_opcodes*.""" + d = load_dictionary(default_dictionary) + return {d["cmd_name_dict"]["Ref.cmdSeq0.RUN"].opcode} | set(failing_opcodes or ()) def _seq_args_buffer_len(d: dict) -> int: @@ -405,70 +236,47 @@ def _expected_stack_bytes(directives: list[Directive], args: bytes | None) -> in return len(args or b"") + setup_size +def _as_int(v) -> int: + """An error code as a plain int, whether it is a DirectiveErrorCode or + already an int.""" + return v.value if isinstance(v, DirectiveErrorCode) else v + + def run_seq( - fprime_test_api, directives: list[Directive], tlm: dict[str, bytes] = None, time_base: int = 0, time_context: int = 0, initial_time_us: int = 0, - timeout_s: int = 4, failing_opcodes: set[int] = None, args: bytes = None, + arg_types: list[tuple[str, FpyType]] = None, seq_run_opcodes: set[int] = None, - arg_name_types: list[tuple[str, FpyType]] = None, ground_binary_dir: str = None, -): - """Run a list of directives. - - When fprime_test_api is None (the default), runs against a real - Svc::FpySequencer through the test harness (test/harness). When - fprime_test_api is a live IntegrationTestAPI (i.e. --use-gds was passed - to pytest), serializes the directives to a temp file and sends them to - the running GDS deployment. + prms: dict[str, bytes] = None, +) -> list[bytes]: + """Run a list of directives on a real Svc::FpySequencer through the test + harness (test/harness). *tlm* and *prms* map channel/parameter names to + the serialized values the harness answers reads with. Returns the command + buffers the sequence dispatched (the big-endian serialized FwOpcodeType + + arguments), in call order. Raises ValidationError when the sequencer rejects the sequence before running it, and RuntimeError when the sequence fails: with the DirectiveErrorCode for a trap, or the raw error code int for a nonzero exit. """ - if tlm is None: - tlm = {} - - if fprime_test_api is not None: - seq_path = _write_seq_to_tmpfile(directives, arg_name_types) - if args: - seq_args = _build_seq_args_json(args) - fprime_test_api.send_and_assert_command( - "Ref.seqDisp.RUN_ARGS", [seq_path, "BLOCK", seq_args], timeout=timeout_s - ) - else: - fprime_test_api.send_and_assert_command( - "Ref.seqDisp.RUN", [seq_path, "BLOCK"], timeout=timeout_s - ) - return - d = load_dictionary(default_dictionary) - ch_name_dict = d["ch_name_dict"] - # These RUN commands always fail when called from within a running sequence - # on the same sequencer instance; the harness completes them with - # EXECUTION_ERROR. - always_failing = { - d["cmd_name_dict"]["Ref.cmdSeq0.RUN"].opcode, - } - if failing_opcodes: - always_failing |= failing_opcodes - - # The sequence file always sits in the harness's working directory and is - # named by its short relative name: the RUN command's file path argument - # is a command string, which F Prime silently caps at - # FW_CMD_STRING_MAX_SIZE (40) characters. When the test provides a - # ground_binary_dir, that directory doubles as the working directory so - # child sequence files resolve against it, like they did against the - # model's cwd. - seq_dir, seq_file = _write_seq_for_harness( - directives, arg_name_types, directory=ground_binary_dir + + # When the test provides a ground_binary_dir, that directory doubles as + # the harness's working directory so child sequence files resolve against + # it, like they did against the compiler's ground_binary_dir. + seq_dir, seq_file = _write_for_harness( + _serialize_seq(directives, arg_types), "s0.bin", directory=ground_binary_dir ) + if seq_run_opcodes is None and ground_binary_dir is not None: + seq_run_opcodes = {d["cmd_name_dict"]["Ref.seqDisp.RUN_ARGS"].opcode} + request = { "seqFile": seq_file, "cwd": seq_dir, @@ -479,10 +287,14 @@ def run_seq( "useconds": initial_time_us % 1_000_000, }, "tlm": { - str(ch_name_dict[chan_name].ch_id): bytes(val).hex() - for chan_name, val in tlm.items() + str(d["ch_name_dict"][chan_name].ch_id): bytes(val).hex() + for chan_name, val in (tlm or {}).items() }, - "failOpcodes": sorted(always_failing), + "prms": { + str(d["prm_name_dict"][prm_name].prm_id): bytes(val).hex() + for prm_name, val in (prms or {}).items() + }, + "failOpcodes": sorted(_always_failing_opcodes(failing_opcodes)), } if args is not None: request["args"] = args.hex() @@ -517,7 +329,7 @@ def run_seq( actual_stack = len(bytes.fromhex(result["stack"])) if actual_stack != expected_stack: raise RuntimeError(f"Sequence leaked {actual_stack - expected_stack} bytes") - return + return [bytes.fromhex(c) for c in result["cmds"]] if response != CMD_RESPONSE_EXECUTION_ERROR: raise HarnessError(f"unexpected response {response} to the RUN command") @@ -537,24 +349,147 @@ def run_seq( raise RuntimeError(DirectiveErrorCode(result["lastDirectiveError"])) -def assert_compile_success( - fprime_test_api, - seq: str, - import_directories: list[str] | None = None, - expected_warnings=None, -): - if USE_WASM: - compile_seq_wasm( - seq, - import_directories=import_directories, - expected_warnings=expected_warnings, +def run_wasm( + wasm: bytes, + failing_opcodes: set[int] = None, + cmd_response: int = None, +) -> tuple[int, list[tuple[int, str]], list[bytes]]: + """Run an already-linked wasm module on a real Svc::WasmSequencer through + the wasm harness and return (error code, reported events, dispatched + command buffers). + + Every command completes with *cmd_response* (an Fw.CmdResponse value, + default OK) unless its opcode is in *failing_opcodes*, which makes it + complete with EXECUTION_ERROR.""" + seq_dir, seq_file = _write_for_harness(wasm, "m0.wasm") + request = { + "seqFile": seq_file, + "cwd": seq_dir, + "time": {"base": 0, "context": 0, "seconds": 0, "useconds": 0}, + "failOpcodes": sorted(_always_failing_opcodes(failing_opcodes)), + } + if cmd_response is not None: + request["cmdResponse"] = cmd_response + + result = wasm_harness().run(request) + + if "error" in result: + raise HarnessError(result["error"]) + if "cmdResponse" not in result: + raise HarnessError(f"wasm harness gave no command response: {result}") + + # The guest-flagged events are the ones the sequence itself logged; the + # rest are the sequencer's own reporting. + events = [(e["severity"], e["text"]) for e in result["events"] if e.get("guest")] + cmds = [bytes.fromhex(c) for c in result["cmds"]] + + if result["cmdResponse"] == CMD_RESPONSE_OK: + return 0, events, cmds + if "exitCode" in result: + # The code the sequence passed to the exit or panic host import, + # reported through the SequenceExitedWithError event. + return result["exitCode"], events, cmds + raise HarnessError( + "wasm sequence failed without an exit code (interpreter trap): " + + "; ".join(e["text"] for e in result["events"]) + ) + + +def _run_seq_wasm( + seq: str, failing_opcodes: set[int] = None, cmd_response: int = None, **kwargs +) -> tuple[int, list[tuple[int, str]], list[bytes]]: + """Compile *seq* to wasm and run it through the wasm harness. Returns + (error code, reported events, dispatched command buffers). See _compile + for the remaining keyword args.""" + wasm = compile_seq_wasm(seq, **kwargs) + return run_wasm(wasm, failing_opcodes=failing_opcodes, cmd_response=cmd_response) + + +def run_seq_wasm(seq: str, **kwargs) -> int: + """Compile *seq* to wasm and run it, returning the sequence's error code + (reported via the exit/panic host imports; 0 when the void entrypoint + falls off its end without failing).""" + code, _, _ = _run_seq_wasm(seq, **kwargs) + return code + + +def run_seq_wasm_with_events(seq: str, **kwargs) -> tuple[int, list[tuple[int, str]]]: + """Like run_seq_wasm, but also returns the events the sequence reported + through the event host import (the log() builtin) as (severity, message) + pairs, in call order.""" + code, events, _ = _run_seq_wasm(seq, **kwargs) + return code, events + + +def run_seq_wasm_with_cmds(seq: str, **kwargs) -> tuple[int, list[bytes]]: + """Like run_seq_wasm, but also returns the command buffers the sequence + dispatched through the cmd host import (the big-endian serialized + FwOpcodeType + arguments), in call order.""" + code, _, cmds = _run_seq_wasm(seq, **kwargs) + return code, cmds + + +# --------------------------------------------------------------------------- +# Running on a live GDS deployment (--use-gds) +# --------------------------------------------------------------------------- + + +def _build_seq_args_json(args: bytes) -> str: + """Build a JSON string for the Svc.SeqArgs struct expected by RUN_ARGS.""" + buf = list(args) + [0] * (255 - len(args)) + return json.dumps({"size": len(args), "buffer": buf}) + + +def _run_gds(fprime_test_api, file_path, args, wasm, expect_ok, timeout_s=4): + """Send a compiled sequence file to a live GDS deployment. With + *expect_ok*, assert the RUN command succeeds; otherwise assert it fails + with an OpCodeError event.""" + if wasm: + cmd, cmd_args = "Ref.wasmSeq.RUN", [file_path, "BLOCK"] + elif args: + cmd, cmd_args = ( + "Ref.seqDisp.RUN_ARGS", + [file_path, "BLOCK", _build_seq_args_json(args)], + ) + else: + cmd, cmd_args = "Ref.seqDisp.RUN", [file_path, "BLOCK"] + if expect_ok: + fprime_test_api.send_and_assert_command(cmd, cmd_args, timeout=timeout_s) + else: + fprime_test_api.send_and_assert_event( + cmd, cmd_args, events="CdhCore.cmdDisp.OpCodeError", timeout=timeout_s ) + + +# --------------------------------------------------------------------------- +# Asserts +# --------------------------------------------------------------------------- + + +def lookup_type(type_name: str) -> FpyType: + """Look up a type from the test dictionary by name.""" + return load_dictionary(default_dictionary)["type_defs"][type_name] + + +def assert_compile_success(fprime_test_api, seq: str, **kwargs): + """Compile *seq* on the current backend. See _compile for the keyword + args.""" + if USE_WASM: + compile_seq_wasm(seq, **kwargs) + else: + compile_seq(seq, **kwargs) + + +def assert_compile_failure(fprime_test_api, seq: str, match: str = None, **kwargs): + """Compile *seq* on the current backend and assert it fails, optionally + matching the error message against the *match* regex.""" + try: + assert_compile_success(fprime_test_api, seq, **kwargs) + except (SystemExit, CompilationFailed) as e: + if match is not None: + assert re.search(match, str(e)), f"Expected match {match!r} in {e!r}" return - compile_seq( - seq, - import_directories=import_directories, - expected_warnings=expected_warnings, - ) + raise RuntimeError("compile_seq succeeded") def assert_run_success( @@ -572,104 +507,64 @@ def assert_run_success( import_directories: list[str] | None = None, expected_warnings=None, main_file_dir: str | None = None, -): + prms: dict[str, bytes] = None, +) -> list[bytes] | None: + """Compile *seq* on the current backend, run it, and assert it succeeds. + Returns the command buffers the sequence dispatched, or None when running + against a live GDS deployment. + + Runs on the test harness by default, or against a live GDS deployment + when fprime_test_api is not None (--use-gds).""" + compile_kwargs = dict( + ground_binary_dir=ground_binary_dir, + import_directories=import_directories, + expected_warnings=expected_warnings, + main_file_dir=main_file_dir, + ) if USE_WASM: + wasm = compile_seq_wasm(seq, **compile_kwargs) if fprime_test_api is not None: - wasm = compile_seq_wasm( - seq, - ground_binary_dir=ground_binary_dir, - import_directories=import_directories, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - ) - wasm_path = _write_wasm_to_tmpfile(wasm) - fprime_test_api.send_and_assert_command( - "Ref.wasmSeq.RUN", [wasm_path, "BLOCK"], timeout=timeout_s + _run_gds( + fprime_test_api, + _write_tmpfile(wasm, ".wasm"), + None, + wasm=True, + expect_ok=True, + timeout_s=timeout_s, ) return - code = run_seq_wasm( - seq, - ground_binary_dir=ground_binary_dir, - import_directories=import_directories, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - failing_opcodes=failing_opcodes, - ) + code, _, cmds = run_wasm(wasm, failing_opcodes=failing_opcodes) if code != DirectiveErrorCode.NO_ERROR.value: raise RuntimeError(f"wasm sequence returned error code {code}") - return - _, directives, arg_name_types = compile_seq( - seq, - ground_binary_dir=ground_binary_dir, - import_directories=import_directories, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - ) - args_bytes = None - if args is not None: - args_bytes = b"".join(v.serialize() for v in args) - if seq_run_opcodes is None and ground_binary_dir is not None: - d = load_dictionary(default_dictionary) - seq_run_opcodes = {d["cmd_name_dict"]["Ref.seqDisp.RUN_ARGS"].opcode} - run_seq( - fprime_test_api, + return cmds + + _, directives, arg_types = compile_seq(seq, **compile_kwargs) + args_bytes = _serialize_args(args) + if fprime_test_api is not None: + _run_gds( + fprime_test_api, + _write_tmpfile(_serialize_seq(directives, arg_types), ".bin"), + args_bytes, + wasm=False, + expect_ok=True, + timeout_s=timeout_s, + ) + return None + return run_seq( directives, tlm, time_base, time_context, initial_time_us, - timeout_s, failing_opcodes, args=args_bytes, - arg_name_types=arg_name_types, + arg_types=arg_types, seq_run_opcodes=seq_run_opcodes, ground_binary_dir=ground_binary_dir, + prms=prms, ) -def assert_compile_failure( - fprime_test_api, - seq: str, - match: str = None, - ground_binary_dir: str = None, - import_directories: list[str] | None = None, - ignored_warnings=None, - error_warnings=None, - expected_warnings=None, - main_file_dir: str | None = None, -): - try: - if USE_WASM: - compile_seq_wasm( - seq, - ground_binary_dir=ground_binary_dir, - import_directories=import_directories, - ignored_warnings=ignored_warnings, - error_warnings=error_warnings, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - ) - else: - compile_seq( - seq, - ground_binary_dir=ground_binary_dir, - import_directories=import_directories, - ignored_warnings=ignored_warnings, - error_warnings=error_warnings, - expected_warnings=expected_warnings, - main_file_dir=main_file_dir, - ) - except (SystemExit, CompilationFailed) as e: - if match is not None: - import re - - assert re.search(match, str(e)), f"Expected match {match!r} in {e!r}" - return - - # no error was generated - raise RuntimeError("compile_seq succeeded") - - def assert_run_failure( fprime_test_api, seq: str, @@ -682,6 +577,10 @@ def assert_run_failure( seq_run_opcodes: set[int] = None, import_directories: list[str] | None = None, ): + """Compile *seq* on the current backend, run it, and assert it fails: + with *error_code* (a DirectiveErrorCode trap or a raw exit code int), or + with *validation_error* when the sequencer must reject the sequence + before running it.""" assert not ( error_code is not None and validation_error ), "Cannot specify both error_code and validation_error" @@ -689,81 +588,49 @@ def assert_run_failure( error_code is not None or validation_error ), "Must specify either error_code or validation_error" + compile_kwargs = dict( + ground_binary_dir=ground_binary_dir, import_directories=import_directories + ) if USE_WASM: + wasm = compile_seq_wasm(seq, **compile_kwargs) if fprime_test_api is not None: - # GDS mode: send the wasm module and assert that it fails via - # OpCodeError event, mirroring the bytecode GDS failure path. - wasm = compile_seq_wasm( - seq, - ground_binary_dir=ground_binary_dir, - import_directories=import_directories, - ) - wasm_path = _write_wasm_to_tmpfile(wasm) - fprime_test_api.send_and_assert_event( - "Ref.wasmSeq.RUN", - [wasm_path, "BLOCK"], - events="CdhCore.cmdDisp.OpCodeError", - timeout=4, + _run_gds( + fprime_test_api, + _write_tmpfile(wasm, ".wasm"), + None, + wasm=True, + expect_ok=False, ) return # The wasm backend has no separate validation step or VM-internal # faults: a failed sequence is one that reports a nonzero code # through the exit/fault host imports. - code = run_seq_wasm( - seq, - ground_binary_dir=ground_binary_dir, - import_directories=import_directories, - failing_opcodes=failing_opcodes, - ) + code, _, _ = run_wasm(wasm, failing_opcodes=failing_opcodes) if code == DirectiveErrorCode.NO_ERROR.value: raise RuntimeError("wasm sequence succeeded") - if error_code is not None: - if ( - isinstance(error_code, DirectiveErrorCode) and code != error_code.value - ) or (isinstance(error_code, int) and code != error_code): - raise RuntimeError( - f"wasm sequence returned {code}, expected {error_code}" - ) + if error_code is not None and code != _as_int(error_code): + raise RuntimeError(f"wasm sequence returned {code}, expected {error_code}") return - _, directives, arg_name_types = compile_seq( - seq, ground_binary_dir=ground_binary_dir, import_directories=import_directories - ) - args_bytes = None - if args is not None: - args_bytes = b"".join(v.serialize() for v in args) - if seq_run_opcodes is None and ground_binary_dir is not None: - d = load_dictionary(default_dictionary) - seq_run_opcodes = {d["cmd_name_dict"]["Ref.seqDisp.RUN_ARGS"].opcode} - + _, directives, arg_types = compile_seq(seq, **compile_kwargs) + args_bytes = _serialize_args(args) if fprime_test_api is not None: - # GDS mode: send the sequence and assert that it fails via OpCodeError event - seq_path = _write_seq_to_tmpfile(directives, arg_name_types) - if args_bytes: - seq_args = _build_seq_args_json(args_bytes) - fprime_test_api.send_and_assert_event( - "Ref.seqDisp.RUN_ARGS", - [seq_path, "BLOCK", seq_args], - events="CdhCore.cmdDisp.OpCodeError", - timeout=4, - ) - else: - fprime_test_api.send_and_assert_event( - "Ref.seqDisp.RUN", - [seq_path, "BLOCK"], - events="CdhCore.cmdDisp.OpCodeError", - timeout=4, - ) + _run_gds( + fprime_test_api, + _write_tmpfile(_serialize_seq(directives, arg_types), ".bin"), + args_bytes, + wasm=False, + expect_ok=False, + ) return try: run_seq( - fprime_test_api, directives, initial_time_us=initial_time_us, failing_opcodes=failing_opcodes, args=args_bytes, - arg_name_types=arg_name_types, + arg_types=arg_types, seq_run_opcodes=seq_run_opcodes, ground_binary_dir=ground_binary_dir, ) @@ -775,13 +642,10 @@ def assert_run_failure( except RuntimeError as e: if validation_error: raise RuntimeError("Expected ValidationError, got", type(e).__name__, e) - - # The failure surfaces as either a DirectiveErrorCode trap or a raw exit - # code int; the expected value may likewise be either. Compare by integer - # value so e.g. an exit code of 7 matches DirectiveErrorCode.EXIT_WITH_ERROR. - def _as_int(v): - return v.value if isinstance(v, DirectiveErrorCode) else v - + # The failure surfaces as either a DirectiveErrorCode trap or a raw + # exit code int; the expected value may likewise be either. Compare by + # integer value so e.g. an exit code of 7 matches + # DirectiveErrorCode.EXIT_WITH_ERROR. if len(e.args) == 1 and _as_int(e.args[0]) != _as_int(error_code): raise RuntimeError( "run_seq failed with error", e.args[0], "expected", error_code diff --git a/test/fpy/test_compiler_config.py b/test/fpy/test_compiler_config.py index 557abe0..cfa271b 100644 --- a/test/fpy/test_compiler_config.py +++ b/test/fpy/test_compiler_config.py @@ -532,6 +532,55 @@ def test_timebase_tb_none_wrong_value_raises_error(): _clear_caches() +def test_log_severity_mismatch_raises_error(): + """Test that a Fw.LogSeverity that doesn't match the canonical one raises an error.""" + _clear_caches() + + with open(DEFAULT_DICTIONARY, "r") as f: + base_dict = json.load(f) + for type_def in base_dict.get("typeDefinitions", []): + if type_def.get("qualifiedName") == "Fw.LogSeverity": + type_def["enumeratedConstants"][0]["value"] = 42 # FATAL is 1 + break + temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) + json.dump(base_dict, temp_file) + temp_file.close() + + try: + from fpy.error import DictionaryError + + with pytest.raises(DictionaryError, match="Fw.LogSeverity"): + get_base_compile_state(temp_file.name, {}) + finally: + Path(temp_file.name).unlink() + _clear_caches() + + +def test_log_severity_missing_raises_error(): + """Test that a dictionary without Fw.LogSeverity raises an error.""" + _clear_caches() + + with open(DEFAULT_DICTIONARY, "r") as f: + base_dict = json.load(f) + base_dict["typeDefinitions"] = [ + t + for t in base_dict.get("typeDefinitions", []) + if t.get("qualifiedName") != "Fw.LogSeverity" + ] + temp_file = tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) + json.dump(base_dict, temp_file) + temp_file.close() + + try: + from fpy.error import DictionaryError + + with pytest.raises(DictionaryError, match="Fw.LogSeverity"): + get_base_compile_state(temp_file.name, {}) + finally: + Path(temp_file.name).unlink() + _clear_caches() + + def test_timebase_additional_constants_available(): """Test that additional TimeBase constants from dict are usable in code.""" _clear_caches() From 4cd9bb75a5071cccf866ff182c30ddb6b1ef581f Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Wed, 12 Aug 2026 10:59:58 -0400 Subject: [PATCH 02/10] Fix tlm test --- test/fpy/test_telemetry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fpy/test_telemetry.py b/test/fpy/test_telemetry.py index 2629f0a..87b06f3 100644 --- a/test/fpy/test_telemetry.py +++ b/test/fpy/test_telemetry.py @@ -37,7 +37,7 @@ def test_get_struct_member_of_tlm(self, fprime_test_api): seq, { "Ref.typeDemo.ChoicePairCh": FpyValue( - lookup_type(fprime_test_api, "Ref.ChoicePair"), + lookup_type("Ref.ChoicePair"), {"firstChoice": "ONE", "secondChoice": "ONE"}, ).serialize() }, From 38a5838603cd4387fc1ffd800df3fc5cb0eb3523 Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Thu, 13 Aug 2026 09:01:36 -0400 Subject: [PATCH 03/10] A couple optimizations --- src/fpy/bytecode/assembler.py | 19 ++++++++++--------- src/fpy/visitors.py | 13 ++++++++++++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/fpy/bytecode/assembler.py b/src/fpy/bytecode/assembler.py index 3bcb016..7a13631 100644 --- a/src/fpy/bytecode/assembler.py +++ b/src/fpy/bytecode/assembler.py @@ -17,16 +17,17 @@ fpybc_grammar_str = (Path(__file__).parent / "grammar.lark").read_text(encoding="utf-8") -def parse(text: str): - parser = Lark( - fpybc_grammar_str, - start="input", - parser="lalr", - propagate_positions=True, - maybe_placeholders=True, - ) +_fpybc_parser = Lark( + fpybc_grammar_str, + start="input", + parser="lalr", + propagate_positions=True, + maybe_placeholders=True, +) - tree = parser.parse(text) + +def parse(text: str): + tree = _fpybc_parser.parse(text) transformed = FpyBcTransformer().transform(tree) return transformed diff --git a/src/fpy/visitors.py b/src/fpy/visitors.py index 9dc525c..16ec5d3 100644 --- a/src/fpy/visitors.py +++ b/src/fpy/visitors.py @@ -4,7 +4,7 @@ import typing from typing import Callable, Iterable, get_args, get_origin -from dataclasses import fields +from dataclasses import fields as _dataclass_fields from fpy.syntax import Ast from fpy.state import CompileState @@ -16,6 +16,17 @@ # Cache for visitor method mappings, keyed by visitor class _visitor_cache: dict[type, dict[type, str]] = {} +_fields_cache: dict[type, tuple] = {} + + +def fields(node): + """dataclasses.fields(), memoized per node type.""" + node_type = type(node) + cached = _fields_cache.get(node_type) + if cached is None: + cached = _fields_cache[node_type] = _dataclass_fields(node) + return cached + class _StopDescent: """Sentinel returned by a visit_* method to prevent the framework from From 1f4e2d52b0a4d4e87c385a8efdad7ea13e6f9d9f Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Thu, 13 Aug 2026 10:05:02 -0400 Subject: [PATCH 04/10] Add many more golden test cases --- test/conftest.py | 12 ++ test/fpy/golden/arith_ops.fpy | 15 ++ test/fpy/golden/arith_ops.fpybc | 91 +++++++++ test/fpy/golden/arith_ops.json | 35 ++++ test/fpy/golden/arith_ops.wat | 290 +++++++++++++++++++++++++++ test/fpy/golden/array_index.fpy | 6 + test/fpy/golden/array_index.fpybc | 107 ++++++++++ test/fpy/golden/array_index.json | 35 ++++ test/fpy/golden/array_index.wat | 133 ++++++++++++ test/fpy/golden/assert_fail.fpy | 2 + test/fpy/golden/assert_fail.fpybc | 6 + test/fpy/golden/assert_fail.json | 47 +++++ test/fpy/golden/assert_fail.wat | 27 +++ test/fpy/golden/bool_logic.fpy | 5 + test/fpy/golden/bool_logic.fpybc | 34 ++++ test/fpy/golden/bool_logic.json | 35 ++++ test/fpy/golden/bool_logic.wat | 89 ++++++++ test/fpy/golden/break_continue.fpy | 11 + test/fpy/golden/break_continue.fpybc | 38 ++++ test/fpy/golden/break_continue.json | 35 ++++ test/fpy/golden/break_continue.wat | 96 +++++++++ test/fpy/golden/casts.fpy | 7 + test/fpy/golden/casts.fpybc | 35 ++++ test/fpy/golden/casts.json | 35 ++++ test/fpy/golden/casts.wat | 75 +++++++ test/fpy/golden/check_simple.json | 24 +++ test/fpy/golden/check_simple.wat | 1 + test/fpy/golden/cmd_args.fpy | 7 + test/fpy/golden/cmd_args.fpybc | 30 +++ test/fpy/golden/cmd_args.json | 38 ++++ test/fpy/golden/cmd_args.wat | 110 ++++++++++ test/fpy/golden/cmd_handled.json | 37 ++++ test/fpy/golden/cmd_handled.wat | 56 ++++++ test/fpy/golden/cmd_unhandled.json | 37 ++++ test/fpy/golden/cmd_unhandled.wat | 47 +++++ test/fpy/golden/const_fold.json | 35 ++++ test/fpy/golden/const_fold.wat | 34 ++++ test/fpy/golden/empty.json | 35 ++++ test/fpy/golden/empty.wat | 24 +++ test/fpy/golden/enum_cmp.fpy | 4 + test/fpy/golden/enum_cmp.fpybc | 19 ++ test/fpy/golden/enum_cmp.json | 35 ++++ test/fpy/golden/enum_cmp.wat | 54 +++++ test/fpy/golden/exit_error.fpy | 2 + test/fpy/golden/exit_error.fpybc | 3 + test/fpy/golden/exit_error.json | 47 +++++ test/fpy/golden/exit_error.wat | 27 +++ test/fpy/golden/exit_success.json | 35 ++++ test/fpy/golden/exit_success.wat | 27 +++ test/fpy/golden/for_range.fpy | 5 + test/fpy/golden/for_range.fpybc | 28 +++ test/fpy/golden/for_range.json | 35 ++++ test/fpy/golden/for_range.wat | 88 ++++++++ test/fpy/golden/func_recursive.fpy | 7 + test/fpy/golden/func_recursive.fpybc | 30 +++ test/fpy/golden/func_recursive.json | 35 ++++ test/fpy/golden/func_recursive.wat | 84 ++++++++ test/fpy/golden/func_unused.json | 35 ++++ test/fpy/golden/func_unused.wat | 34 ++++ test/fpy/golden/func_used.json | 35 ++++ test/fpy/golden/func_used.wat | 51 +++++ test/fpy/golden/if_else.json | 35 ++++ test/fpy/golden/if_else.wat | 47 +++++ test/fpy/golden/if_simple.json | 35 ++++ test/fpy/golden/if_simple.wat | 43 ++++ test/fpy/golden/log_event.fpy | 3 + test/fpy/golden/log_event.fpybc | 9 + test/fpy/golden/log_event.json | 57 ++++++ test/fpy/golden/log_event.wat | 44 ++++ test/fpy/golden/seq_args.fpy | 3 + test/fpy/golden/seq_args.fpybc | 9 + test/fpy/golden/seq_args.json | 24 +++ test/fpy/golden/seq_args.wat | 1 + test/fpy/golden/struct_member.fpy | 5 + test/fpy/golden/struct_member.fpybc | 26 +++ test/fpy/golden/struct_member.json | 35 ++++ test/fpy/golden/struct_member.wat | 56 ++++++ test/fpy/golden/tlm_read.fpy | 4 + test/fpy/golden/tlm_read.fpybc | 11 + test/fpy/golden/tlm_read.json | 24 +++ test/fpy/golden/tlm_read.wat | 1 + test/fpy/golden/unary_ops.fpy | 6 + test/fpy/golden/unary_ops.fpybc | 29 +++ test/fpy/golden/unary_ops.json | 35 ++++ test/fpy/golden/unary_ops.wat | 73 +++++++ test/fpy/golden/var_bool.json | 35 ++++ test/fpy/golden/var_bool.wat | 42 ++++ test/fpy/golden/var_f32.json | 35 ++++ test/fpy/golden/var_f32.wat | 34 ++++ test/fpy/golden/var_reassign.json | 35 ++++ test/fpy/golden/var_reassign.wat | 34 ++++ test/fpy/golden/var_u32.json | 35 ++++ test/fpy/golden/var_u32.wat | 34 ++++ test/fpy/golden/while_simple.json | 35 ++++ test/fpy/golden/while_simple.wat | 52 +++++ test/fpy/golden/write_to_port.json | 49 +++++ test/fpy/golden/write_to_port.wat | 1 + test/fpy/test_golden.py | 234 ++++++++++++++++----- 98 files changed, 3753 insertions(+), 53 deletions(-) create mode 100644 test/fpy/golden/arith_ops.fpy create mode 100644 test/fpy/golden/arith_ops.fpybc create mode 100644 test/fpy/golden/arith_ops.json create mode 100644 test/fpy/golden/arith_ops.wat create mode 100644 test/fpy/golden/array_index.fpy create mode 100644 test/fpy/golden/array_index.fpybc create mode 100644 test/fpy/golden/array_index.json create mode 100644 test/fpy/golden/array_index.wat create mode 100644 test/fpy/golden/assert_fail.fpy create mode 100644 test/fpy/golden/assert_fail.fpybc create mode 100644 test/fpy/golden/assert_fail.json create mode 100644 test/fpy/golden/assert_fail.wat create mode 100644 test/fpy/golden/bool_logic.fpy create mode 100644 test/fpy/golden/bool_logic.fpybc create mode 100644 test/fpy/golden/bool_logic.json create mode 100644 test/fpy/golden/bool_logic.wat create mode 100644 test/fpy/golden/break_continue.fpy create mode 100644 test/fpy/golden/break_continue.fpybc create mode 100644 test/fpy/golden/break_continue.json create mode 100644 test/fpy/golden/break_continue.wat create mode 100644 test/fpy/golden/casts.fpy create mode 100644 test/fpy/golden/casts.fpybc create mode 100644 test/fpy/golden/casts.json create mode 100644 test/fpy/golden/casts.wat create mode 100644 test/fpy/golden/check_simple.json create mode 100644 test/fpy/golden/check_simple.wat create mode 100644 test/fpy/golden/cmd_args.fpy create mode 100644 test/fpy/golden/cmd_args.fpybc create mode 100644 test/fpy/golden/cmd_args.json create mode 100644 test/fpy/golden/cmd_args.wat create mode 100644 test/fpy/golden/cmd_handled.json create mode 100644 test/fpy/golden/cmd_handled.wat create mode 100644 test/fpy/golden/cmd_unhandled.json create mode 100644 test/fpy/golden/cmd_unhandled.wat create mode 100644 test/fpy/golden/const_fold.json create mode 100644 test/fpy/golden/const_fold.wat create mode 100644 test/fpy/golden/empty.json create mode 100644 test/fpy/golden/empty.wat create mode 100644 test/fpy/golden/enum_cmp.fpy create mode 100644 test/fpy/golden/enum_cmp.fpybc create mode 100644 test/fpy/golden/enum_cmp.json create mode 100644 test/fpy/golden/enum_cmp.wat create mode 100644 test/fpy/golden/exit_error.fpy create mode 100644 test/fpy/golden/exit_error.fpybc create mode 100644 test/fpy/golden/exit_error.json create mode 100644 test/fpy/golden/exit_error.wat create mode 100644 test/fpy/golden/exit_success.json create mode 100644 test/fpy/golden/exit_success.wat create mode 100644 test/fpy/golden/for_range.fpy create mode 100644 test/fpy/golden/for_range.fpybc create mode 100644 test/fpy/golden/for_range.json create mode 100644 test/fpy/golden/for_range.wat create mode 100644 test/fpy/golden/func_recursive.fpy create mode 100644 test/fpy/golden/func_recursive.fpybc create mode 100644 test/fpy/golden/func_recursive.json create mode 100644 test/fpy/golden/func_recursive.wat create mode 100644 test/fpy/golden/func_unused.json create mode 100644 test/fpy/golden/func_unused.wat create mode 100644 test/fpy/golden/func_used.json create mode 100644 test/fpy/golden/func_used.wat create mode 100644 test/fpy/golden/if_else.json create mode 100644 test/fpy/golden/if_else.wat create mode 100644 test/fpy/golden/if_simple.json create mode 100644 test/fpy/golden/if_simple.wat create mode 100644 test/fpy/golden/log_event.fpy create mode 100644 test/fpy/golden/log_event.fpybc create mode 100644 test/fpy/golden/log_event.json create mode 100644 test/fpy/golden/log_event.wat create mode 100644 test/fpy/golden/seq_args.fpy create mode 100644 test/fpy/golden/seq_args.fpybc create mode 100644 test/fpy/golden/seq_args.json create mode 100644 test/fpy/golden/seq_args.wat create mode 100644 test/fpy/golden/struct_member.fpy create mode 100644 test/fpy/golden/struct_member.fpybc create mode 100644 test/fpy/golden/struct_member.json create mode 100644 test/fpy/golden/struct_member.wat create mode 100644 test/fpy/golden/tlm_read.fpy create mode 100644 test/fpy/golden/tlm_read.fpybc create mode 100644 test/fpy/golden/tlm_read.json create mode 100644 test/fpy/golden/tlm_read.wat create mode 100644 test/fpy/golden/unary_ops.fpy create mode 100644 test/fpy/golden/unary_ops.fpybc create mode 100644 test/fpy/golden/unary_ops.json create mode 100644 test/fpy/golden/unary_ops.wat create mode 100644 test/fpy/golden/var_bool.json create mode 100644 test/fpy/golden/var_bool.wat create mode 100644 test/fpy/golden/var_f32.json create mode 100644 test/fpy/golden/var_f32.wat create mode 100644 test/fpy/golden/var_reassign.json create mode 100644 test/fpy/golden/var_reassign.wat create mode 100644 test/fpy/golden/var_u32.json create mode 100644 test/fpy/golden/var_u32.wat create mode 100644 test/fpy/golden/while_simple.json create mode 100644 test/fpy/golden/while_simple.wat create mode 100644 test/fpy/golden/write_to_port.json create mode 100644 test/fpy/golden/write_to_port.wat diff --git a/test/conftest.py b/test/conftest.py index ebe6a3e..97c0a58 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -17,6 +17,13 @@ def pytest_addoption(parser): help="Compile and run sequences through the LLVM/wasm backend " "(NASA spacewasm) instead of the fpy bytecode VM", ) + parser.addoption( + "--update-goldens", + action="store_true", + default=False, + help="Rewrite the golden files under test/fpy/golden with the " + "current outputs instead of comparing against them", + ) _wasm_harness_built = False @@ -59,6 +66,11 @@ def pytest_unconfigure(config): fpy.harness.close_all() +@pytest.fixture +def update_goldens(request): + return request.config.getoption("--update-goldens") + + @pytest.fixture(autouse=True) def _ensure_wasm_harness(request): # wasm-marked tests always run on the wasm backend, regardless of --wasm, diff --git a/test/fpy/golden/arith_ops.fpy b/test/fpy/golden/arith_ops.fpy new file mode 100644 index 0000000..2977d34 --- /dev/null +++ b/test/fpy/golden/arith_ops.fpy @@ -0,0 +1,15 @@ +# Every binary arithmetic operator on runtime (non-folded) operands, +# including floored division and modulo on negative values +x: I64 = 17 +y: I64 = 5 +assert x + y == 22 +assert x - y == 12 +assert x * y == 85 +assert x // y == 3 +assert x % y == 2 +neg: I64 = -17 +assert neg // y == -4 +assert neg % y == 3 +f: F64 = 7.0 +assert f / 2.0 == 3.5 +assert f ** 2.0 == 49.0 diff --git a/test/fpy/golden/arith_ops.fpybc b/test/fpy/golden/arith_ops.fpybc new file mode 100644 index 0000000..88429dc --- /dev/null +++ b/test/fpy/golden/arith_ops.fpybc @@ -0,0 +1,91 @@ +push_val 255 +allocate 32 +push_val 0 0 0 0 0 0 0 17 +store_rel_const_offset 1 8 +push_val 0 0 0 0 0 0 0 5 +store_rel_const_offset 9 8 +load_rel 1 8 +load_rel 9 8 +add +push_val 0 0 0 0 0 0 0 22 +ieq +not +if 15 +push_val 0 0 0 7 +exit +load_rel 1 8 +load_rel 9 8 +sub +push_val 0 0 0 0 0 0 0 12 +ieq +not +if 24 +push_val 0 0 0 7 +exit +load_rel 1 8 +load_rel 9 8 +mul +push_val 0 0 0 0 0 0 0 85 +ieq +not +if 33 +push_val 0 0 0 7 +exit +load_rel 1 8 +load_rel 9 8 +sdiv +push_val 0 0 0 0 0 0 0 3 +ieq +not +if 42 +push_val 0 0 0 7 +exit +load_rel 1 8 +load_rel 9 8 +smod +push_val 0 0 0 0 0 0 0 2 +ieq +not +if 51 +push_val 0 0 0 7 +exit +push_val 255 255 255 255 255 255 255 239 +store_rel_const_offset 17 8 +load_rel 17 8 +load_rel 9 8 +sdiv +push_val 255 255 255 255 255 255 255 252 +ieq +not +if 62 +push_val 0 0 0 7 +exit +load_rel 17 8 +load_rel 9 8 +smod +push_val 0 0 0 0 0 0 0 3 +ieq +not +if 71 +push_val 0 0 0 7 +exit +push_val 64 28 0 0 0 0 0 0 +store_rel_const_offset 25 8 +load_rel 25 8 +push_val 64 0 0 0 0 0 0 0 +fdiv +push_val 64 12 0 0 0 0 0 0 +feq +not +if 82 +push_val 0 0 0 7 +exit +load_rel 25 8 +push_val 64 0 0 0 0 0 0 0 +fpow +push_val 64 72 128 0 0 0 0 0 +feq +not +if 91 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/arith_ops.json b/test/fpy/golden/arith_ops.json new file mode 100644 index 0000000..071c876 --- /dev/null +++ b/test/fpy/golden/arith_ops.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00000000000000110000000000000005ffffffffffffffef401c000000000000", + "state": 2, + "statementsDispatched": 73 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/arith_ops.wat b/test/fpy/golden/arith_ops.wat new file mode 100644 index 0000000..bca5aa6 --- /dev/null +++ b/test/fpy/golden/arith_ops.wat @@ -0,0 +1,290 @@ + .file "" + .functype pow (f64, f64) -> (f64) + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + .local i64, i64, i64 + i32.const 0 + i64.const 5 + i64.store y + i32.const 0 + i64.const 17 + i64.store x + block + block + block + block + block + block + block + block + block + block + block + block + block + i32.const 1 + i32.eqz + br_if 0 + i32.const 0 + i64.load x + i32.const 0 + i64.load y + i64.sub + i64.const 12 + i64.ne + br_if 1 + i32.const 0 + i64.load x + i32.const 0 + i64.load y + i64.mul + i64.const 85 + i64.ne + br_if 2 + i32.const 0 + i64.load y + local.tee 0 + i64.const 0 + i64.eq + br_if 3 + i32.const 0 + i64.load x + local.tee 1 + local.get 0 + i64.div_s + local.tee 2 + i64.const -1 + i64.add + local.get 2 + local.get 1 + local.get 0 + i64.xor + i64.const 0 + i64.lt_s + i64.select + local.get 2 + local.get 1 + local.get 0 + i64.rem_s + i64.const 0 + i64.ne + i64.select + i64.const 3 + i64.ne + br_if 4 + i32.const 0 + i64.load y + local.tee 1 + i64.const 0 + i64.eq + br_if 5 + i32.const 0 + i64.load x + local.get 1 + i64.rem_s + local.tee 0 + local.get 1 + i64.add + local.get 0 + local.get 0 + local.get 1 + i64.xor + i64.const 0 + i64.lt_s + i64.select + local.get 0 + local.get 0 + i64.const 0 + i64.ne + i64.select + i64.const 2 + i64.ne + br_if 6 + i32.const 0 + i64.const -17 + i64.store neg + i32.const 0 + i64.load y + local.tee 0 + i64.const 0 + i64.eq + br_if 7 + i64.const -17 + local.get 0 + i64.div_s + local.tee 1 + i64.const -1 + i64.add + local.get 1 + i64.const -17 + local.get 0 + i64.xor + i64.const 0 + i64.lt_s + i64.select + local.get 1 + i64.const -17 + local.get 0 + i64.rem_s + i64.const 0 + i64.ne + i64.select + i64.const -4 + i64.ne + br_if 8 + i32.const 0 + i64.load y + local.tee 1 + i64.const 0 + i64.eq + br_if 9 + i32.const 0 + i64.load neg + local.get 1 + i64.rem_s + local.tee 0 + local.get 1 + i64.add + local.get 0 + local.get 0 + local.get 1 + i64.xor + i64.const 0 + i64.lt_s + i64.select + local.get 0 + local.get 0 + i64.const 0 + i64.ne + i64.select + i64.const 3 + i64.ne + br_if 10 + i32.const 0 + i64.const 4619567317775286272 + i64.store f + i32.const 1 + i32.eqz + br_if 11 + i32.const 0 + f64.load f + f64.const 0x1p1 + call pow + f64.const 0x1.88p5 + f64.ne + br_if 12 + return +.LBB0_14: + end_block + i32.const 7 + call exit + unreachable +.LBB0_15: + end_block + i32.const 7 + call exit + unreachable +.LBB0_16: + end_block + i32.const 7 + call exit + unreachable +.LBB0_17: + end_block + i32.const 10 + call panic + unreachable +.LBB0_18: + end_block + i32.const 7 + call exit + unreachable +.LBB0_19: + end_block + i32.const 10 + call panic + unreachable +.LBB0_20: + end_block + i32.const 7 + call exit + unreachable +.LBB0_21: + end_block + i32.const 10 + call panic + unreachable +.LBB0_22: + end_block + i32.const 7 + call exit + unreachable +.LBB0_23: + end_block + i32.const 10 + call panic + unreachable +.LBB0_24: + end_block + i32.const 7 + call exit + unreachable +.LBB0_25: + end_block + i32.const 7 + call exit + unreachable +.LBB0_26: + end_block + i32.const 7 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 3, 0x0 +x: + .int64 0 + .size x, 8 + + .type y,@object + .section .bss.y,"",@ + .p2align 3, 0x0 +y: + .int64 0 + .size y, 8 + + .type neg,@object + .section .bss.neg,"",@ + .p2align 3, 0x0 +neg: + .int64 0 + .size neg, 8 + + .type f,@object + .section .bss.f,"",@ + .p2align 3, 0x0 +f: + .int64 0x0000000000000000 + .size f, 8 + diff --git a/test/fpy/golden/array_index.fpy b/test/fpy/golden/array_index.fpy new file mode 100644 index 0000000..2d2dd03 --- /dev/null +++ b/test/fpy/golden/array_index.fpy @@ -0,0 +1,6 @@ +# Array element read and write, with a constant and a runtime index +a: Svc.ComQueueDepth = Svc.ComQueueDepth(7, 8) +i: I8 = 1 +a[i] = U32(a[i] + 1) +assert a[0] == 7 +assert a[1] == 9 diff --git a/test/fpy/golden/array_index.fpybc b/test/fpy/golden/array_index.fpybc new file mode 100644 index 0000000..36693bc --- /dev/null +++ b/test/fpy/golden/array_index.fpybc @@ -0,0 +1,107 @@ +push_val 255 +allocate 9 +push_val 0 0 0 7 0 0 0 8 +store_rel_const_offset 1 8 +push_val 1 +store_rel_const_offset 9 1 +load_rel 1 8 +load_rel 9 1 +siext_8_64 +push_val 0 0 0 8 +push_val 0 0 0 0 +peek +push_val 0 0 0 0 0 0 0 2 +sge +push_val 0 0 0 8 +push_val 0 0 0 1 +peek +push_val 0 0 0 0 0 0 0 0 +slt +or +if 23 +push_val 0 0 0 11 +exit +push_val 0 0 0 0 0 0 0 4 +mul +itrunc_64_32 +get_field 8 4 +ziext_32_64 +push_val 0 0 0 0 0 0 0 1 +add +itrunc_64_32 +load_rel 9 1 +siext_8_64 +push_val 0 0 0 8 +push_val 0 0 0 0 +peek +push_val 0 0 0 0 0 0 0 2 +sge +push_val 0 0 0 8 +push_val 0 0 0 1 +peek +push_val 0 0 0 0 0 0 0 0 +slt +or +if 47 +push_val 0 0 0 11 +exit +push_val 0 0 0 0 0 0 0 4 +mul +push_val 0 0 0 0 0 0 0 1 +add +itrunc_64_32 +store_rel 4 +load_rel 1 8 +push_val 0 0 0 0 0 0 0 0 +push_val 0 0 0 8 +push_val 0 0 0 0 +peek +push_val 0 0 0 0 0 0 0 2 +sge +push_val 0 0 0 8 +push_val 0 0 0 1 +peek +push_val 0 0 0 0 0 0 0 0 +slt +or +if 69 +push_val 0 0 0 11 +exit +push_val 0 0 0 0 0 0 0 4 +mul +itrunc_64_32 +get_field 8 4 +ziext_32_64 +push_val 0 0 0 0 0 0 0 7 +ieq +not +if 80 +push_val 0 0 0 7 +exit +load_rel 1 8 +push_val 0 0 0 0 0 0 0 1 +push_val 0 0 0 8 +push_val 0 0 0 0 +peek +push_val 0 0 0 0 0 0 0 2 +sge +push_val 0 0 0 8 +push_val 0 0 0 1 +peek +push_val 0 0 0 0 0 0 0 0 +slt +or +if 96 +push_val 0 0 0 11 +exit +push_val 0 0 0 0 0 0 0 4 +mul +itrunc_64_32 +get_field 8 4 +ziext_32_64 +push_val 0 0 0 0 0 0 0 9 +ieq +not +if 107 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/array_index.json b/test/fpy/golden/array_index.json new file mode 100644 index 0000000..49e5860 --- /dev/null +++ b/test/fpy/golden/array_index.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff000000070000000901", + "state": 2, + "statementsDispatched": 95 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/array_index.wat b/test/fpy/golden/array_index.wat new file mode 100644 index 0000000..820769b --- /dev/null +++ b/test/fpy/golden/array_index.wat @@ -0,0 +1,133 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + .local i64 + i32.const 0 + i64.const 34359738375 + i64.store a:p2align=2 + i32.const 0 + i32.const 1 + i32.store8 i + block + block + block + block + block + block + i32.const 0 + br_if 0 + i64.const 1 + i64.const 2 + i64.ge_s + br_if 0 + i32.const 0 + i64.load8_s i + local.tee 0 + i64.const 0 + i64.lt_s + br_if 1 + local.get 0 + i64.const 2 + i64.ge_s + br_if 1 + local.get 0 + i32.wrap_i64 + i32.const 2 + i32.shl + i32.const a + i32.add + i64.const 1 + i32.wrap_i64 + i32.const 2 + i32.shl + i32.const a + i32.add + i32.load 0 + i32.const 1 + i32.add + i32.store 0 + i32.const 0 + br_if 2 + i32.const 1 + i32.eqz + br_if 2 + i32.const 0 + i32.load a + i32.const 7 + i32.ne + br_if 3 + i32.const 0 + br_if 4 + i32.const 1 + i32.eqz + br_if 4 + i32.const 0 + i32.load a+4 + i32.const 9 + i32.ne + br_if 5 + return +.LBB0_11: + end_block + i32.const 11 + call panic + unreachable +.LBB0_12: + end_block + i32.const 11 + call panic + unreachable +.LBB0_13: + end_block + i32.const 11 + call panic + unreachable +.LBB0_14: + end_block + i32.const 7 + call exit + unreachable +.LBB0_15: + end_block + i32.const 11 + call panic + unreachable +.LBB0_16: + end_block + i32.const 7 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type a,@object + .section .bss.a,"",@ + .p2align 2, 0x0 +a: + .skip 8 + .size a, 8 + + .type i,@object + .section .bss.i,"",@ +i: + .int8 0 + .size i, 1 + diff --git a/test/fpy/golden/assert_fail.fpy b/test/fpy/golden/assert_fail.fpy new file mode 100644 index 0000000..53c549d --- /dev/null +++ b/test/fpy/golden/assert_fail.fpy @@ -0,0 +1,2 @@ +# A failing assert with an explicit exit code +assert 1 == 2, 55 diff --git a/test/fpy/golden/assert_fail.fpybc b/test/fpy/golden/assert_fail.fpybc new file mode 100644 index 0000000..b011e76 --- /dev/null +++ b/test/fpy/golden/assert_fail.fpybc @@ -0,0 +1,6 @@ +push_val 255 +push_val 0 +not +if 6 +push_val 0 0 0 55 +exit diff --git a/test/fpy/golden/assert_fail.json b/test/fpy/golden/assert_fail.json new file mode 100644 index 0000000..27a020e --- /dev/null +++ b/test/fpy/golden/assert_fail.json @@ -0,0 +1,47 @@ +{ + "common": { + "cmdResponse": 4, + "cmds": [], + "exitCode": 55, + "frameStart": 0, + "reachedRunning": true, + "sequencesSucceeded": 0, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 17, + "severity": 2, + "text": "(FpySequencer) SequenceExitedWithError : Sequence s0.bin exited with error code 55" + } + ], + "lastDirectiveError": 7, + "stack": "ff", + "state": 2, + "statementsDispatched": 6 + }, + "wasm": { + "events": [ + { + "id": 14, + "severity": 2, + "text": "(WasmSequencer) SequenceExitedWithError : Wasm program exited with error code 55" + }, + { + "id": 15, + "severity": 2, + "text": "(WasmSequencer) SequenceTrap : Wasm program trapped: HOST (2)" + }, + { + "id": 0, + "severity": 7, + "text": "(WasmSequencer) StoreAllocationSucceeded : Successfully allocated store with 8 modules" + } + ], + "lastDirectiveError": 0, + "stack": "", + "state": 1, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/assert_fail.wat b/test/fpy/golden/assert_fail.wat new file mode 100644 index 0000000..e1c51ea --- /dev/null +++ b/test/fpy/golden/assert_fail.wat @@ -0,0 +1,27 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 55 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + diff --git a/test/fpy/golden/bool_logic.fpy b/test/fpy/golden/bool_logic.fpy new file mode 100644 index 0000000..4261a5d --- /dev/null +++ b/test/fpy/golden/bool_logic.fpy @@ -0,0 +1,5 @@ +# Short-circuit and/or over runtime operands +x: U64 = 5 +ok: bool = (x == 5) and (x > 10) +assert ok == False +assert (x == 1) or (x == 5) diff --git a/test/fpy/golden/bool_logic.fpybc b/test/fpy/golden/bool_logic.fpybc new file mode 100644 index 0000000..6eb895d --- /dev/null +++ b/test/fpy/golden/bool_logic.fpybc @@ -0,0 +1,34 @@ +push_val 255 +allocate 9 +push_val 0 0 0 0 0 0 0 5 +store_rel_const_offset 1 8 +load_rel 1 8 +push_val 0 0 0 0 0 0 0 5 +ieq +if 12 +load_rel 1 8 +push_val 0 0 0 0 0 0 0 10 +ugt +goto 13 +push_val 0 +store_rel_const_offset 9 1 +load_rel 9 1 +push_val 0 +memcmp 1 +not +if 21 +push_val 0 0 0 7 +exit +load_rel 1 8 +push_val 0 0 0 0 0 0 0 1 +ieq +if 27 +push_val 255 +goto 30 +load_rel 1 8 +push_val 0 0 0 0 0 0 0 5 +ieq +not +if 34 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/bool_logic.json b/test/fpy/golden/bool_logic.json new file mode 100644 index 0000000..6a260d7 --- /dev/null +++ b/test/fpy/golden/bool_logic.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff000000000000000500", + "state": 2, + "statementsDispatched": 27 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/bool_logic.wat b/test/fpy/golden/bool_logic.wat new file mode 100644 index 0000000..caf14a7 --- /dev/null +++ b/test/fpy/golden/bool_logic.wat @@ -0,0 +1,89 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + .local i32 + i32.const 0 + i64.const 5 + i64.store x + i32.const 0 + local.set 0 + block + i32.const 0 + br_if 0 + i32.const 0 + i64.load x + i64.const 10 + i64.gt_u + local.set 0 +.LBB0_2: + end_block + i32.const 0 + local.get 0 + i32.store8 ok + block + block + local.get 0 + br_if 0 + i32.const 1 + local.set 0 + block + i32.const 0 + i64.load x + i64.const 1 + i64.eq + br_if 0 + i32.const 0 + i64.load x + i64.const 5 + i64.eq + local.set 0 +.LBB0_5: + end_block + local.get 0 + i32.eqz + br_if 1 + return +.LBB0_7: + end_block + i32.const 7 + call exit + unreachable +.LBB0_8: + end_block + i32.const 7 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 3, 0x0 +x: + .int64 0 + .size x, 8 + + .type ok,@object + .section .bss.ok,"",@ +ok: + .int8 0 + .size ok, 1 + diff --git a/test/fpy/golden/break_continue.fpy b/test/fpy/golden/break_continue.fpy new file mode 100644 index 0000000..f02b93c --- /dev/null +++ b/test/fpy/golden/break_continue.fpy @@ -0,0 +1,11 @@ +# break and continue inside a while loop +n: U64 = 0 +evens: U64 = 0 +while True: + n += 1 + if n >= 10: + break + if n % 2 == 1: + continue + evens += 1 +assert evens == 4 diff --git a/test/fpy/golden/break_continue.fpybc b/test/fpy/golden/break_continue.fpybc new file mode 100644 index 0000000..42b7407 --- /dev/null +++ b/test/fpy/golden/break_continue.fpybc @@ -0,0 +1,38 @@ +push_val 255 +allocate 16 +push_val 0 0 0 0 0 0 0 0 +store_rel_const_offset 1 8 +push_val 0 0 0 0 0 0 0 0 +store_rel_const_offset 9 8 +push_val 255 +if 31 +load_rel 1 8 +push_val 0 0 0 0 0 0 0 1 +add +store_rel_const_offset 1 8 +load_rel 1 8 +push_val 0 0 0 0 0 0 0 10 +uge +if 18 +goto 31 +goto 18 +load_rel 1 8 +push_val 0 0 0 0 0 0 0 2 +umod +push_val 0 0 0 0 0 0 0 1 +ieq +if 26 +goto 6 +goto 26 +load_rel 9 8 +push_val 0 0 0 0 0 0 0 1 +add +store_rel_const_offset 9 8 +goto 6 +load_rel 9 8 +push_val 0 0 0 0 0 0 0 4 +ieq +not +if 38 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/break_continue.json b/test/fpy/golden/break_continue.json new file mode 100644 index 0000000..df00f9c --- /dev/null +++ b/test/fpy/golden/break_continue.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff000000000000000a0000000000000004", + "state": 2, + "statementsDispatched": 191 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/break_continue.wat b/test/fpy/golden/break_continue.wat new file mode 100644 index 0000000..7a538e9 --- /dev/null +++ b/test/fpy/golden/break_continue.wat @@ -0,0 +1,96 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + .local i64 + i32.const 0 + i64.const 0 + i64.store evens + i32.const 0 + i64.const 0 + i64.store n +.LBB0_1: + block + block + loop + i32.const 0 + i32.const 0 + i64.load n + i64.const 1 + i64.add + local.tee 0 + i64.store n + block + local.get 0 + i64.const 10 + i64.lt_u + br_if 0 + i32.const 0 + i64.load evens + i64.const 4 + i64.eq + br_if 2 + i32.const 7 + call exit + unreachable +.LBB0_4: + end_block + i32.const 1 + i32.eqz + br_if 2 + i32.const 0 + i64.load n + i32.wrap_i64 + i32.const 1 + i32.and + br_if 0 + i32.const 0 + i32.const 0 + i64.load evens + i64.const 1 + i64.add + i64.store evens + br 0 +.LBB0_7: + end_loop + end_block + return +.LBB0_8: + end_block + i32.const 10 + call panic + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type n,@object + .section .bss.n,"",@ + .p2align 3, 0x0 +n: + .int64 0 + .size n, 8 + + .type evens,@object + .section .bss.evens,"",@ + .p2align 3, 0x0 +evens: + .int64 0 + .size evens, 8 + diff --git a/test/fpy/golden/casts.fpy b/test/fpy/golden/casts.fpy new file mode 100644 index 0000000..ff5509d --- /dev/null +++ b/test/fpy/golden/casts.fpy @@ -0,0 +1,7 @@ +# Explicit numeric casts on runtime values: float->int truncation, +# int narrowing, int->float +f: F64 = 5.9 +assert I32(f) == 5 +n: I32 = 300 +assert U8(n) == 44 +assert F64(n) == 300.0 diff --git a/test/fpy/golden/casts.fpybc b/test/fpy/golden/casts.fpybc new file mode 100644 index 0000000..d9387c9 --- /dev/null +++ b/test/fpy/golden/casts.fpybc @@ -0,0 +1,35 @@ +push_val 255 +allocate 12 +push_val 64 23 153 153 153 153 153 154 +store_rel_const_offset 1 8 +load_rel 1 8 +fptosi +itrunc_64_32 +siext_32_64 +push_val 0 0 0 0 0 0 0 5 +ieq +not +if 14 +push_val 0 0 0 7 +exit +push_val 0 0 1 44 +store_rel_const_offset 9 4 +load_rel 9 4 +siext_32_64 +itrunc_64_8 +ziext_8_64 +push_val 0 0 0 0 0 0 0 44 +ieq +not +if 26 +push_val 0 0 0 7 +exit +load_rel 9 4 +siext_32_64 +sitofp +push_val 64 114 192 0 0 0 0 0 +feq +not +if 35 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/casts.json b/test/fpy/golden/casts.json new file mode 100644 index 0000000..8a88720 --- /dev/null +++ b/test/fpy/golden/casts.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff401799999999999a0000012c", + "state": 2, + "statementsDispatched": 29 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/casts.wat b/test/fpy/golden/casts.wat new file mode 100644 index 0000000..f96ad7a --- /dev/null +++ b/test/fpy/golden/casts.wat @@ -0,0 +1,75 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i64.const 4618328827877759386 + i64.store f + block + block + block + i32.const 1 + i32.eqz + br_if 0 + i32.const 0 + i32.const 300 + i32.store n + i32.const 1 + i32.eqz + br_if 1 + i32.const 0 + i32.load n + f64.convert_i32_s + f64.const 0x1.2cp8 + f64.ne + br_if 2 + return +.LBB0_4: + end_block + i32.const 7 + call exit + unreachable +.LBB0_5: + end_block + i32.const 7 + call exit + unreachable +.LBB0_6: + end_block + i32.const 7 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type f,@object + .section .bss.f,"",@ + .p2align 3, 0x0 +f: + .int64 0x0000000000000000 + .size f, 8 + + .type n,@object + .section .bss.n,"",@ + .p2align 2, 0x0 +n: + .int32 0 + .size n, 4 + diff --git a/test/fpy/golden/check_simple.json b/test/fpy/golden/check_simple.json new file mode 100644 index 0000000..0a4d2be --- /dev/null +++ b/test/fpy/golden/check_simple.json @@ -0,0 +1,24 @@ +{ + "fpybc": { + "cmdResponse": 0, + "cmds": [], + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [], + "stack": "ff0000000000000000000000000000010000000000000000000186a0ffff000000000000000000000000000000000000000000000000000000000000000000ffffffff00000000", + "state": 2, + "statementsDispatched": 263 + }, + "wasm": { + "compileError": "BackendError: : LLVM backend can't lower runtime type-constructor calls yet" + } +} diff --git a/test/fpy/golden/check_simple.wat b/test/fpy/golden/check_simple.wat new file mode 100644 index 0000000..f904145 --- /dev/null +++ b/test/fpy/golden/check_simple.wat @@ -0,0 +1 @@ +compile error: BackendError: : LLVM backend can't lower runtime type-constructor calls yet diff --git a/test/fpy/golden/cmd_args.fpy b/test/fpy/golden/cmd_args.fpy new file mode 100644 index 0000000..a02ef18 --- /dev/null +++ b/test/fpy/golden/cmd_args.fpy @@ -0,0 +1,7 @@ +# Commands with runtime scalar arguments and a constant string argument; +# the .json golden pins the exact serialized command buffers +v1: I32 = -2 +v2: F32 = 1.5 +v3: U8 = 8 +CdhCore.cmdDisp.CMD_TEST_CMD_1(v1, v2, v3) +CdhCore.cmdDisp.CMD_NO_OP_STRING("hello") diff --git a/test/fpy/golden/cmd_args.fpybc b/test/fpy/golden/cmd_args.fpybc new file mode 100644 index 0000000..33349b3 --- /dev/null +++ b/test/fpy/golden/cmd_args.fpybc @@ -0,0 +1,30 @@ +push_val 255 +allocate 9 +push_val 255 255 255 254 +store_rel_const_offset 1 4 +push_val 63 192 0 0 +store_rel_const_offset 5 4 +push_val 8 +store_rel_const_offset 9 1 +load_rel 1 4 +load_rel 5 4 +load_rel 9 1 +push_val 1 0 0 2 +stack_cmd 9 +push_val 0 +memcmp 1 +if 17 +goto 21 +load_abs 0 1 +if 21 +push_val 0 0 0 17 +exit +const_cmd 16777217 0 5 104 101 108 108 111 +push_val 0 +memcmp 1 +if 26 +goto 30 +load_abs 0 1 +if 30 +push_val 0 0 0 17 +exit diff --git a/test/fpy/golden/cmd_args.json b/test/fpy/golden/cmd_args.json new file mode 100644 index 0000000..77d6329 --- /dev/null +++ b/test/fpy/golden/cmd_args.json @@ -0,0 +1,38 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [ + "01000002fffffffe3fc0000008", + "01000001000568656c6c6f" + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "fffffffffe3fc0000008", + "state": 2, + "statementsDispatched": 22 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 2 + } +} diff --git a/test/fpy/golden/cmd_args.wat b/test/fpy/golden/cmd_args.wat new file mode 100644 index 0000000..ea94a72 --- /dev/null +++ b/test/fpy/golden/cmd_args.wat @@ -0,0 +1,110 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 1069547520 + i32.store v2 + i32.const 0 + i32.const -2 + i32.store v1 + i32.const 0 + i32.const 8 + i32.store8 v3 + i32.const 0 + i32.const 8 + i32.store8 .Lcmd_buf+12 + i32.const 0 + i64.const 211381093662719 + i64.store .Lcmd_buf+4:p2align=0 + block + block + block + i32.const .Lcmd_buf + i32.const 13 + call cmd + i32.const 255 + i32.and + i32.eqz + br_if 0 + i32.const 0 + i32.load8_u flags + br_if 1 +.LBB0_2: + end_block + block + i32.const .Lcmd_buf.1 + i32.const 11 + call cmd + i32.const 255 + i32.and + i32.eqz + br_if 0 + i32.const 0 + i32.load8_u flags + br_if 2 +.LBB0_4: + end_block + return +.LBB0_5: + end_block + i32.const 17 + call exit + unreachable +.LBB0_6: + end_block + i32.const 17 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type v1,@object + .section .bss.v1,"",@ + .p2align 2, 0x0 +v1: + .int32 0 + .size v1, 4 + + .type v2,@object + .section .bss.v2,"",@ + .p2align 2, 0x0 +v2: + .int32 0x00000000 + .size v2, 4 + + .type v3,@object + .section .bss.v3,"",@ +v3: + .int8 0 + .size v3, 1 + + .type .Lcmd_buf,@object + .section .data..Lcmd_buf,"",@ +.Lcmd_buf: + .asciz "\001\000\000\002\000\000\000\000\000\000\000\000" + .size .Lcmd_buf, 13 + + .type .Lcmd_buf.1,@object + .section .rodata..Lcmd_buf.1,"",@ +.Lcmd_buf.1: + .ascii "\001\000\000\001\000\005hello" + .size .Lcmd_buf.1, 11 + diff --git a/test/fpy/golden/cmd_handled.json b/test/fpy/golden/cmd_handled.json new file mode 100644 index 0000000..9ea7cd6 --- /dev/null +++ b/test/fpy/golden/cmd_handled.json @@ -0,0 +1,37 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [ + "01000000" + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00", + "state": 2, + "statementsDispatched": 10 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 1 + } +} diff --git a/test/fpy/golden/cmd_handled.wat b/test/fpy/golden/cmd_handled.wat new file mode 100644 index 0000000..5828c1f --- /dev/null +++ b/test/fpy/golden/cmd_handled.wat @@ -0,0 +1,56 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + .local i32 + i32.const 0 + i32.const .Lcmd_buf + i32.const 4 + call cmd + local.tee 0 + i32.store8 ret + block + local.get 0 + i32.const 255 + i32.and + br_if 0 + i32.const 0 + call exit + unreachable +.LBB0_2: + end_block + i32.const 1 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type ret,@object + .section .bss.ret,"",@ +ret: + .int8 0 + .size ret, 1 + + .type .Lcmd_buf,@object + .section .rodata..Lcmd_buf,"",@ +.Lcmd_buf: + .asciz "\001\000\000" + .size .Lcmd_buf, 4 + diff --git a/test/fpy/golden/cmd_unhandled.json b/test/fpy/golden/cmd_unhandled.json new file mode 100644 index 0000000..674cc28 --- /dev/null +++ b/test/fpy/golden/cmd_unhandled.json @@ -0,0 +1,37 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [ + "01000000" + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff", + "state": 2, + "statementsDispatched": 6 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 1 + } +} diff --git a/test/fpy/golden/cmd_unhandled.wat b/test/fpy/golden/cmd_unhandled.wat new file mode 100644 index 0000000..1ab8c03 --- /dev/null +++ b/test/fpy/golden/cmd_unhandled.wat @@ -0,0 +1,47 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + block + i32.const .Lcmd_buf + i32.const 4 + call cmd + i32.const 255 + i32.and + i32.eqz + br_if 0 + i32.const 0 + i32.load8_u flags + i32.eqz + br_if 0 + i32.const 17 + call exit + unreachable +.LBB0_3: + end_block + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type .Lcmd_buf,@object + .section .rodata..Lcmd_buf,"",@ +.Lcmd_buf: + .asciz "\001\000\000" + .size .Lcmd_buf, 4 + diff --git a/test/fpy/golden/const_fold.json b/test/fpy/golden/const_fold.json new file mode 100644 index 0000000..a77b132 --- /dev/null +++ b/test/fpy/golden/const_fold.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00000003", + "state": 2, + "statementsDispatched": 4 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/const_fold.wat b/test/fpy/golden/const_fold.wat new file mode 100644 index 0000000..6ae6aba --- /dev/null +++ b/test/fpy/golden/const_fold.wat @@ -0,0 +1,34 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 3 + i32.store x + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 2, 0x0 +x: + .int32 0 + .size x, 4 + diff --git a/test/fpy/golden/empty.json b/test/fpy/golden/empty.json new file mode 100644 index 0000000..5866272 --- /dev/null +++ b/test/fpy/golden/empty.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff", + "state": 2, + "statementsDispatched": 1 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/empty.wat b/test/fpy/golden/empty.wat new file mode 100644 index 0000000..eb0f1da --- /dev/null +++ b/test/fpy/golden/empty.wat @@ -0,0 +1,24 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + diff --git a/test/fpy/golden/enum_cmp.fpy b/test/fpy/golden/enum_cmp.fpy new file mode 100644 index 0000000..905c65a --- /dev/null +++ b/test/fpy/golden/enum_cmp.fpy @@ -0,0 +1,4 @@ +# Enum variables and comparisons +c: Ref.Choice = Ref.Choice.RED +assert c == Ref.Choice.RED +assert c != Ref.Choice.ONE diff --git a/test/fpy/golden/enum_cmp.fpybc b/test/fpy/golden/enum_cmp.fpybc new file mode 100644 index 0000000..f7cd0c9 --- /dev/null +++ b/test/fpy/golden/enum_cmp.fpybc @@ -0,0 +1,19 @@ +push_val 255 +allocate 4 +push_val 0 0 0 2 +store_rel_const_offset 1 4 +load_rel 1 4 +push_val 0 0 0 2 +memcmp 4 +not +if 11 +push_val 0 0 0 7 +exit +load_rel 1 4 +push_val 0 0 0 0 +memcmp 4 +not +not +if 19 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/enum_cmp.json b/test/fpy/golden/enum_cmp.json new file mode 100644 index 0000000..cbb0dd4 --- /dev/null +++ b/test/fpy/golden/enum_cmp.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00000002", + "state": 2, + "statementsDispatched": 15 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/enum_cmp.wat b/test/fpy/golden/enum_cmp.wat new file mode 100644 index 0000000..ec0fe3d --- /dev/null +++ b/test/fpy/golden/enum_cmp.wat @@ -0,0 +1,54 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 2 + i32.store c + block + block + i32.const 1 + i32.eqz + br_if 0 + i32.const 0 + i32.load c + i32.eqz + br_if 1 + return +.LBB0_3: + end_block + i32.const 7 + call exit + unreachable +.LBB0_4: + end_block + i32.const 7 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type c,@object + .section .bss.c,"",@ + .p2align 2, 0x0 +c: + .int32 0 + .size c, 4 + diff --git a/test/fpy/golden/exit_error.fpy b/test/fpy/golden/exit_error.fpy new file mode 100644 index 0000000..0761e27 --- /dev/null +++ b/test/fpy/golden/exit_error.fpy @@ -0,0 +1,2 @@ +# A nonzero exit; the .json golden pins how the failure is reported +exit(3) diff --git a/test/fpy/golden/exit_error.fpybc b/test/fpy/golden/exit_error.fpybc new file mode 100644 index 0000000..4548d3a --- /dev/null +++ b/test/fpy/golden/exit_error.fpybc @@ -0,0 +1,3 @@ +push_val 255 +push_val 0 0 0 3 +exit diff --git a/test/fpy/golden/exit_error.json b/test/fpy/golden/exit_error.json new file mode 100644 index 0000000..225611e --- /dev/null +++ b/test/fpy/golden/exit_error.json @@ -0,0 +1,47 @@ +{ + "common": { + "cmdResponse": 4, + "cmds": [], + "exitCode": 3, + "frameStart": 0, + "reachedRunning": true, + "sequencesSucceeded": 0, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 17, + "severity": 2, + "text": "(FpySequencer) SequenceExitedWithError : Sequence s0.bin exited with error code 3" + } + ], + "lastDirectiveError": 7, + "stack": "ff", + "state": 2, + "statementsDispatched": 3 + }, + "wasm": { + "events": [ + { + "id": 14, + "severity": 2, + "text": "(WasmSequencer) SequenceExitedWithError : Wasm program exited with error code 3" + }, + { + "id": 15, + "severity": 2, + "text": "(WasmSequencer) SequenceTrap : Wasm program trapped: HOST (2)" + }, + { + "id": 0, + "severity": 7, + "text": "(WasmSequencer) StoreAllocationSucceeded : Successfully allocated store with 8 modules" + } + ], + "lastDirectiveError": 0, + "stack": "", + "state": 1, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/exit_error.wat b/test/fpy/golden/exit_error.wat new file mode 100644 index 0000000..ff29e7e --- /dev/null +++ b/test/fpy/golden/exit_error.wat @@ -0,0 +1,27 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 3 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + diff --git a/test/fpy/golden/exit_success.json b/test/fpy/golden/exit_success.json new file mode 100644 index 0000000..70d5ed1 --- /dev/null +++ b/test/fpy/golden/exit_success.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff", + "state": 2, + "statementsDispatched": 3 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/exit_success.wat b/test/fpy/golden/exit_success.wat new file mode 100644 index 0000000..a8ddf63 --- /dev/null +++ b/test/fpy/golden/exit_success.wat @@ -0,0 +1,27 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + diff --git a/test/fpy/golden/for_range.fpy b/test/fpy/golden/for_range.fpy new file mode 100644 index 0000000..500cc85 --- /dev/null +++ b/test/fpy/golden/for_range.fpy @@ -0,0 +1,5 @@ +# for loops desugar to while loops over a hidden counter +total: I64 = 0 +for i in 0 .. 5: + total += i +assert total == 10 diff --git a/test/fpy/golden/for_range.fpybc b/test/fpy/golden/for_range.fpybc new file mode 100644 index 0000000..8f98e4e --- /dev/null +++ b/test/fpy/golden/for_range.fpybc @@ -0,0 +1,28 @@ +push_val 255 +allocate 24 +push_val 0 0 0 0 0 0 0 0 +store_rel_const_offset 1 8 +push_val 0 0 0 0 0 0 0 0 +store_rel_const_offset 9 8 +push_val 0 0 0 0 0 0 0 5 +store_rel_const_offset 17 8 +load_rel 9 8 +load_rel 17 8 +slt +if 21 +load_rel 1 8 +load_rel 9 8 +add +store_rel_const_offset 1 8 +load_rel 9 8 +push_val 0 0 0 0 0 0 0 1 +add +store_rel_const_offset 9 8 +goto 8 +load_rel 1 8 +push_val 0 0 0 0 0 0 0 10 +ieq +not +if 28 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/for_range.json b/test/fpy/golden/for_range.json new file mode 100644 index 0000000..ecbaf86 --- /dev/null +++ b/test/fpy/golden/for_range.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff000000000000000a00000000000000050000000000000005", + "state": 2, + "statementsDispatched": 82 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/for_range.wat b/test/fpy/golden/for_range.wat new file mode 100644 index 0000000..541d8b1 --- /dev/null +++ b/test/fpy/golden/for_range.wat @@ -0,0 +1,88 @@ + .file "" + .globaltype __stack_pointer, i32 + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + .local i32 + global.get __stack_pointer + i32.const 16 + i32.sub + local.tee 0 + global.set __stack_pointer + i32.const 0 + i64.const 0 + i64.store total + local.get 0 + i64.const 0 + i64.store 8 + local.get 0 + i64.const 5 + i64.store 0 +.LBB0_1: + block + loop + local.get 0 + i64.load 8 + local.get 0 + i64.load 0 + i64.ge_s + br_if 1 + i32.const 0 + i32.const 0 + i64.load total + local.get 0 + i64.load 8 + i64.add + i64.store total + local.get 0 + local.get 0 + i64.load 8 + i64.const 1 + i64.add + i64.store 8 + br 0 +.LBB0_3: + end_loop + end_block + block + i32.const 0 + i64.load total + i64.const 10 + i64.eq + br_if 0 + i32.const 7 + call exit + unreachable +.LBB0_5: + end_block + local.get 0 + i32.const 16 + i32.add + global.set __stack_pointer + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type total,@object + .section .bss.total,"",@ + .p2align 3, 0x0 +total: + .int64 0 + .size total, 8 + diff --git a/test/fpy/golden/func_recursive.fpy b/test/fpy/golden/func_recursive.fpy new file mode 100644 index 0000000..2383c49 --- /dev/null +++ b/test/fpy/golden/func_recursive.fpy @@ -0,0 +1,7 @@ +# A recursive function call +def fib(a: U64) -> U64: + if a < 2: + return 1 + return fib(a - 1) + fib(a - 2) + +assert fib(10) == 89 diff --git a/test/fpy/golden/func_recursive.fpybc b/test/fpy/golden/func_recursive.fpybc new file mode 100644 index 0000000..12154bf --- /dev/null +++ b/test/fpy/golden/func_recursive.fpybc @@ -0,0 +1,30 @@ +goto 20 +load_rel -16 8 +push_val 0 0 0 0 0 0 0 2 +ult +if 8 +push_val 0 0 0 0 0 0 0 1 +return 8 8 +goto 8 +load_rel -16 8 +push_val 0 0 0 0 0 0 0 1 +sub +push_val 0 0 0 1 +call +load_rel -16 8 +push_val 0 0 0 0 0 0 0 2 +sub +push_val 0 0 0 1 +call +add +return 8 8 +push_val 255 +push_val 0 0 0 0 0 0 0 10 +push_val 0 0 0 1 +call +push_val 0 0 0 0 0 0 0 89 +ieq +not +if 30 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/func_recursive.json b/test/fpy/golden/func_recursive.json new file mode 100644 index 0000000..70c342a --- /dev/null +++ b/test/fpy/golden/func_recursive.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff", + "state": 2, + "statementsDispatched": 1951 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/func_recursive.wat b/test/fpy/golden/func_recursive.wat new file mode 100644 index 0000000..2f4bc69 --- /dev/null +++ b/test/fpy/golden/func_recursive.wat @@ -0,0 +1,84 @@ + .file "" + .globaltype __stack_pointer, i32 + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .functype fib (i64) -> (i64) + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + block + i64.const 10 + call fib + i64.const 89 + i64.eq + br_if 0 + i32.const 7 + call exit + unreachable +.LBB0_2: + end_block + end_function + + .section .text.fib,"",@ + .type fib,@function +fib: + .functype fib (i64) -> (i64) + .local i32, i64 + global.get __stack_pointer + i32.const 16 + i32.sub + local.tee 1 + global.set __stack_pointer + local.get 1 + local.get 0 + i64.store 8 + block + local.get 0 + i64.const 1 + i64.gt_u + br_if 0 + local.get 1 + i32.const 16 + i32.add + global.set __stack_pointer + i64.const 1 + return +.LBB1_2: + end_block + local.get 1 + i64.load 8 + i64.const -1 + i64.add + call fib + local.set 0 + local.get 1 + i64.load 8 + i64.const -2 + i64.add + call fib + local.set 2 + local.get 1 + i32.const 16 + i32.add + global.set __stack_pointer + local.get 0 + local.get 2 + i64.add + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + diff --git a/test/fpy/golden/func_unused.json b/test/fpy/golden/func_unused.json new file mode 100644 index 0000000..3bf47cf --- /dev/null +++ b/test/fpy/golden/func_unused.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00000005", + "state": 2, + "statementsDispatched": 4 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/func_unused.wat b/test/fpy/golden/func_unused.wat new file mode 100644 index 0000000..bf907ad --- /dev/null +++ b/test/fpy/golden/func_unused.wat @@ -0,0 +1,34 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 5 + i32.store result + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type result,@object + .section .bss.result,"",@ + .p2align 2, 0x0 +result: + .int32 0 + .size result, 4 + diff --git a/test/fpy/golden/func_used.json b/test/fpy/golden/func_used.json new file mode 100644 index 0000000..f77cca6 --- /dev/null +++ b/test/fpy/golden/func_used.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00000006", + "state": 2, + "statementsDispatched": 13 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/func_used.wat b/test/fpy/golden/func_used.wat new file mode 100644 index 0000000..e9280af --- /dev/null +++ b/test/fpy/golden/func_used.wat @@ -0,0 +1,51 @@ + .file "" + .globaltype __stack_pointer, i32 + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .functype add_one (i32) -> (i32) + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 5 + call add_one + i32.store result + end_function + + .section .text.add_one,"",@ + .type add_one,@function +add_one: + .functype add_one (i32) -> (i32) + global.get __stack_pointer + i32.const 16 + i32.sub + local.get 0 + i32.store 12 + local.get 0 + i32.const 1 + i32.add + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type result,@object + .section .bss.result,"",@ + .p2align 2, 0x0 +result: + .int32 0 + .size result, 4 + diff --git a/test/fpy/golden/if_else.json b/test/fpy/golden/if_else.json new file mode 100644 index 0000000..256fc8b --- /dev/null +++ b/test/fpy/golden/if_else.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00000002", + "state": 2, + "statementsDispatched": 12 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/if_else.wat b/test/fpy/golden/if_else.wat new file mode 100644 index 0000000..354fb6f --- /dev/null +++ b/test/fpy/golden/if_else.wat @@ -0,0 +1,47 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 1 + i32.store x + block + i32.const 1 + i32.eqz + br_if 0 + i32.const 0 + i32.const 2 + i32.store x + return +.LBB0_2: + end_block + i32.const 0 + i32.const 3 + i32.store x + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 2, 0x0 +x: + .int32 0 + .size x, 4 + diff --git a/test/fpy/golden/if_simple.json b/test/fpy/golden/if_simple.json new file mode 100644 index 0000000..256fc8b --- /dev/null +++ b/test/fpy/golden/if_simple.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00000002", + "state": 2, + "statementsDispatched": 12 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/if_simple.wat b/test/fpy/golden/if_simple.wat new file mode 100644 index 0000000..513171c --- /dev/null +++ b/test/fpy/golden/if_simple.wat @@ -0,0 +1,43 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 1 + i32.store x + block + i32.const 1 + i32.eqz + br_if 0 + i32.const 0 + i32.const 2 + i32.store x +.LBB0_2: + end_block + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 2, 0x0 +x: + .int32 0 + .size x, 4 + diff --git a/test/fpy/golden/log_event.fpy b/test/fpy/golden/log_event.fpy new file mode 100644 index 0000000..41cdde4 --- /dev/null +++ b/test/fpy/golden/log_event.fpy @@ -0,0 +1,3 @@ +# log() with the default and an explicit severity +log("hello") +log("bad", Fw.LogSeverity.WARNING_HI) diff --git a/test/fpy/golden/log_event.fpybc b/test/fpy/golden/log_event.fpybc new file mode 100644 index 0000000..3dd55eb --- /dev/null +++ b/test/fpy/golden/log_event.fpybc @@ -0,0 +1,9 @@ +push_val 255 +push_val 5 +push_val 104 101 108 108 111 +push_val 0 0 0 5 +pop_event +push_val 2 +push_val 98 97 100 +push_val 0 0 0 3 +pop_event diff --git a/test/fpy/golden/log_event.json b/test/fpy/golden/log_event.json new file mode 100644 index 0000000..e9fd8e9 --- /dev/null +++ b/test/fpy/golden/log_event.json @@ -0,0 +1,57 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 42, + "severity": 5, + "text": "(FpySequencer) LogActivityHi : Sequence s0.bin: hello" + }, + { + "id": 39, + "severity": 2, + "text": "(FpySequencer) LogWarningHi : Sequence s0.bin: bad" + }, + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff", + "state": 2, + "statementsDispatched": 9 + }, + "wasm": { + "events": [ + { + "guest": true, + "id": 24, + "severity": 5, + "text": "hello" + }, + { + "guest": true, + "id": 21, + "severity": 2, + "text": "bad" + }, + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/log_event.wat b/test/fpy/golden/log_event.wat new file mode 100644 index 0000000..197fae8 --- /dev/null +++ b/test/fpy/golden/log_event.wat @@ -0,0 +1,44 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 5 + i32.const .Llog_msg + i32.const 5 + call event + i32.const 2 + i32.const .Llog_msg.1 + i32.const 3 + call event + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type .Llog_msg,@object + .section .rodata..Llog_msg,"",@ +.Llog_msg: + .ascii "hello" + .size .Llog_msg, 5 + + .type .Llog_msg.1,@object + .section .rodata..Llog_msg.1,"",@ +.Llog_msg.1: + .ascii "bad" + .size .Llog_msg.1, 3 + diff --git a/test/fpy/golden/seq_args.fpy b/test/fpy/golden/seq_args.fpy new file mode 100644 index 0000000..92a3b09 --- /dev/null +++ b/test/fpy/golden/seq_args.fpy @@ -0,0 +1,3 @@ +# Sequence arguments (the harness passes the values in RUN_INPUTS) +sequence(x: U32) +assert x == 7 diff --git a/test/fpy/golden/seq_args.fpybc b/test/fpy/golden/seq_args.fpybc new file mode 100644 index 0000000..3c9b44c --- /dev/null +++ b/test/fpy/golden/seq_args.fpybc @@ -0,0 +1,9 @@ +push_val 255 +load_rel 0 4 +ziext_32_64 +push_val 0 0 0 0 0 0 0 7 +ieq +not +if 9 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/seq_args.json b/test/fpy/golden/seq_args.json new file mode 100644 index 0000000..9831415 --- /dev/null +++ b/test/fpy/golden/seq_args.json @@ -0,0 +1,24 @@ +{ + "fpybc": { + "cmdResponse": 0, + "cmds": [], + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [], + "stack": "00000007ff", + "state": 2, + "statementsDispatched": 7 + }, + "wasm": { + "compileError": "BackendError: : LLVM backend can't lower sequences with parameters yet" + } +} diff --git a/test/fpy/golden/seq_args.wat b/test/fpy/golden/seq_args.wat new file mode 100644 index 0000000..9eef823 --- /dev/null +++ b/test/fpy/golden/seq_args.wat @@ -0,0 +1 @@ +compile error: BackendError: : LLVM backend can't lower sequences with parameters yet diff --git a/test/fpy/golden/struct_member.fpy b/test/fpy/golden/struct_member.fpy new file mode 100644 index 0000000..70d6a74 --- /dev/null +++ b/test/fpy/golden/struct_member.fpy @@ -0,0 +1,5 @@ +# Struct construction, member read, and in-place member write +p: Ref.SignalPair = Ref.SignalPair(3.0, 4.0) +p.value = 9.5 +assert p.time == 3.0 +assert p.value == 9.5 diff --git a/test/fpy/golden/struct_member.fpybc b/test/fpy/golden/struct_member.fpybc new file mode 100644 index 0000000..cd695d3 --- /dev/null +++ b/test/fpy/golden/struct_member.fpybc @@ -0,0 +1,26 @@ +push_val 255 +allocate 8 +push_val 64 64 0 0 64 128 0 0 +store_rel_const_offset 1 8 +push_val 65 24 0 0 +store_rel_const_offset 5 4 +load_rel 1 8 +push_val 0 0 0 0 +get_field 8 4 +fpext +push_val 64 8 0 0 0 0 0 0 +feq +not +if 16 +push_val 0 0 0 7 +exit +load_rel 1 8 +push_val 0 0 0 4 +get_field 8 4 +fpext +push_val 64 35 0 0 0 0 0 0 +feq +not +if 26 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/struct_member.json b/test/fpy/golden/struct_member.json new file mode 100644 index 0000000..89a7921 --- /dev/null +++ b/test/fpy/golden/struct_member.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff4040000041180000", + "state": 2, + "statementsDispatched": 22 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/struct_member.wat b/test/fpy/golden/struct_member.wat new file mode 100644 index 0000000..4554324 --- /dev/null +++ b/test/fpy/golden/struct_member.wat @@ -0,0 +1,56 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i64.const 4690499012984307712 + i64.store p + block + block + i32.const 1 + i32.eqz + br_if 0 + i32.const 0 + f32.load p+4 + f64.promote_f32 + f64.const 0x1.3p3 + f64.ne + br_if 1 + return +.LBB0_3: + end_block + i32.const 7 + call exit + unreachable +.LBB0_4: + end_block + i32.const 7 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type p,@object + .section .bss.p,"",@ + .p2align 3, 0x0 +p: + .skip 8 + .size p, 8 + diff --git a/test/fpy/golden/tlm_read.fpy b/test/fpy/golden/tlm_read.fpy new file mode 100644 index 0000000..0b8bd5d --- /dev/null +++ b/test/fpy/golden/tlm_read.fpy @@ -0,0 +1,4 @@ +# Telemetry read (the harness answers with the value in RUN_INPUTS) +if CdhCore.cmdDisp.CommandsDispatched >= 5: + exit(0) +exit(1) diff --git a/test/fpy/golden/tlm_read.fpybc b/test/fpy/golden/tlm_read.fpybc new file mode 100644 index 0000000..7bafcd9 --- /dev/null +++ b/test/fpy/golden/tlm_read.fpybc @@ -0,0 +1,11 @@ +push_val 255 +push_tlm_val 16777216 +ziext_32_64 +push_val 0 0 0 0 0 0 0 5 +uge +if 9 +push_val 0 0 0 0 +exit +goto 9 +push_val 0 0 0 1 +exit diff --git a/test/fpy/golden/tlm_read.json b/test/fpy/golden/tlm_read.json new file mode 100644 index 0000000..22a47ae --- /dev/null +++ b/test/fpy/golden/tlm_read.json @@ -0,0 +1,24 @@ +{ + "fpybc": { + "cmdResponse": 0, + "cmds": [], + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [], + "stack": "ff", + "state": 2, + "statementsDispatched": 8 + }, + "wasm": { + "compileError": "BackendError: : LLVM backend can't read telemetry channels or parameters yet" + } +} diff --git a/test/fpy/golden/tlm_read.wat b/test/fpy/golden/tlm_read.wat new file mode 100644 index 0000000..21e9f2d --- /dev/null +++ b/test/fpy/golden/tlm_read.wat @@ -0,0 +1 @@ +compile error: BackendError: : LLVM backend can't read telemetry channels or parameters yet diff --git a/test/fpy/golden/unary_ops.fpy b/test/fpy/golden/unary_ops.fpy new file mode 100644 index 0000000..4ec9f93 --- /dev/null +++ b/test/fpy/golden/unary_ops.fpy @@ -0,0 +1,6 @@ +# Unary negate and not on runtime operands +x: I64 = 5 +assert -x == -5 +assert +x == 5 +b: bool = False +assert not b diff --git a/test/fpy/golden/unary_ops.fpybc b/test/fpy/golden/unary_ops.fpybc new file mode 100644 index 0000000..d545b96 --- /dev/null +++ b/test/fpy/golden/unary_ops.fpybc @@ -0,0 +1,29 @@ +push_val 255 +allocate 9 +push_val 0 0 0 0 0 0 0 5 +store_rel_const_offset 1 8 +load_rel 1 8 +push_val 255 255 255 255 255 255 255 255 +mul +push_val 255 255 255 255 255 255 255 251 +ieq +not +if 13 +push_val 0 0 0 7 +exit +load_rel 1 8 +no_op +push_val 0 0 0 0 0 0 0 5 +ieq +not +if 21 +push_val 0 0 0 7 +exit +push_val 0 +store_rel_const_offset 9 1 +load_rel 9 1 +not +not +if 29 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/unary_ops.json b/test/fpy/golden/unary_ops.json new file mode 100644 index 0000000..69f5cfd --- /dev/null +++ b/test/fpy/golden/unary_ops.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff000000000000000500", + "state": 2, + "statementsDispatched": 23 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/unary_ops.wat b/test/fpy/golden/unary_ops.wat new file mode 100644 index 0000000..3582135 --- /dev/null +++ b/test/fpy/golden/unary_ops.wat @@ -0,0 +1,73 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i64.const 5 + i64.store x + block + block + block + i32.const 1 + i32.eqz + br_if 0 + i32.const 0 + i64.load x + i64.const 5 + i64.ne + br_if 1 + i32.const 0 + i32.const 0 + i32.store8 b + i32.const 1 + i32.eqz + br_if 2 + return +.LBB0_4: + end_block + i32.const 7 + call exit + unreachable +.LBB0_5: + end_block + i32.const 7 + call exit + unreachable +.LBB0_6: + end_block + i32.const 7 + call exit + unreachable + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 3, 0x0 +x: + .int64 0 + .size x, 8 + + .type b,@object + .section .bss.b,"",@ +b: + .int8 0 + .size b, 1 + diff --git a/test/fpy/golden/var_bool.json b/test/fpy/golden/var_bool.json new file mode 100644 index 0000000..4b0343b --- /dev/null +++ b/test/fpy/golden/var_bool.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ffff00", + "state": 2, + "statementsDispatched": 6 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/var_bool.wat b/test/fpy/golden/var_bool.wat new file mode 100644 index 0000000..44282bf --- /dev/null +++ b/test/fpy/golden/var_bool.wat @@ -0,0 +1,42 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 1 + i32.store8 x + i32.const 0 + i32.const 0 + i32.store8 y + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ +x: + .int8 0 + .size x, 1 + + .type y,@object + .section .bss.y,"",@ +y: + .int8 0 + .size y, 1 + diff --git a/test/fpy/golden/var_f32.json b/test/fpy/golden/var_f32.json new file mode 100644 index 0000000..4efa673 --- /dev/null +++ b/test/fpy/golden/var_f32.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff4048f5c3", + "state": 2, + "statementsDispatched": 4 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/var_f32.wat b/test/fpy/golden/var_f32.wat new file mode 100644 index 0000000..c5dcabb --- /dev/null +++ b/test/fpy/golden/var_f32.wat @@ -0,0 +1,34 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 1078523331 + i32.store x + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 2, 0x0 +x: + .int32 0x00000000 + .size x, 4 + diff --git a/test/fpy/golden/var_reassign.json b/test/fpy/golden/var_reassign.json new file mode 100644 index 0000000..004b9db --- /dev/null +++ b/test/fpy/golden/var_reassign.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff00000003", + "state": 2, + "statementsDispatched": 8 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/var_reassign.wat b/test/fpy/golden/var_reassign.wat new file mode 100644 index 0000000..6ae6aba --- /dev/null +++ b/test/fpy/golden/var_reassign.wat @@ -0,0 +1,34 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 3 + i32.store x + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 2, 0x0 +x: + .int32 0 + .size x, 4 + diff --git a/test/fpy/golden/var_u32.json b/test/fpy/golden/var_u32.json new file mode 100644 index 0000000..c056f19 --- /dev/null +++ b/test/fpy/golden/var_u32.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff0000002a", + "state": 2, + "statementsDispatched": 4 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/var_u32.wat b/test/fpy/golden/var_u32.wat new file mode 100644 index 0000000..c0be984 --- /dev/null +++ b/test/fpy/golden/var_u32.wat @@ -0,0 +1,34 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 42 + i32.store x + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 2, 0x0 +x: + .int32 0 + .size x, 4 + diff --git a/test/fpy/golden/while_simple.json b/test/fpy/golden/while_simple.json new file mode 100644 index 0000000..c3afa39 --- /dev/null +++ b/test/fpy/golden/while_simple.json @@ -0,0 +1,35 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff0000000a", + "state": 2, + "statementsDispatched": 129 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 0 + } +} diff --git a/test/fpy/golden/while_simple.wat b/test/fpy/golden/while_simple.wat new file mode 100644 index 0000000..3e57878 --- /dev/null +++ b/test/fpy/golden/while_simple.wat @@ -0,0 +1,52 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + i32.const 0 + i32.const 0 + i32.store x +.LBB0_1: + block + loop + i32.const 0 + i32.load x + i32.const 9 + i32.gt_u + br_if 1 + i32.const 0 + i32.const 0 + i64.load32_u x + i64.const 1 + i64.add + i64.store32 x + br 0 +.LBB0_3: + end_loop + end_block + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type x,@object + .section .bss.x,"",@ + .p2align 2, 0x0 +x: + .int32 0 + .size x, 4 + diff --git a/test/fpy/golden/write_to_port.json b/test/fpy/golden/write_to_port.json new file mode 100644 index 0000000..c0d8f0c --- /dev/null +++ b/test/fpy/golden/write_to_port.json @@ -0,0 +1,49 @@ +{ + "fpybc": { + "cmdResponse": 0, + "cmds": [], + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [ + { + "data": "0000002a", + "port": 0 + }, + { + "data": "64", + "port": 1 + }, + { + "data": "400921f9f01b866e", + "port": 2 + }, + { + "data": "3f80000040000000", + "port": 3 + }, + { + "data": "0000012c", + "port": 4 + }, + { + "data": "000461736466", + "port": 0 + } + ], + "stack": "ff0000002a64400921f9f01b866e3f80000040000000", + "state": 2, + "statementsDispatched": 22 + }, + "wasm": { + "compileError": "NotImplementedError: this builtin has no LLVM/wasm lowering yet" + } +} diff --git a/test/fpy/golden/write_to_port.wat b/test/fpy/golden/write_to_port.wat new file mode 100644 index 0000000..c44b636 --- /dev/null +++ b/test/fpy/golden/write_to_port.wat @@ -0,0 +1 @@ +compile error: NotImplementedError: this builtin has no LLVM/wasm lowering yet diff --git a/test/fpy/test_golden.py b/test/fpy/test_golden.py index 1bdcdd0..8e5d055 100644 --- a/test/fpy/test_golden.py +++ b/test/fpy/test_golden.py @@ -1,30 +1,62 @@ """ Golden tests for the fpy compiler. -These tests compile simple fpy programs and compare the generated bytecode -against expected output stored in golden files. - -Golden files are stored in test/fpy/golden/ with: -- .fpy: The source file to compile -- .fpybc: The expected bytecode output +Each golden case is a .fpy source file in test/fpy/golden/ with golden +artifacts alongside it: +- .fpybc: the bytecode backend's assembly output +- .wat: the LLVM backend's WebAssembly text output, or the backend + error it raises for constructs it does not support +- .json: the raw JSON replies from running the compiled sequence on + the FpySequencer ("fpybc") and WasmSequencer ("wasm") test harnesses -- + commands dispatched, events, final state, stack bytes. The fields both + replies agree on are stored once under "common"; each backend's key + holds only the fields where it diverges from the other + +Regenerate the artifacts with: + uv run pytest test/fpy/test_golden.py --update-goldens """ -import pytest +import json from pathlib import Path +import pytest + import fpy.error -from fpy.compiler import text_to_ast, analyze_ast, analysis_to_fpybc_directives -from fpy.state import get_base_compile_state from fpy.bytecode.assembler import fpybc_directives_to_fpyasm +from fpy.compiler import ( + analysis_to_fpybc_directives, + analysis_to_wasm, + analysis_to_wat, + analyze_ast, + text_to_ast, +) +from fpy.error import BackendError +from fpy.state import get_base_compile_state +from fpy.test_helpers import run_seq_raw, run_wasm_raw +from fpy.types import FpyValue, U32 GOLDEN_DIR = Path(__file__).parent / "golden" +# The backends a golden run file records, one harness each. +BACKENDS = ("fpybc", "wasm") + # Path to the test dictionary DEFAULT_DICTIONARY = str(Path(__file__).parent / "RefTopologyDictionary.json") +# Harness inputs for cases whose sequences read values from the outside: +# telemetry answers and sequence arguments, keyed by case name. These apply +# to the fpybc harness run; the wasm backend rejects such sequences. +RUN_INPUTS = { + "tlm_read": { + "tlm": {"CdhCore.cmdDisp.CommandsDispatched": FpyValue(U32, 5).serialize()} + }, + "seq_args": {"args": [FpyValue(U32, 7)]}, +} -def compile_to_fpybc(source: str) -> str: - """Compile fpy source code to fpybc bytecode text.""" + +def _analyze(source: str): + """Parse and semantically analyze fpy source, returning the compile + state ready for a backend.""" fpy.error.file_name = "" fpy.error.input_text = source fpy.error.input_lines = source.splitlines() @@ -34,62 +66,158 @@ def compile_to_fpybc(source: str) -> str: body = text_to_ast(source) assert body is not None, "Parsing failed" - state = analyze_ast(body, state) - directives, _ = analysis_to_fpybc_directives(state) - return fpybc_directives_to_fpyasm(directives) - + return analyze_ast(body, state) -def get_golden_test_cases(): - """Find all golden test cases (pairs of .fpy and .fpybc files).""" - if not GOLDEN_DIR.exists(): - return [] - test_cases = [] - for fpy_file in sorted(GOLDEN_DIR.glob("*.fpy")): - fpybc_file = fpy_file.with_suffix(".fpybc") - if fpybc_file.exists(): - test_cases.append(fpy_file.stem) - return test_cases +def compile_to_fpybc(source: str) -> str: + """Compile fpy source code to fpybc bytecode text.""" + directives, _ = analysis_to_fpybc_directives(_analyze(source)) + return fpybc_directives_to_fpyasm(directives) -@pytest.mark.parametrize("test_name", get_golden_test_cases()) -def test_golden(test_name: str): - """ - Golden test: compile the .fpy file and compare against the .fpybc file. - """ - fpy_file = GOLDEN_DIR / f"{test_name}.fpy" - fpybc_file = GOLDEN_DIR / f"{test_name}.fpybc" +def compile_to_wat(source: str) -> str: + """Compile fpy source code to WebAssembly text, or the error message the + LLVM backend raises for sequences it does not support.""" + state = _analyze(source) + try: + wat, _ = analysis_to_wat(state) + return wat + except (BackendError, NotImplementedError) as e: + return f"compile error: {type(e).__name__}: {e}\n" + + +def run_on_fpybc_harness(name: str, source: str) -> dict: + """Compile fpy source to bytecode and run it on the FpySequencer harness, + returning the raw JSON reply.""" + directives, arg_types = analysis_to_fpybc_directives(_analyze(source)) + inputs = dict(RUN_INPUTS.get(name, {})) + if "args" in inputs: + inputs["args"] = b"".join(v.serialize() for v in inputs["args"]) + reply = run_seq_raw(directives, arg_types=arg_types, **inputs) + assert "error" not in reply, f"harness failed to run {name}: {reply}" + return reply + + +def run_on_wasm_harness(name: str, source: str) -> dict: + """Compile fpy source to wasm and run it on the WasmSequencer harness, + returning the raw JSON reply, or the error message the LLVM backend + raises for sequences it does not support.""" + state = _analyze(source) + try: + wasm, _ = analysis_to_wasm(state) + except (BackendError, NotImplementedError) as e: + return {"compileError": f"{type(e).__name__}: {e}"} + reply = run_wasm_raw(wasm) + assert "error" not in reply, f"harness failed to run {name}: {reply}" + return reply - source = fpy_file.read_text() - expected = fpybc_file.read_text() - - actual = compile_to_fpybc(source) +def get_golden_test_cases(): + """Find all golden test cases (.fpy files).""" + return sorted(f.stem for f in GOLDEN_DIR.glob("*.fpy")) + + +def _check_or_update_text(path: Path, actual: str, update: bool): + """Compare *actual* against the golden file, or rewrite it with + --update-goldens.""" + if update: + if not path.exists() or path.read_text() != actual: + path.write_text(actual) + return + assert path.exists(), ( + f"golden file {path.name} is missing; generate it with " + "'pytest test/fpy/test_golden.py --update-goldens'" + ) + expected = path.read_text() assert actual == expected, ( - f"Golden test '{test_name}' failed.\n" + f"Golden test '{path.name}' failed.\n" f"Expected:\n{expected}\n" f"Actual:\n{actual}\n" ) -def update_golden(test_name: str): - """ - Utility to update a golden file. Run manually if needed. - """ - fpy_file = GOLDEN_DIR / f"{test_name}.fpy" - fpybc_file = GOLDEN_DIR / f"{test_name}.fpybc" +def _merged_run(recorded: dict, backend: str) -> dict | None: + """The full reply recorded for *backend*: the "common" fields plus the + backend's own section. None when the backend has no recorded run.""" + if backend not in recorded: + return None + return {**recorded.get("common", {}), **recorded[backend]} + + +def _split_runs(replies: dict[str, dict]) -> dict: + """The golden file layout for the backends' full replies: the fields + every reply agrees on once under "common", the rest under each backend's + own key.""" + first, *rest = replies.values() + common = {} + if rest: + common = { + k: v for k, v in first.items() if all(k in r and r[k] == v for r in rest) + } + split = {"common": common} if common else {} + for backend, reply in replies.items(): + split[backend] = {k: v for k, v in reply.items() if k not in common} + return split + + +def _check_or_update_run(path: Path, backend: str, actual: dict, update: bool): + """Compare *actual* against the *backend* run recorded in the golden run + file, or rewrite that run with --update-goldens.""" + recorded = json.loads(path.read_text()) if path.exists() else {} + replies = {b: _merged_run(recorded, b) for b in BACKENDS} + replies = {b: r for b, r in replies.items() if r is not None} + if update: + replies[backend] = actual + split = _split_runs(replies) + if split != recorded: + path.write_text(json.dumps(split, indent=2, sort_keys=True) + "\n") + return + assert backend in replies, ( + f"golden file {path.name} has no '{backend}' run; generate it with " + "'pytest test/fpy/test_golden.py --update-goldens'" + ) + assert actual == replies[backend], ( + f"Golden run '{path.name}' ({backend}) failed.\n" + f"Expected:\n{json.dumps(replies[backend], indent=2, sort_keys=True)}\n" + f"Actual:\n{json.dumps(actual, indent=2, sort_keys=True)}\n" + ) - source = fpy_file.read_text() + +@pytest.mark.parametrize("test_name", get_golden_test_cases()) +def test_golden_fpybc(test_name: str, update_goldens: bool): + """Compile the .fpy file and compare against the .fpybc file.""" + source = (GOLDEN_DIR / f"{test_name}.fpy").read_text() actual = compile_to_fpybc(source) + _check_or_update_text(GOLDEN_DIR / f"{test_name}.fpybc", actual, update_goldens) - fpybc_file.write_text(actual) - print(f"Updated {fpybc_file}") + +@pytest.mark.parametrize("test_name", get_golden_test_cases()) +def test_golden_wat(test_name: str, update_goldens: bool): + """Compile the .fpy file with the LLVM backend and compare against the + .wat file.""" + source = (GOLDEN_DIR / f"{test_name}.fpy").read_text() + actual = compile_to_wat(source) + _check_or_update_text(GOLDEN_DIR / f"{test_name}.wat", actual, update_goldens) + + +@pytest.mark.parametrize("test_name", get_golden_test_cases()) +def test_golden_run_fpybc(test_name: str, update_goldens: bool): + """Run the compiled sequence on the FpySequencer harness and compare the + raw reply against the .json file's "fpybc" entry.""" + source = (GOLDEN_DIR / f"{test_name}.fpy").read_text() + actual = run_on_fpybc_harness(test_name, source) + _check_or_update_run( + GOLDEN_DIR / f"{test_name}.json", "fpybc", actual, update_goldens + ) -def update_all_golden_seqs(): - """ - Utility to update all golden files. Run manually if needed. - """ - for fpy_file in sorted(GOLDEN_DIR.glob("*.fpy")): - test_name = fpy_file.stem - update_golden(test_name) +@pytest.mark.wasm +@pytest.mark.parametrize("test_name", get_golden_test_cases()) +def test_golden_run_wasm(test_name: str, update_goldens: bool): + """Run the compiled sequence on the WasmSequencer harness and compare the + raw reply against the .json file's "wasm" entry.""" + source = (GOLDEN_DIR / f"{test_name}.fpy").read_text() + actual = run_on_wasm_harness(test_name, source) + _check_or_update_run( + GOLDEN_DIR / f"{test_name}.json", "wasm", actual, update_goldens + ) From d46da1546d9e8ddba0f474461515e5522460b9d9 Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Thu, 13 Aug 2026 10:05:15 -0400 Subject: [PATCH 05/10] Update golden test helper --- src/fpy/test_helpers.py | 88 ++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/src/fpy/test_helpers.py b/src/fpy/test_helpers.py index aa43b0a..27aee81 100644 --- a/src/fpy/test_helpers.py +++ b/src/fpy/test_helpers.py @@ -242,7 +242,7 @@ def _as_int(v) -> int: return v.value if isinstance(v, DirectiveErrorCode) else v -def run_seq( +def run_seq_raw( directives: list[Directive], tlm: dict[str, bytes] = None, time_base: int = 0, @@ -254,18 +254,11 @@ def run_seq( seq_run_opcodes: set[int] = None, ground_binary_dir: str = None, prms: dict[str, bytes] = None, -) -> list[bytes]: +) -> dict: """Run a list of directives on a real Svc::FpySequencer through the test - harness (test/harness). *tlm* and *prms* map channel/parameter names to - the serialized values the harness answers reads with. Returns the command - buffers the sequence dispatched (the big-endian serialized FwOpcodeType + - arguments), in call order. - - Raises ValidationError when the sequencer rejects the sequence before - running it, and RuntimeError when the sequence fails: with the - DirectiveErrorCode for a trap, or the raw error code int for a nonzero - exit. - """ + harness (test/harness) and return the harness's raw JSON reply. *tlm* and + *prms* map channel/parameter names to the serialized values the harness + answers reads with.""" d = load_dictionary(default_dictionary) # When the test provides a ground_binary_dir, that directory doubles as @@ -302,7 +295,46 @@ def run_seq( request["seqRunOpcodes"] = sorted(seq_run_opcodes) request["seqArgsBufferSize"] = _seq_args_buffer_len(d) - result = fpy_harness().run(request) + return fpy_harness().run(request) + + +def run_seq( + directives: list[Directive], + tlm: dict[str, bytes] = None, + time_base: int = 0, + time_context: int = 0, + initial_time_us: int = 0, + failing_opcodes: set[int] = None, + args: bytes = None, + arg_types: list[tuple[str, FpyType]] = None, + seq_run_opcodes: set[int] = None, + ground_binary_dir: str = None, + prms: dict[str, bytes] = None, +) -> list[bytes]: + """Run a list of directives on a real Svc::FpySequencer through the test + harness (test/harness). *tlm* and *prms* map channel/parameter names to + the serialized values the harness answers reads with. Returns the command + buffers the sequence dispatched (the big-endian serialized FwOpcodeType + + arguments), in call order. + + Raises ValidationError when the sequencer rejects the sequence before + running it, and RuntimeError when the sequence fails: with the + DirectiveErrorCode for a trap, or the raw error code int for a nonzero + exit. + """ + result = run_seq_raw( + directives, + tlm=tlm, + time_base=time_base, + time_context=time_context, + initial_time_us=initial_time_us, + failing_opcodes=failing_opcodes, + args=args, + arg_types=arg_types, + seq_run_opcodes=seq_run_opcodes, + ground_binary_dir=ground_binary_dir, + prms=prms, + ) if "error" in result: raise HarnessError(result["error"]) @@ -349,18 +381,13 @@ def run_seq( raise RuntimeError(DirectiveErrorCode(result["lastDirectiveError"])) -def run_wasm( +def run_wasm_raw( wasm: bytes, failing_opcodes: set[int] = None, cmd_response: int = None, -) -> tuple[int, list[tuple[int, str]], list[bytes]]: +) -> dict: """Run an already-linked wasm module on a real Svc::WasmSequencer through - the wasm harness and return (error code, reported events, dispatched - command buffers). - - Every command completes with *cmd_response* (an Fw.CmdResponse value, - default OK) unless its opcode is in *failing_opcodes*, which makes it - complete with EXECUTION_ERROR.""" + the wasm harness and return the harness's raw JSON reply.""" seq_dir, seq_file = _write_for_harness(wasm, "m0.wasm") request = { "seqFile": seq_file, @@ -371,7 +398,24 @@ def run_wasm( if cmd_response is not None: request["cmdResponse"] = cmd_response - result = wasm_harness().run(request) + return wasm_harness().run(request) + + +def run_wasm( + wasm: bytes, + failing_opcodes: set[int] = None, + cmd_response: int = None, +) -> tuple[int, list[tuple[int, str]], list[bytes]]: + """Run an already-linked wasm module on a real Svc::WasmSequencer through + the wasm harness and return (error code, reported events, dispatched + command buffers). + + Every command completes with *cmd_response* (an Fw.CmdResponse value, + default OK) unless its opcode is in *failing_opcodes*, which makes it + complete with EXECUTION_ERROR.""" + result = run_wasm_raw( + wasm, failing_opcodes=failing_opcodes, cmd_response=cmd_response + ) if "error" in result: raise HarnessError(result["error"]) From 59acb5b16f11a8ca95c9982334e8713ab467d13b Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Thu, 13 Aug 2026 10:34:04 -0400 Subject: [PATCH 06/10] Add magic comments for golden tests --- src/fpy/test_helpers.py | 107 +++++++++++++++++------- test/fpy/golden/cmd_fail.fpy | 3 + test/fpy/golden/cmd_fail.fpybc | 10 +++ test/fpy/golden/cmd_fail.json | 49 +++++++++++ test/fpy/golden/cmd_fail.wat | 47 +++++++++++ test/fpy/golden/cmd_response_busy.fpy | 5 ++ test/fpy/golden/cmd_response_busy.fpybc | 11 +++ test/fpy/golden/cmd_response_busy.json | 37 ++++++++ test/fpy/golden/cmd_response_busy.wat | 55 ++++++++++++ test/fpy/golden/prm_read.fpy | 5 ++ test/fpy/golden/prm_read.fpybc | 11 +++ test/fpy/golden/prm_read.json | 24 ++++++ test/fpy/golden/prm_read.wat | 1 + test/fpy/golden/seq_args.fpy | 3 +- test/fpy/golden/start_time.fpy | 4 + test/fpy/golden/start_time.fpybc | 76 +++++++++++++++++ test/fpy/golden/start_time.json | 24 ++++++ test/fpy/golden/start_time.wat | 1 + test/fpy/golden/tlm_read.fpy | 3 +- test/fpy/test_golden.py | 64 ++++++++++---- 20 files changed, 493 insertions(+), 47 deletions(-) create mode 100644 test/fpy/golden/cmd_fail.fpy create mode 100644 test/fpy/golden/cmd_fail.fpybc create mode 100644 test/fpy/golden/cmd_fail.json create mode 100644 test/fpy/golden/cmd_fail.wat create mode 100644 test/fpy/golden/cmd_response_busy.fpy create mode 100644 test/fpy/golden/cmd_response_busy.fpybc create mode 100644 test/fpy/golden/cmd_response_busy.json create mode 100644 test/fpy/golden/cmd_response_busy.wat create mode 100644 test/fpy/golden/prm_read.fpy create mode 100644 test/fpy/golden/prm_read.fpybc create mode 100644 test/fpy/golden/prm_read.json create mode 100644 test/fpy/golden/prm_read.wat create mode 100644 test/fpy/golden/start_time.fpy create mode 100644 test/fpy/golden/start_time.fpybc create mode 100644 test/fpy/golden/start_time.json create mode 100644 test/fpy/golden/start_time.wat diff --git a/src/fpy/test_helpers.py b/src/fpy/test_helpers.py index 27aee81..65357e8 100644 --- a/src/fpy/test_helpers.py +++ b/src/fpy/test_helpers.py @@ -205,6 +205,49 @@ def _always_failing_opcodes(failing_opcodes) -> set[int]: return {d["cmd_name_dict"]["Ref.cmdSeq0.RUN"].opcode} | set(failing_opcodes or ()) +def _run_request( + seq_file: str, + seq_dir: str, + tlm: dict[str, bytes] = None, + prms: dict[str, bytes] = None, + time_base: int = 0, + time_context: int = 0, + initial_time_us: int = 0, + failing_opcodes: set[int] = None, + args: bytes = None, + cmd_response: int = None, +) -> dict: + """The run request fields common to both sequencer harnesses. *tlm* and + *prms* map channel/parameter names to the serialized values the harness + answers reads with; every command completes with *cmd_response* (default + OK) unless its opcode is in *failing_opcodes*.""" + d = load_dictionary(default_dictionary) + request = { + "seqFile": seq_file, + "cwd": seq_dir, + "time": { + "base": time_base, + "context": time_context, + "seconds": initial_time_us // 1_000_000, + "useconds": initial_time_us % 1_000_000, + }, + "tlm": { + str(d["ch_name_dict"][chan_name].ch_id): bytes(val).hex() + for chan_name, val in (tlm or {}).items() + }, + "prms": { + str(d["prm_name_dict"][prm_name].prm_id): bytes(val).hex() + for prm_name, val in (prms or {}).items() + }, + "failOpcodes": sorted(_always_failing_opcodes(failing_opcodes)), + } + if args is not None: + request["args"] = args.hex() + if cmd_response is not None: + request["cmdResponse"] = cmd_response + return request + + def _seq_args_buffer_len(d: dict) -> int: """The dictionary's Svc.SeqArgs buffer length. The harness needs it to parse seq-run commands, and it can differ from the flight build's own @@ -254,6 +297,7 @@ def run_seq_raw( seq_run_opcodes: set[int] = None, ground_binary_dir: str = None, prms: dict[str, bytes] = None, + cmd_response: int = None, ) -> dict: """Run a list of directives on a real Svc::FpySequencer through the test harness (test/harness) and return the harness's raw JSON reply. *tlm* and @@ -270,27 +314,18 @@ def run_seq_raw( if seq_run_opcodes is None and ground_binary_dir is not None: seq_run_opcodes = {d["cmd_name_dict"]["Ref.seqDisp.RUN_ARGS"].opcode} - request = { - "seqFile": seq_file, - "cwd": seq_dir, - "time": { - "base": time_base, - "context": time_context, - "seconds": initial_time_us // 1_000_000, - "useconds": initial_time_us % 1_000_000, - }, - "tlm": { - str(d["ch_name_dict"][chan_name].ch_id): bytes(val).hex() - for chan_name, val in (tlm or {}).items() - }, - "prms": { - str(d["prm_name_dict"][prm_name].prm_id): bytes(val).hex() - for prm_name, val in (prms or {}).items() - }, - "failOpcodes": sorted(_always_failing_opcodes(failing_opcodes)), - } - if args is not None: - request["args"] = args.hex() + request = _run_request( + seq_file, + seq_dir, + tlm=tlm, + prms=prms, + time_base=time_base, + time_context=time_context, + initial_time_us=initial_time_us, + failing_opcodes=failing_opcodes, + args=args, + cmd_response=cmd_response, + ) if seq_run_opcodes: request["seqRunOpcodes"] = sorted(seq_run_opcodes) request["seqArgsBufferSize"] = _seq_args_buffer_len(d) @@ -383,21 +418,31 @@ def run_seq( def run_wasm_raw( wasm: bytes, + tlm: dict[str, bytes] = None, + prms: dict[str, bytes] = None, + time_base: int = 0, + time_context: int = 0, + initial_time_us: int = 0, failing_opcodes: set[int] = None, + args: bytes = None, cmd_response: int = None, ) -> dict: """Run an already-linked wasm module on a real Svc::WasmSequencer through - the wasm harness and return the harness's raw JSON reply.""" + the wasm harness and return the harness's raw JSON reply. See _run_request + for the inputs.""" seq_dir, seq_file = _write_for_harness(wasm, "m0.wasm") - request = { - "seqFile": seq_file, - "cwd": seq_dir, - "time": {"base": 0, "context": 0, "seconds": 0, "useconds": 0}, - "failOpcodes": sorted(_always_failing_opcodes(failing_opcodes)), - } - if cmd_response is not None: - request["cmdResponse"] = cmd_response - + request = _run_request( + seq_file, + seq_dir, + tlm=tlm, + prms=prms, + time_base=time_base, + time_context=time_context, + initial_time_us=initial_time_us, + failing_opcodes=failing_opcodes, + args=args, + cmd_response=cmd_response, + ) return wasm_harness().run(request) diff --git a/test/fpy/golden/cmd_fail.fpy b/test/fpy/golden/cmd_fail.fpy new file mode 100644 index 0000000..5698676 --- /dev/null +++ b/test/fpy/golden/cmd_fail.fpy @@ -0,0 +1,3 @@ +# A bare command that completes with EXECUTION_ERROR ends the sequence +# harness: fail CdhCore.cmdDisp.CMD_NO_OP +CdhCore.cmdDisp.CMD_NO_OP() diff --git a/test/fpy/golden/cmd_fail.fpybc b/test/fpy/golden/cmd_fail.fpybc new file mode 100644 index 0000000..739a74a --- /dev/null +++ b/test/fpy/golden/cmd_fail.fpybc @@ -0,0 +1,10 @@ +push_val 255 +const_cmd 16777216 +push_val 0 +memcmp 1 +if 6 +goto 10 +load_abs 0 1 +if 10 +push_val 0 0 0 17 +exit diff --git a/test/fpy/golden/cmd_fail.json b/test/fpy/golden/cmd_fail.json new file mode 100644 index 0000000..e96f612 --- /dev/null +++ b/test/fpy/golden/cmd_fail.json @@ -0,0 +1,49 @@ +{ + "common": { + "cmdResponse": 4, + "cmds": [ + "01000000" + ], + "exitCode": 17, + "frameStart": 0, + "reachedRunning": true, + "sequencesSucceeded": 0, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 17, + "severity": 2, + "text": "(FpySequencer) SequenceExitedWithError : Sequence s0.bin exited with error code 17" + } + ], + "lastDirectiveError": 7, + "stack": "ff", + "state": 2, + "statementsDispatched": 9 + }, + "wasm": { + "events": [ + { + "id": 14, + "severity": 2, + "text": "(WasmSequencer) SequenceExitedWithError : Wasm program exited with error code 17" + }, + { + "id": 15, + "severity": 2, + "text": "(WasmSequencer) SequenceTrap : Wasm program trapped: HOST (2)" + }, + { + "id": 0, + "severity": 7, + "text": "(WasmSequencer) StoreAllocationSucceeded : Successfully allocated store with 8 modules" + } + ], + "lastDirectiveError": 0, + "stack": "", + "state": 1, + "statementsDispatched": 1 + } +} diff --git a/test/fpy/golden/cmd_fail.wat b/test/fpy/golden/cmd_fail.wat new file mode 100644 index 0000000..1ab8c03 --- /dev/null +++ b/test/fpy/golden/cmd_fail.wat @@ -0,0 +1,47 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + block + i32.const .Lcmd_buf + i32.const 4 + call cmd + i32.const 255 + i32.and + i32.eqz + br_if 0 + i32.const 0 + i32.load8_u flags + i32.eqz + br_if 0 + i32.const 17 + call exit + unreachable +.LBB0_3: + end_block + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type .Lcmd_buf,@object + .section .rodata..Lcmd_buf,"",@ +.Lcmd_buf: + .asciz "\001\000\000" + .size .Lcmd_buf, 4 + diff --git a/test/fpy/golden/cmd_response_busy.fpy b/test/fpy/golden/cmd_response_busy.fpy new file mode 100644 index 0000000..45b753e --- /dev/null +++ b/test/fpy/golden/cmd_response_busy.fpy @@ -0,0 +1,5 @@ +# Every command completes BUSY; capturing the response takes responsibility +# for it, so the sequence keeps running +# harness: cmd_response 5 +ret: Fw.CmdResponse = CdhCore.cmdDisp.CMD_NO_OP() +assert ret == Fw.CmdResponse.BUSY diff --git a/test/fpy/golden/cmd_response_busy.fpybc b/test/fpy/golden/cmd_response_busy.fpybc new file mode 100644 index 0000000..1b9326c --- /dev/null +++ b/test/fpy/golden/cmd_response_busy.fpybc @@ -0,0 +1,11 @@ +push_val 255 +allocate 1 +const_cmd 16777216 +store_rel_const_offset 1 1 +load_rel 1 1 +push_val 5 +memcmp 1 +not +if 11 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/cmd_response_busy.json b/test/fpy/golden/cmd_response_busy.json new file mode 100644 index 0000000..11e9047 --- /dev/null +++ b/test/fpy/golden/cmd_response_busy.json @@ -0,0 +1,37 @@ +{ + "common": { + "cmdResponse": 0, + "cmds": [ + "01000000" + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [] + }, + "fpybc": { + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "stack": "ff05", + "state": 2, + "statementsDispatched": 9 + }, + "wasm": { + "events": [ + { + "id": 7, + "severity": 5, + "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" + } + ], + "stack": "", + "state": 3, + "statementsDispatched": 1 + } +} diff --git a/test/fpy/golden/cmd_response_busy.wat b/test/fpy/golden/cmd_response_busy.wat new file mode 100644 index 0000000..d66d045 --- /dev/null +++ b/test/fpy/golden/cmd_response_busy.wat @@ -0,0 +1,55 @@ + .file "" + .functype exit (i32) -> () + .import_module exit, fprime_v1 + .functype panic (i32) -> () + .import_module panic, fprime_v1 + .functype event (i32, i32, i32) -> () + .import_module event, fprime_v1 + .functype cmd (i32, i32) -> (i32) + .import_module cmd, fprime_v1 + .functype main () -> () + .section .text.main,"",@ + .globl main + .type main,@function +main: + .functype main () -> () + .local i32 + i32.const 0 + i32.const .Lcmd_buf + i32.const 4 + call cmd + local.tee 0 + i32.store8 ret + block + local.get 0 + i32.const 255 + i32.and + i32.const 5 + i32.eq + br_if 0 + i32.const 7 + call exit + unreachable +.LBB0_2: + end_block + end_function + + .type flags,@object + .section .data.flags,"",@ + .p2align 3, 0x0 +flags: + .int8 1 + .size flags, 1 + + .type ret,@object + .section .bss.ret,"",@ +ret: + .int8 0 + .size ret, 1 + + .type .Lcmd_buf,@object + .section .rodata..Lcmd_buf,"",@ +.Lcmd_buf: + .asciz "\001\000\000" + .size .Lcmd_buf, 4 + diff --git a/test/fpy/golden/prm_read.fpy b/test/fpy/golden/prm_read.fpy new file mode 100644 index 0000000..d05315c --- /dev/null +++ b/test/fpy/golden/prm_read.fpy @@ -0,0 +1,5 @@ +# Parameter read; the harness answers with the declared value (F32 5.0) +# harness: prm Ref.cmdSeq0.STATEMENT_TIMEOUT_SECS 40a00000 +if Ref.cmdSeq0.STATEMENT_TIMEOUT_SECS == 5.0: + exit(0) +exit(1) diff --git a/test/fpy/golden/prm_read.fpybc b/test/fpy/golden/prm_read.fpybc new file mode 100644 index 0000000..7630ecb --- /dev/null +++ b/test/fpy/golden/prm_read.fpybc @@ -0,0 +1,11 @@ +push_val 255 +push_prm 268460032 +fpext +push_val 64 20 0 0 0 0 0 0 +feq +if 9 +push_val 0 0 0 0 +exit +goto 9 +push_val 0 0 0 1 +exit diff --git a/test/fpy/golden/prm_read.json b/test/fpy/golden/prm_read.json new file mode 100644 index 0000000..22a47ae --- /dev/null +++ b/test/fpy/golden/prm_read.json @@ -0,0 +1,24 @@ +{ + "fpybc": { + "cmdResponse": 0, + "cmds": [], + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [], + "stack": "ff", + "state": 2, + "statementsDispatched": 8 + }, + "wasm": { + "compileError": "BackendError: : LLVM backend can't read telemetry channels or parameters yet" + } +} diff --git a/test/fpy/golden/prm_read.wat b/test/fpy/golden/prm_read.wat new file mode 100644 index 0000000..21e9f2d --- /dev/null +++ b/test/fpy/golden/prm_read.wat @@ -0,0 +1 @@ +compile error: BackendError: : LLVM backend can't read telemetry channels or parameters yet diff --git a/test/fpy/golden/seq_args.fpy b/test/fpy/golden/seq_args.fpy index 92a3b09..70362d5 100644 --- a/test/fpy/golden/seq_args.fpy +++ b/test/fpy/golden/seq_args.fpy @@ -1,3 +1,4 @@ -# Sequence arguments (the harness passes the values in RUN_INPUTS) +# Sequence arguments; the harness passes the declared bytes (U32 7) +# harness: arg 00000007 sequence(x: U32) assert x == 7 diff --git a/test/fpy/golden/start_time.fpy b/test/fpy/golden/start_time.fpy new file mode 100644 index 0000000..0ba496a --- /dev/null +++ b/test/fpy/golden/start_time.fpy @@ -0,0 +1,4 @@ +# The sequencer starts at the declared simulated time +# harness: time 2 0 5250000 +t: Fw.Time = now() +assert t == Fw.Time(TimeBase.TB_WORKSTATION_TIME, 0, 5, 250000) diff --git a/test/fpy/golden/start_time.fpybc b/test/fpy/golden/start_time.fpybc new file mode 100644 index 0000000..f277bf9 --- /dev/null +++ b/test/fpy/golden/start_time.fpybc @@ -0,0 +1,76 @@ +goto 61 +allocate 16 +load_rel -30 11 +push_val 0 0 0 0 +get_field 11 2 +load_rel -19 11 +push_val 0 0 0 0 +get_field 11 2 +memcmp 2 +not +if 21 +push_val 2 +push_val 116 105 109 101 95 99 109 112 95 97 115 115 101 114 116 95 99 111 109 112 97 114 97 98 108 101 58 32 111 112 101 114 97 110 100 115 32 104 97 118 101 32 100 105 102 102 101 114 101 110 116 32 116 105 109 101 32 98 97 115 101 115 +push_val 0 0 0 62 +pop_event +push_val 0 +not +if 20 +push_val 0 0 0 1 +exit +goto 21 +load_rel -30 11 +push_val 0 0 0 3 +get_field 11 4 +ziext_32_64 +push_val 0 0 0 0 0 15 66 64 +mul +load_rel -30 11 +push_val 0 0 0 7 +get_field 11 4 +ziext_32_64 +add +store_rel_const_offset 0 8 +load_rel -19 11 +push_val 0 0 0 3 +get_field 11 4 +ziext_32_64 +push_val 0 0 0 0 0 15 66 64 +mul +load_rel -19 11 +push_val 0 0 0 7 +get_field 11 4 +ziext_32_64 +add +store_rel_const_offset 8 8 +load_rel 0 8 +load_rel 8 8 +ult +if 52 +push_val 255 255 255 255 +return 4 22 +goto 59 +load_rel 0 8 +load_rel 8 8 +ieq +if 59 +push_val 0 0 0 0 +return 4 22 +goto 59 +push_val 0 0 0 1 +return 4 22 +push_val 255 +allocate 11 +push_time +store_rel_const_offset 1 11 +load_rel 1 11 +push_val 0 2 0 0 0 0 5 0 3 208 144 +push_val 0 0 0 1 +call +siext_32_64 +push_val 0 0 0 0 0 0 0 0 +ieq +not +if 76 +push_val 0 0 0 7 +exit diff --git a/test/fpy/golden/start_time.json b/test/fpy/golden/start_time.json new file mode 100644 index 0000000..8b808a4 --- /dev/null +++ b/test/fpy/golden/start_time.json @@ -0,0 +1,24 @@ +{ + "fpybc": { + "cmdResponse": 0, + "cmds": [], + "events": [ + { + "id": 15, + "severity": 5, + "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" + } + ], + "frameStart": 0, + "lastDirectiveError": 0, + "reachedRunning": true, + "sequencesSucceeded": 1, + "serial": [], + "stack": "ff000200000000050003d090", + "state": 2, + "statementsDispatched": 58 + }, + "wasm": { + "compileError": "NotImplementedError: this builtin has no LLVM/wasm lowering yet" + } +} diff --git a/test/fpy/golden/start_time.wat b/test/fpy/golden/start_time.wat new file mode 100644 index 0000000..c44b636 --- /dev/null +++ b/test/fpy/golden/start_time.wat @@ -0,0 +1 @@ +compile error: NotImplementedError: this builtin has no LLVM/wasm lowering yet diff --git a/test/fpy/golden/tlm_read.fpy b/test/fpy/golden/tlm_read.fpy index 0b8bd5d..abbab5c 100644 --- a/test/fpy/golden/tlm_read.fpy +++ b/test/fpy/golden/tlm_read.fpy @@ -1,4 +1,5 @@ -# Telemetry read (the harness answers with the value in RUN_INPUTS) +# Telemetry read; the harness answers with the declared value (U32 5) +# harness: tlm CdhCore.cmdDisp.CommandsDispatched 00000005 if CdhCore.cmdDisp.CommandsDispatched >= 5: exit(0) exit(1) diff --git a/test/fpy/test_golden.py b/test/fpy/test_golden.py index 8e5d055..0d6b99a 100644 --- a/test/fpy/test_golden.py +++ b/test/fpy/test_golden.py @@ -12,11 +12,21 @@ replies agree on are stored once under "common"; each backend's key holds only the fields where it diverges from the other +A sequence declares the inputs its harness runs need in "# harness:" +comments, one directive per line: + # harness: tlm answer to a telemetry read + # harness: prm answer to a parameter read + # harness: arg appended to the sequence arguments + # harness: time the sequencer's start time + # harness: fail completes with EXECUTION_ERROR + # harness: cmd_response every command's Fw.CmdResponse + Regenerate the artifacts with: uv run pytest test/fpy/test_golden.py --update-goldens """ import json +import re from pathlib import Path import pytest @@ -30,10 +40,10 @@ analyze_ast, text_to_ast, ) +from fpy.dictionary import load_dictionary from fpy.error import BackendError from fpy.state import get_base_compile_state from fpy.test_helpers import run_seq_raw, run_wasm_raw -from fpy.types import FpyValue, U32 GOLDEN_DIR = Path(__file__).parent / "golden" @@ -43,15 +53,43 @@ # Path to the test dictionary DEFAULT_DICTIONARY = str(Path(__file__).parent / "RefTopologyDictionary.json") -# Harness inputs for cases whose sequences read values from the outside: -# telemetry answers and sequence arguments, keyed by case name. These apply -# to the fpybc harness run; the wasm backend rejects such sequences. -RUN_INPUTS = { - "tlm_read": { - "tlm": {"CdhCore.cmdDisp.CommandsDispatched": FpyValue(U32, 5).serialize()} - }, - "seq_args": {"args": [FpyValue(U32, 7)]}, -} +_HARNESS_INPUT = re.compile(r"^#\s*harness:\s*(.+?)\s*$", re.MULTILINE) + + +def parse_harness_inputs(source: str) -> dict: + """The harness run inputs declared in the sequence's "# harness:" + comments (see the module docstring for the directives), as keyword + arguments for run_seq_raw / run_wasm_raw.""" + d = load_dictionary(DEFAULT_DICTIONARY) + inputs = {"tlm": {}, "prms": {}, "failing_opcodes": set()} + args = b"" + for match in _HARNESS_INPUT.finditer(source): + directive, *operands = match.group(1).split() + if directive == "tlm": + name, value = operands + inputs["tlm"][name] = bytes.fromhex(value) + elif directive == "prm": + name, value = operands + inputs["prms"][name] = bytes.fromhex(value) + elif directive == "arg": + (value,) = operands + args += bytes.fromhex(value) + elif directive == "time": + base, context, microseconds = operands + inputs["time_base"] = int(base) + inputs["time_context"] = int(context) + inputs["initial_time_us"] = int(microseconds) + elif directive == "fail": + (name,) = operands + inputs["failing_opcodes"].add(d["cmd_name_dict"][name].opcode) + elif directive == "cmd_response": + (value,) = operands + inputs["cmd_response"] = int(value) + else: + raise ValueError(f"unknown harness input directive: {match.group(1)!r}") + if args: + inputs["args"] = args + return inputs def _analyze(source: str): @@ -90,9 +128,7 @@ def run_on_fpybc_harness(name: str, source: str) -> dict: """Compile fpy source to bytecode and run it on the FpySequencer harness, returning the raw JSON reply.""" directives, arg_types = analysis_to_fpybc_directives(_analyze(source)) - inputs = dict(RUN_INPUTS.get(name, {})) - if "args" in inputs: - inputs["args"] = b"".join(v.serialize() for v in inputs["args"]) + inputs = parse_harness_inputs(source) reply = run_seq_raw(directives, arg_types=arg_types, **inputs) assert "error" not in reply, f"harness failed to run {name}: {reply}" return reply @@ -107,7 +143,7 @@ def run_on_wasm_harness(name: str, source: str) -> dict: wasm, _ = analysis_to_wasm(state) except (BackendError, NotImplementedError) as e: return {"compileError": f"{type(e).__name__}: {e}"} - reply = run_wasm_raw(wasm) + reply = run_wasm_raw(wasm, **parse_harness_inputs(source)) assert "error" not in reply, f"harness failed to run {name}: {reply}" return reply From 829607edc9d20d5dc76a4ef83d1ac05cfa01d376 Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Thu, 13 Aug 2026 15:22:21 -0400 Subject: [PATCH 07/10] Iterate on test helpers --- src/fpy/test_helpers.py | 79 +++++++++---------- test/fpy/golden/cmd_fail.fpy | 2 +- test/fpy/golden/cmd_response_busy.fpy | 4 +- test/fpy/test_golden.py | 21 +++-- test/fpy/test_wasm.py | 33 ++++---- test/harness/FpySequencerTester.cpp | 6 +- test/harness/Harness.cpp | 10 +-- test/harness/Harness.hpp | 6 +- .../wasm/tester/WasmSequencerTester.cpp | 6 +- 9 files changed, 80 insertions(+), 87 deletions(-) diff --git a/src/fpy/test_helpers.py b/src/fpy/test_helpers.py index 65357e8..353313d 100644 --- a/src/fpy/test_helpers.py +++ b/src/fpy/test_helpers.py @@ -197,12 +197,16 @@ def _serialize_args(args: list[FpyValue] | None) -> bytes | None: # --------------------------------------------------------------------------- -def _always_failing_opcodes(failing_opcodes) -> set[int]: - """The opcodes the harness completes with EXECUTION_ERROR: the RUN - commands that always fail when called from within a running sequence on - the same sequencer instance, plus the caller's *failing_opcodes*.""" +def _base_cmd_responses(cmd_responses) -> dict[int, int]: + """The per-opcode command responses: the RUN command always completes + with EXECUTION_ERROR (it cannot run nested on the same sequencer + instance), plus the caller's *cmd_responses*.""" d = load_dictionary(default_dictionary) - return {d["cmd_name_dict"]["Ref.cmdSeq0.RUN"].opcode} | set(failing_opcodes or ()) + responses = { + d["cmd_name_dict"]["Ref.cmdSeq0.RUN"].opcode: CMD_RESPONSE_EXECUTION_ERROR + } + responses.update(cmd_responses or {}) + return responses def _run_request( @@ -213,15 +217,15 @@ def _run_request( time_base: int = 0, time_context: int = 0, initial_time_us: int = 0, - failing_opcodes: set[int] = None, args: bytes = None, - cmd_response: int = None, + cmd_responses: dict[int, int] = None, ) -> dict: """The run request fields common to both sequencer harnesses. *tlm* and *prms* map channel/parameter names to the serialized values the harness - answers reads with; every command completes with *cmd_response* (default - OK) unless its opcode is in *failing_opcodes*.""" + answers reads with; every command completes OK unless *cmd_responses* + maps its opcode to another Fw.CmdResponse value.""" d = load_dictionary(default_dictionary) + responses = _base_cmd_responses(cmd_responses) request = { "seqFile": seq_file, "cwd": seq_dir, @@ -239,12 +243,12 @@ def _run_request( str(d["prm_name_dict"][prm_name].prm_id): bytes(val).hex() for prm_name, val in (prms or {}).items() }, - "failOpcodes": sorted(_always_failing_opcodes(failing_opcodes)), + "cmdResponses": { + str(opcode): response for opcode, response in sorted(responses.items()) + }, } if args is not None: request["args"] = args.hex() - if cmd_response is not None: - request["cmdResponse"] = cmd_response return request @@ -291,18 +295,16 @@ def run_seq_raw( time_base: int = 0, time_context: int = 0, initial_time_us: int = 0, - failing_opcodes: set[int] = None, args: bytes = None, arg_types: list[tuple[str, FpyType]] = None, seq_run_opcodes: set[int] = None, ground_binary_dir: str = None, prms: dict[str, bytes] = None, - cmd_response: int = None, + cmd_responses: dict[int, int] = None, ) -> dict: """Run a list of directives on a real Svc::FpySequencer through the test - harness (test/harness) and return the harness's raw JSON reply. *tlm* and - *prms* map channel/parameter names to the serialized values the harness - answers reads with.""" + harness (test/harness) and return the harness's raw JSON reply. See + _run_request for the inputs.""" d = load_dictionary(default_dictionary) # When the test provides a ground_binary_dir, that directory doubles as @@ -322,9 +324,8 @@ def run_seq_raw( time_base=time_base, time_context=time_context, initial_time_us=initial_time_us, - failing_opcodes=failing_opcodes, args=args, - cmd_response=cmd_response, + cmd_responses=cmd_responses, ) if seq_run_opcodes: request["seqRunOpcodes"] = sorted(seq_run_opcodes) @@ -339,7 +340,7 @@ def run_seq( time_base: int = 0, time_context: int = 0, initial_time_us: int = 0, - failing_opcodes: set[int] = None, + cmd_responses: dict[int, int] = None, args: bytes = None, arg_types: list[tuple[str, FpyType]] = None, seq_run_opcodes: set[int] = None, @@ -363,7 +364,7 @@ def run_seq( time_base=time_base, time_context=time_context, initial_time_us=initial_time_us, - failing_opcodes=failing_opcodes, + cmd_responses=cmd_responses, args=args, arg_types=arg_types, seq_run_opcodes=seq_run_opcodes, @@ -423,9 +424,8 @@ def run_wasm_raw( time_base: int = 0, time_context: int = 0, initial_time_us: int = 0, - failing_opcodes: set[int] = None, args: bytes = None, - cmd_response: int = None, + cmd_responses: dict[int, int] = None, ) -> dict: """Run an already-linked wasm module on a real Svc::WasmSequencer through the wasm harness and return the harness's raw JSON reply. See _run_request @@ -439,28 +439,23 @@ def run_wasm_raw( time_base=time_base, time_context=time_context, initial_time_us=initial_time_us, - failing_opcodes=failing_opcodes, args=args, - cmd_response=cmd_response, + cmd_responses=cmd_responses, ) return wasm_harness().run(request) def run_wasm( wasm: bytes, - failing_opcodes: set[int] = None, - cmd_response: int = None, + cmd_responses: dict[int, int] = None, ) -> tuple[int, list[tuple[int, str]], list[bytes]]: """Run an already-linked wasm module on a real Svc::WasmSequencer through the wasm harness and return (error code, reported events, dispatched command buffers). - Every command completes with *cmd_response* (an Fw.CmdResponse value, - default OK) unless its opcode is in *failing_opcodes*, which makes it - complete with EXECUTION_ERROR.""" - result = run_wasm_raw( - wasm, failing_opcodes=failing_opcodes, cmd_response=cmd_response - ) + Every command completes OK unless *cmd_responses* maps its opcode to + another Fw.CmdResponse value.""" + result = run_wasm_raw(wasm, cmd_responses=cmd_responses) if "error" in result: raise HarnessError(result["error"]) @@ -485,13 +480,15 @@ def run_wasm( def _run_seq_wasm( - seq: str, failing_opcodes: set[int] = None, cmd_response: int = None, **kwargs + seq: str, + cmd_responses: dict[int, int] = None, + **kwargs, ) -> tuple[int, list[tuple[int, str]], list[bytes]]: """Compile *seq* to wasm and run it through the wasm harness. Returns (error code, reported events, dispatched command buffers). See _compile for the remaining keyword args.""" wasm = compile_seq_wasm(seq, **kwargs) - return run_wasm(wasm, failing_opcodes=failing_opcodes, cmd_response=cmd_response) + return run_wasm(wasm, cmd_responses=cmd_responses) def run_seq_wasm(seq: str, **kwargs) -> int: @@ -589,7 +586,7 @@ def assert_run_success( time_context: int = 0, initial_time_us: int = 0, timeout_s: int = 4, - failing_opcodes: set[int] = None, + cmd_responses: dict[int, int] = None, args: list[FpyValue] = None, ground_binary_dir: str = None, seq_run_opcodes: set[int] = None, @@ -622,7 +619,7 @@ def assert_run_success( timeout_s=timeout_s, ) return - code, _, cmds = run_wasm(wasm, failing_opcodes=failing_opcodes) + code, _, cmds = run_wasm(wasm, cmd_responses=cmd_responses) if code != DirectiveErrorCode.NO_ERROR.value: raise RuntimeError(f"wasm sequence returned error code {code}") return cmds @@ -645,7 +642,7 @@ def assert_run_success( time_base, time_context, initial_time_us, - failing_opcodes, + cmd_responses, args=args_bytes, arg_types=arg_types, seq_run_opcodes=seq_run_opcodes, @@ -660,7 +657,7 @@ def assert_run_failure( error_code: DirectiveErrorCode | int = None, validation_error: bool = False, initial_time_us: int = 0, - failing_opcodes: set[int] = None, + cmd_responses: dict[int, int] = None, args: list[FpyValue] = None, ground_binary_dir: str = None, seq_run_opcodes: set[int] = None, @@ -694,7 +691,7 @@ def assert_run_failure( # The wasm backend has no separate validation step or VM-internal # faults: a failed sequence is one that reports a nonzero code # through the exit/fault host imports. - code, _, _ = run_wasm(wasm, failing_opcodes=failing_opcodes) + code, _, _ = run_wasm(wasm, cmd_responses=cmd_responses) if code == DirectiveErrorCode.NO_ERROR.value: raise RuntimeError("wasm sequence succeeded") if error_code is not None and code != _as_int(error_code): @@ -717,7 +714,7 @@ def assert_run_failure( run_seq( directives, initial_time_us=initial_time_us, - failing_opcodes=failing_opcodes, + cmd_responses=cmd_responses, args=args_bytes, arg_types=arg_types, seq_run_opcodes=seq_run_opcodes, diff --git a/test/fpy/golden/cmd_fail.fpy b/test/fpy/golden/cmd_fail.fpy index 5698676..c10fb14 100644 --- a/test/fpy/golden/cmd_fail.fpy +++ b/test/fpy/golden/cmd_fail.fpy @@ -1,3 +1,3 @@ # A bare command that completes with EXECUTION_ERROR ends the sequence -# harness: fail CdhCore.cmdDisp.CMD_NO_OP +# harness: cmd_response CdhCore.cmdDisp.CMD_NO_OP 4 CdhCore.cmdDisp.CMD_NO_OP() diff --git a/test/fpy/golden/cmd_response_busy.fpy b/test/fpy/golden/cmd_response_busy.fpy index 45b753e..4673f51 100644 --- a/test/fpy/golden/cmd_response_busy.fpy +++ b/test/fpy/golden/cmd_response_busy.fpy @@ -1,5 +1,5 @@ -# Every command completes BUSY; capturing the response takes responsibility +# The command completes BUSY; capturing the response takes responsibility # for it, so the sequence keeps running -# harness: cmd_response 5 +# harness: cmd_response CdhCore.cmdDisp.CMD_NO_OP 5 ret: Fw.CmdResponse = CdhCore.cmdDisp.CMD_NO_OP() assert ret == Fw.CmdResponse.BUSY diff --git a/test/fpy/test_golden.py b/test/fpy/test_golden.py index 0d6b99a..316e157 100644 --- a/test/fpy/test_golden.py +++ b/test/fpy/test_golden.py @@ -14,12 +14,11 @@ A sequence declares the inputs its harness runs need in "# harness:" comments, one directive per line: - # harness: tlm answer to a telemetry read - # harness: prm answer to a parameter read - # harness: arg appended to the sequence arguments - # harness: time the sequencer's start time - # harness: fail completes with EXECUTION_ERROR - # harness: cmd_response every command's Fw.CmdResponse + # harness: tlm answer to a telemetry read + # harness: prm answer to a parameter read + # harness: arg appended to the sequence arguments + # harness: time the sequencer's start time + # harness: cmd_response that command's Fw.CmdResponse Regenerate the artifacts with: uv run pytest test/fpy/test_golden.py --update-goldens @@ -61,7 +60,7 @@ def parse_harness_inputs(source: str) -> dict: comments (see the module docstring for the directives), as keyword arguments for run_seq_raw / run_wasm_raw.""" d = load_dictionary(DEFAULT_DICTIONARY) - inputs = {"tlm": {}, "prms": {}, "failing_opcodes": set()} + inputs = {"tlm": {}, "prms": {}, "cmd_responses": {}} args = b"" for match in _HARNESS_INPUT.finditer(source): directive, *operands = match.group(1).split() @@ -79,12 +78,10 @@ def parse_harness_inputs(source: str) -> dict: inputs["time_base"] = int(base) inputs["time_context"] = int(context) inputs["initial_time_us"] = int(microseconds) - elif directive == "fail": - (name,) = operands - inputs["failing_opcodes"].add(d["cmd_name_dict"][name].opcode) elif directive == "cmd_response": - (value,) = operands - inputs["cmd_response"] = int(value) + name, value = operands + opcode = d["cmd_name_dict"][name].opcode + inputs["cmd_responses"][opcode] = int(value) else: raise ValueError(f"unknown harness input directive: {match.group(1)!r}") if args: diff --git a/test/fpy/test_wasm.py b/test/fpy/test_wasm.py index 5e7f027..e071942 100644 --- a/test/fpy/test_wasm.py +++ b/test/fpy/test_wasm.py @@ -1046,9 +1046,12 @@ class TestWasmCommands: compile time; runtime arguments are byte-swapped and stored into their packed offsets before each dispatch.""" + def _opcode_int(self, name: str) -> int: + return load_dictionary(default_dictionary)["cmd_name_dict"][name].opcode + + # FIXME rename to opcode bytes def _opcode(self, name: str) -> bytes: - d = load_dictionary(default_dictionary) - return struct.pack(">I", d["cmd_name_dict"][name].opcode) + return struct.pack(">I", self._opcode_int(name)) def test_cmd_emits_fprime_cmd_import(self): # Document the host-call contract: the linked module imports @@ -1179,7 +1182,7 @@ def test_captured_response_carries_host_value(self): code, _ = run_seq_wasm_with_cmds( "ret: Fw.CmdResponse = CdhCore.cmdDisp.CMD_NO_OP()\n" "assert ret == Fw.CmdResponse.BUSY\n", - cmd_response=5, # BUSY + cmd_responses={self._opcode_int("CdhCore.cmdDisp.CMD_NO_OP"): 5}, # BUSY ) assert code == NO_ERROR @@ -1189,27 +1192,23 @@ def test_captured_failing_response_does_not_auto_exit(self): code, _ = run_seq_wasm_with_cmds( "ret: Fw.CmdResponse = CdhCore.cmdDisp.CMD_NO_OP()\n" "assert ret == Fw.CmdResponse.EXECUTION_ERROR\n", - cmd_response=4, # EXECUTION_ERROR + cmd_responses={ + self._opcode_int("CdhCore.cmdDisp.CMD_NO_OP"): 4 # EXECUTION_ERROR + }, ) assert code == NO_ERROR def test_bare_failing_command_exits_cmd_fail(self): code, cmds = run_seq_wasm_with_cmds( "CdhCore.cmdDisp.CMD_NO_OP()\nassert False\n", - cmd_response=4, # EXECUTION_ERROR + cmd_responses={ + self._opcode_int("CdhCore.cmdDisp.CMD_NO_OP"): 4 # EXECUTION_ERROR + }, ) assert code == DirectiveErrorCode.CMD_FAIL.value # The command was dispatched; the sequence ended on its response. assert cmds == [self._opcode("CdhCore.cmdDisp.CMD_NO_OP")] - def test_bare_failing_command_via_fail_opcodes(self): - d = load_dictionary(default_dictionary) - code, _ = run_seq_wasm_with_cmds( - "CdhCore.cmdDisp.CMD_NO_OP()\n", - failing_opcodes={d["cmd_name_dict"]["CdhCore.cmdDisp.CMD_NO_OP"].opcode}, - ) - assert code == DirectiveErrorCode.CMD_FAIL.value - def test_assert_cmd_success_flag_disables_check(self): # With the flag cleared, failing bare commands don't end the sequence; # both commands are still dispatched. @@ -1217,7 +1216,9 @@ def test_assert_cmd_success_flag_disables_check(self): "flags.assert_cmd_success = False\n" "CdhCore.cmdDisp.CMD_NO_OP()\n" "CdhCore.cmdDisp.CMD_NO_OP()\n", - cmd_response=4, # EXECUTION_ERROR + cmd_responses={ + self._opcode_int("CdhCore.cmdDisp.CMD_NO_OP"): 4 # EXECUTION_ERROR + }, ) assert code == NO_ERROR assert cmds == [self._opcode("CdhCore.cmdDisp.CMD_NO_OP")] * 2 @@ -1229,7 +1230,9 @@ def test_bare_command_inside_if_block(self): "if x == 1:\n" " CdhCore.cmdDisp.CMD_NO_OP()\n" "assert False\n", - cmd_response=4, # EXECUTION_ERROR + cmd_responses={ + self._opcode_int("CdhCore.cmdDisp.CMD_NO_OP"): 4 # EXECUTION_ERROR + }, ) assert code == DirectiveErrorCode.CMD_FAIL.value diff --git a/test/harness/FpySequencerTester.cpp b/test/harness/FpySequencerTester.cpp index 14dda6a..7a7ce95 100644 --- a/test/harness/FpySequencerTester.cpp +++ b/test/harness/FpySequencerTester.cpp @@ -171,13 +171,13 @@ void FpySequencerTester::comCmdIn_handler(FwIndexType portNum, Fw::ComBuffer& da FwSizeType cmdSize = packetSize - sizeof(FwPacketDescriptorType); this->m_result.cmds.emplace_back(cmd, cmd + cmdSize); - Fw::CmdResponse response(static_cast(request.cmdResponse)); + Fw::CmdResponse response(Fw::CmdResponse::OK); if (request.seqRunOpcodes.count(opcode) > 0) { const U8* args = cmd + sizeof(FwOpcodeType); FwSizeType argsSize = cmdSize - sizeof(FwOpcodeType); response = this->runChildSequence(args, argsSize); - } else if (request.failOpcodes.count(opcode) > 0) { - response = Fw::CmdResponse::EXECUTION_ERROR; + } else if (request.cmdResponses.count(opcode) > 0) { + response = Fw::CmdResponse(static_cast(request.cmdResponses.at(opcode))); } // Answer right away, echoing the context back as the command sequence diff --git a/test/harness/Harness.cpp b/test/harness/Harness.cpp index a5fb701..9a31384 100644 --- a/test/harness/Harness.cpp +++ b/test/harness/Harness.cpp @@ -60,9 +60,10 @@ HarnessRequest parseRequest(const JsonValue& json) { request.seconds = static_cast(require(*time, "seconds").intValue); request.useconds = static_cast(require(*time, "useconds").intValue); } - if (const JsonValue* opcodes = json.get("failOpcodes")) { - for (const JsonValue& opcode : opcodes->items) { - request.failOpcodes.insert(static_cast(opcode.intValue)); + if (const JsonValue* responses = json.get("cmdResponses")) { + for (const auto& member : responses->members) { + request.cmdResponses[static_cast(std::stoull(member.first))] = + static_cast(member.second.intValue); } } if (const JsonValue* opcodes = json.get("seqRunOpcodes")) { @@ -73,9 +74,6 @@ HarnessRequest parseRequest(const JsonValue& json) { if (const JsonValue* size = json.get("seqArgsBufferSize")) { request.seqArgsBufferSize = static_cast(size->intValue); } - if (const JsonValue* response = json.get("cmdResponse")) { - request.cmdResponse = static_cast(response->intValue); - } return request; } diff --git a/test/harness/Harness.hpp b/test/harness/Harness.hpp index 48ee5e3..f457cf0 100644 --- a/test/harness/Harness.hpp +++ b/test/harness/Harness.hpp @@ -38,8 +38,8 @@ struct HarnessRequest { U8 timeContext = 0; U32 seconds = 0; U32 useconds = 0; - // Commands that complete with EXECUTION_ERROR. - std::set failOpcodes; + // Per-command response overrides (opcode -> Fw.CmdResponse value). + std::map cmdResponses; // Commands that mean "run another sequence". Their arguments are parsed // as (fileName, blockState, seqArgs) and the child sequence is run for // real on a nested tester; its outcome becomes the command response. @@ -48,8 +48,6 @@ struct HarnessRequest { // of a seq-run command. (The dictionary's buffer length can differ from // the flight build's Svc::SeqArgs, so the flight type cannot be used.) U32 seqArgsBufferSize = 0; - // Response for all other commands (an Fw.CmdResponse value, default OK). - U8 cmdResponse = 0; }; struct HarnessEvent { diff --git a/test/harness/wasm/tester/WasmSequencerTester.cpp b/test/harness/wasm/tester/WasmSequencerTester.cpp index 4b71306..c18ec76 100644 --- a/test/harness/wasm/tester/WasmSequencerTester.cpp +++ b/test/harness/wasm/tester/WasmSequencerTester.cpp @@ -155,9 +155,9 @@ void WasmSequencerTester::comCmdIn_handler(FwIndexType portNum, Fw::ComBuffer& d FwSizeType cmdSize = packetSize - sizeof(FwPacketDescriptorType); this->m_result.cmds.emplace_back(cmd, cmd + cmdSize); - Fw::CmdResponse response(static_cast(request.cmdResponse)); - if (request.failOpcodes.count(opcode) > 0) { - response = Fw::CmdResponse::EXECUTION_ERROR; + Fw::CmdResponse response(Fw::CmdResponse::OK); + if (request.cmdResponses.count(opcode) > 0) { + response = Fw::CmdResponse(static_cast(request.cmdResponses.at(opcode))); } // Answer right away, echoing the context back as the command sequence From a1892b7632943226931c503c61a38848c1edfad1 Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Thu, 13 Aug 2026 15:32:19 -0400 Subject: [PATCH 08/10] Iterate on test helpers --- README.md | 1 - src/fpy/harness.py | 38 ++++--- src/fpy/test_helpers.py | 99 +++++-------------- test/conftest.py | 2 +- test/fpy/golden/arith_ops.json | 6 +- test/fpy/golden/array_index.json | 6 +- test/fpy/golden/assert_fail.json | 6 +- test/fpy/golden/bool_logic.json | 6 +- test/fpy/golden/break_continue.json | 6 +- test/fpy/golden/casts.json | 6 +- test/fpy/golden/cmd_args.json | 6 +- test/fpy/golden/cmd_fail.json | 6 +- test/fpy/golden/cmd_handled.json | 6 +- test/fpy/golden/cmd_response_busy.json | 6 +- test/fpy/golden/cmd_unhandled.json | 6 +- test/fpy/golden/const_fold.json | 6 +- test/fpy/golden/empty.json | 6 +- test/fpy/golden/enum_cmp.json | 6 +- test/fpy/golden/exit_error.json | 6 +- test/fpy/golden/exit_success.json | 6 +- test/fpy/golden/for_range.json | 6 +- test/fpy/golden/func_recursive.json | 6 +- test/fpy/golden/func_unused.json | 6 +- test/fpy/golden/func_used.json | 6 +- test/fpy/golden/if_else.json | 6 +- test/fpy/golden/if_simple.json | 6 +- test/fpy/golden/log_event.json | 6 +- test/fpy/golden/struct_member.json | 6 +- test/fpy/golden/unary_ops.json | 6 +- test/fpy/golden/var_bool.json | 6 +- test/fpy/golden/var_f32.json | 6 +- test/fpy/golden/var_reassign.json | 6 +- test/fpy/golden/var_u32.json | 6 +- test/fpy/golden/while_simple.json | 6 +- test/fpy/test_commands.py | 26 ++++- test/fpy/test_control_flow.py | 2 +- test/fpy/test_integration.py | 20 +++- test/fpy/test_telemetry.py | 4 +- test/fpy/test_wasm.py | 2 + test/harness/FpySequencerTester.cpp | 1 + test/harness/Harness.cpp | 10 +- test/harness/Harness.hpp | 9 +- .../wasm/tester/WasmSequencerTester.cpp | 1 - 43 files changed, 159 insertions(+), 236 deletions(-) diff --git a/README.md b/README.md index 704ea15..8a4ff25 100644 --- a/README.md +++ b/README.md @@ -759,7 +759,6 @@ The wasm harness is built automatically at the start of the test session, with a Tests marked with `@pytest.mark.wasm` are end-to-end LLVM/wasm tests and always run on the wasm backend (with the same requirements as above), even when `--wasm` is not passed. -# FIXME I'd like to remove the use-gds feature ### `--use-gds` By default, tests run against a local `Svc::FpySequencer` through the harness. Passing `--use-gds` runs sequences against a live F Prime GDS deployment instead; see [Running on a test F Prime deployment](#running-on-a-test-f-prime-deployment) for how to set one up and the full command line (a `--dictionary` argument is also required). diff --git a/src/fpy/harness.py b/src/fpy/harness.py index de7e782..4832a87 100644 --- a/src/fpy/harness.py +++ b/src/fpy/harness.py @@ -12,7 +12,7 @@ from pathlib import Path REPO_ROOT = Path(__file__).parent.parent.parent -FPY_HARNESS_BINARY = ( +FPYBC_HARNESS_BINARY = ( REPO_ROOT / "build-artifacts" / "Linux" / "FpyHarness" / "bin" / "FpyHarness" ) _WASM_HARNESS_ROOT = REPO_ROOT / "test" / "harness" / "wasm" @@ -31,8 +31,7 @@ class HarnessError(Exception): a malformed reply, or crashed. Distinct from a sequence failing.""" -# FIXME this should be called build fpybc harness -def build_harness() -> None: +def build_fpybc_harness() -> None: """Builds the FpySequencer harness executable from the fprime submodule. The build is incremental, so this is cheap when nothing changed.""" if not (REPO_ROOT / "test" / "fprime" / "CMakeLists.txt").exists(): @@ -135,7 +134,7 @@ def run(self, request: dict) -> dict: def _start(self) -> None: if not self._binary.exists(): raise HarnessError( - f"harness binary {self._binary} does not exist; build it with build_harness()" + f"harness binary {self._binary} does not exist; build it first" ) self._stderr_file = tempfile.TemporaryFile(mode="w+") self._process = subprocess.Popen( @@ -161,28 +160,27 @@ def close(self) -> None: self._stderr_file = None -_fpy_harness: SequencerHarness | None = None +_fpybc_harness: SequencerHarness | None = None _wasm_harness: SequencerHarness | None = None # The first failed build, re-raised on later calls: retrying the build once # it has failed only repeats the same slow failure. -_fpy_build_error: HarnessError | None = None +_fpybc_build_error: HarnessError | None = None -# FIXME: should be fpybc -def fpy_harness() -> SequencerHarness: +def fpybc_harness() -> SequencerHarness: """The shared harness for the fpy bytecode backend, building its binary on first use.""" - global _fpy_harness, _fpy_build_error - if _fpy_build_error is not None: - raise _fpy_build_error - if _fpy_harness is None: + global _fpybc_harness, _fpybc_build_error + if _fpybc_build_error is not None: + raise _fpybc_build_error + if _fpybc_harness is None: try: - build_harness() + build_fpybc_harness() except HarnessError as e: - _fpy_build_error = e + _fpybc_build_error = e raise - _fpy_harness = SequencerHarness(FPY_HARNESS_BINARY) - return _fpy_harness + _fpybc_harness = SequencerHarness(FPYBC_HARNESS_BINARY) + return _fpybc_harness def wasm_harness() -> SequencerHarness: @@ -195,10 +193,10 @@ def wasm_harness() -> SequencerHarness: def close_all() -> None: """Stops any running harness processes (end of the test session).""" - global _fpy_harness, _wasm_harness - if _fpy_harness is not None: - _fpy_harness.close() - _fpy_harness = None + global _fpybc_harness, _wasm_harness + if _fpybc_harness is not None: + _fpybc_harness.close() + _fpybc_harness = None if _wasm_harness is not None: _wasm_harness.close() _wasm_harness = None diff --git a/src/fpy/test_helpers.py b/src/fpy/test_helpers.py index 353313d..928d98c 100644 --- a/src/fpy/test_helpers.py +++ b/src/fpy/test_helpers.py @@ -26,7 +26,7 @@ ) from fpy.dictionary import load_dictionary from fpy.error import WarningType -from fpy.harness import HarnessError, fpy_harness, wasm_harness +from fpy.harness import HarnessError, fpybc_harness, wasm_harness from fpy.state import CompileState, get_base_compile_state from fpy.types import CmdDef, FpyType, FpyValue, TypeKind @@ -197,18 +197,6 @@ def _serialize_args(args: list[FpyValue] | None) -> bytes | None: # --------------------------------------------------------------------------- -def _base_cmd_responses(cmd_responses) -> dict[int, int]: - """The per-opcode command responses: the RUN command always completes - with EXECUTION_ERROR (it cannot run nested on the same sequencer - instance), plus the caller's *cmd_responses*.""" - d = load_dictionary(default_dictionary) - responses = { - d["cmd_name_dict"]["Ref.cmdSeq0.RUN"].opcode: CMD_RESPONSE_EXECUTION_ERROR - } - responses.update(cmd_responses or {}) - return responses - - def _run_request( seq_file: str, seq_dir: str, @@ -225,7 +213,7 @@ def _run_request( answers reads with; every command completes OK unless *cmd_responses* maps its opcode to another Fw.CmdResponse value.""" d = load_dictionary(default_dictionary) - responses = _base_cmd_responses(cmd_responses) + responses = cmd_responses or {} request = { "seqFile": seq_file, "cwd": seq_dir, @@ -331,46 +319,21 @@ def run_seq_raw( request["seqRunOpcodes"] = sorted(seq_run_opcodes) request["seqArgsBufferSize"] = _seq_args_buffer_len(d) - return fpy_harness().run(request) + return fpybc_harness().run(request) -def run_seq( - directives: list[Directive], - tlm: dict[str, bytes] = None, - time_base: int = 0, - time_context: int = 0, - initial_time_us: int = 0, - cmd_responses: dict[int, int] = None, - args: bytes = None, - arg_types: list[tuple[str, FpyType]] = None, - seq_run_opcodes: set[int] = None, - ground_binary_dir: str = None, - prms: dict[str, bytes] = None, -) -> list[bytes]: +def run_seq(directives: list[Directive], **run_kwargs) -> list[bytes]: """Run a list of directives on a real Svc::FpySequencer through the test - harness (test/harness). *tlm* and *prms* map channel/parameter names to - the serialized values the harness answers reads with. Returns the command - buffers the sequence dispatched (the big-endian serialized FwOpcodeType + - arguments), in call order. + harness (test/harness). Returns the command buffers the sequence + dispatched (the big-endian serialized FwOpcodeType + arguments), in call + order. See run_seq_raw for the keyword args. Raises ValidationError when the sequencer rejects the sequence before running it, and RuntimeError when the sequence fails: with the DirectiveErrorCode for a trap, or the raw error code int for a nonzero exit. """ - result = run_seq_raw( - directives, - tlm=tlm, - time_base=time_base, - time_context=time_context, - initial_time_us=initial_time_us, - cmd_responses=cmd_responses, - args=args, - arg_types=arg_types, - seq_run_opcodes=seq_run_opcodes, - ground_binary_dir=ground_binary_dir, - prms=prms, - ) + result = run_seq_raw(directives, **run_kwargs) if "error" in result: raise HarnessError(result["error"]) @@ -393,7 +356,7 @@ def run_seq( ) # A finished run must leave exactly the stack bytes the compiler # expected; a leak of even one byte is a failure. - expected_stack = _expected_stack_bytes(directives, args) + expected_stack = _expected_stack_bytes(directives, run_kwargs.get("args")) actual_stack = len(bytes.fromhex(result["stack"])) if actual_stack != expected_stack: raise RuntimeError(f"Sequence leaked {actual_stack - expected_stack} bytes") @@ -446,16 +409,12 @@ def run_wasm_raw( def run_wasm( - wasm: bytes, - cmd_responses: dict[int, int] = None, + wasm: bytes, **run_kwargs ) -> tuple[int, list[tuple[int, str]], list[bytes]]: """Run an already-linked wasm module on a real Svc::WasmSequencer through the wasm harness and return (error code, reported events, dispatched - command buffers). - - Every command completes OK unless *cmd_responses* maps its opcode to - another Fw.CmdResponse value.""" - result = run_wasm_raw(wasm, cmd_responses=cmd_responses) + command buffers). See run_wasm_raw for the keyword args.""" + result = run_wasm_raw(wasm, **run_kwargs) if "error" in result: raise HarnessError(result["error"]) @@ -581,23 +540,18 @@ def assert_compile_failure(fprime_test_api, seq: str, match: str = None, **kwarg def assert_run_success( fprime_test_api, seq: str, - tlm: dict[str, bytes] = None, - time_base: int = 0, - time_context: int = 0, - initial_time_us: int = 0, timeout_s: int = 4, - cmd_responses: dict[int, int] = None, args: list[FpyValue] = None, ground_binary_dir: str = None, - seq_run_opcodes: set[int] = None, import_directories: list[str] | None = None, expected_warnings=None, main_file_dir: str | None = None, - prms: dict[str, bytes] = None, + **run_kwargs, ) -> list[bytes] | None: """Compile *seq* on the current backend, run it, and assert it succeeds. Returns the command buffers the sequence dispatched, or None when running - against a live GDS deployment. + against a live GDS deployment. The remaining keyword args (tlm, prms, + time, cmd_responses, ...) are the current backend's run_*_raw inputs. Runs on the test harness by default, or against a live GDS deployment when fprime_test_api is not None (--use-gds).""" @@ -619,7 +573,7 @@ def assert_run_success( timeout_s=timeout_s, ) return - code, _, cmds = run_wasm(wasm, cmd_responses=cmd_responses) + code, _, cmds = run_wasm(wasm, **run_kwargs) if code != DirectiveErrorCode.NO_ERROR.value: raise RuntimeError(f"wasm sequence returned error code {code}") return cmds @@ -638,16 +592,10 @@ def assert_run_success( return None return run_seq( directives, - tlm, - time_base, - time_context, - initial_time_us, - cmd_responses, args=args_bytes, arg_types=arg_types, - seq_run_opcodes=seq_run_opcodes, ground_binary_dir=ground_binary_dir, - prms=prms, + **run_kwargs, ) @@ -656,17 +604,16 @@ def assert_run_failure( seq: str, error_code: DirectiveErrorCode | int = None, validation_error: bool = False, - initial_time_us: int = 0, - cmd_responses: dict[int, int] = None, args: list[FpyValue] = None, ground_binary_dir: str = None, - seq_run_opcodes: set[int] = None, import_directories: list[str] | None = None, + **run_kwargs, ): """Compile *seq* on the current backend, run it, and assert it fails: with *error_code* (a DirectiveErrorCode trap or a raw exit code int), or with *validation_error* when the sequencer must reject the sequence - before running it.""" + before running it. The remaining keyword args (tlm, prms, time, + cmd_responses, ...) are the current backend's run_*_raw inputs.""" assert not ( error_code is not None and validation_error ), "Cannot specify both error_code and validation_error" @@ -691,7 +638,7 @@ def assert_run_failure( # The wasm backend has no separate validation step or VM-internal # faults: a failed sequence is one that reports a nonzero code # through the exit/fault host imports. - code, _, _ = run_wasm(wasm, cmd_responses=cmd_responses) + code, _, _ = run_wasm(wasm, **run_kwargs) if code == DirectiveErrorCode.NO_ERROR.value: raise RuntimeError("wasm sequence succeeded") if error_code is not None and code != _as_int(error_code): @@ -713,12 +660,10 @@ def assert_run_failure( try: run_seq( directives, - initial_time_us=initial_time_us, - cmd_responses=cmd_responses, args=args_bytes, arg_types=arg_types, - seq_run_opcodes=seq_run_opcodes, ground_binary_dir=ground_binary_dir, + **run_kwargs, ) except ValidationError as e: if not validation_error: diff --git a/test/conftest.py b/test/conftest.py index 97c0a58..df9b52b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -57,7 +57,7 @@ def pytest_configure(config): _build_wasm_harness_once() # The FpySequencer harness builds itself lazily, on the first test that - # runs a sequence through it (fpy.harness.fpy_harness), so runs that + # runs a sequence through it (fpy.harness.fpybc_harness), so runs that # never touch it -- compiler unit tests, --collect-only -- skip the # build entirely. diff --git a/test/fpy/golden/arith_ops.json b/test/fpy/golden/arith_ops.json index 071c876..1d776da 100644 --- a/test/fpy/golden/arith_ops.json +++ b/test/fpy/golden/arith_ops.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00000000000000110000000000000005ffffffffffffffef401c000000000000", "state": 2, "statementsDispatched": 73 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/array_index.json b/test/fpy/golden/array_index.json index 49e5860..b0f7efe 100644 --- a/test/fpy/golden/array_index.json +++ b/test/fpy/golden/array_index.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff000000070000000901", "state": 2, "statementsDispatched": 95 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/assert_fail.json b/test/fpy/golden/assert_fail.json index 27a020e..2d4482a 100644 --- a/test/fpy/golden/assert_fail.json +++ b/test/fpy/golden/assert_fail.json @@ -3,7 +3,6 @@ "cmdResponse": 4, "cmds": [], "exitCode": 55, - "frameStart": 0, "reachedRunning": true, "sequencesSucceeded": 0, "serial": [] @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceExitedWithError : Sequence s0.bin exited with error code 55" } ], + "frameStart": 0, "lastDirectiveError": 7, "stack": "ff", "state": 2, @@ -40,8 +40,6 @@ } ], "lastDirectiveError": 0, - "stack": "", - "state": 1, - "statementsDispatched": 0 + "state": 1 } } diff --git a/test/fpy/golden/bool_logic.json b/test/fpy/golden/bool_logic.json index 6a260d7..3cc49ce 100644 --- a/test/fpy/golden/bool_logic.json +++ b/test/fpy/golden/bool_logic.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff000000000000000500", "state": 2, "statementsDispatched": 27 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/break_continue.json b/test/fpy/golden/break_continue.json index df00f9c..da2aaeb 100644 --- a/test/fpy/golden/break_continue.json +++ b/test/fpy/golden/break_continue.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff000000000000000a0000000000000004", "state": 2, "statementsDispatched": 191 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/casts.json b/test/fpy/golden/casts.json index 8a88720..4d2da5e 100644 --- a/test/fpy/golden/casts.json +++ b/test/fpy/golden/casts.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff401799999999999a0000012c", "state": 2, "statementsDispatched": 29 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/cmd_args.json b/test/fpy/golden/cmd_args.json index 77d6329..3de261e 100644 --- a/test/fpy/golden/cmd_args.json +++ b/test/fpy/golden/cmd_args.json @@ -5,7 +5,6 @@ "01000002fffffffe3fc0000008", "01000001000568656c6c6f" ], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -19,6 +18,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "fffffffffe3fc0000008", "state": 2, "statementsDispatched": 22 @@ -31,8 +31,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 2 + "state": 3 } } diff --git a/test/fpy/golden/cmd_fail.json b/test/fpy/golden/cmd_fail.json index e96f612..415085d 100644 --- a/test/fpy/golden/cmd_fail.json +++ b/test/fpy/golden/cmd_fail.json @@ -5,7 +5,6 @@ "01000000" ], "exitCode": 17, - "frameStart": 0, "reachedRunning": true, "sequencesSucceeded": 0, "serial": [] @@ -18,6 +17,7 @@ "text": "(FpySequencer) SequenceExitedWithError : Sequence s0.bin exited with error code 17" } ], + "frameStart": 0, "lastDirectiveError": 7, "stack": "ff", "state": 2, @@ -42,8 +42,6 @@ } ], "lastDirectiveError": 0, - "stack": "", - "state": 1, - "statementsDispatched": 1 + "state": 1 } } diff --git a/test/fpy/golden/cmd_handled.json b/test/fpy/golden/cmd_handled.json index 9ea7cd6..dcb77ad 100644 --- a/test/fpy/golden/cmd_handled.json +++ b/test/fpy/golden/cmd_handled.json @@ -4,7 +4,6 @@ "cmds": [ "01000000" ], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -18,6 +17,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00", "state": 2, "statementsDispatched": 10 @@ -30,8 +30,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 1 + "state": 3 } } diff --git a/test/fpy/golden/cmd_response_busy.json b/test/fpy/golden/cmd_response_busy.json index 11e9047..465bc46 100644 --- a/test/fpy/golden/cmd_response_busy.json +++ b/test/fpy/golden/cmd_response_busy.json @@ -4,7 +4,6 @@ "cmds": [ "01000000" ], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -18,6 +17,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff05", "state": 2, "statementsDispatched": 9 @@ -30,8 +30,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 1 + "state": 3 } } diff --git a/test/fpy/golden/cmd_unhandled.json b/test/fpy/golden/cmd_unhandled.json index 674cc28..e770888 100644 --- a/test/fpy/golden/cmd_unhandled.json +++ b/test/fpy/golden/cmd_unhandled.json @@ -4,7 +4,6 @@ "cmds": [ "01000000" ], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -18,6 +17,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff", "state": 2, "statementsDispatched": 6 @@ -30,8 +30,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 1 + "state": 3 } } diff --git a/test/fpy/golden/const_fold.json b/test/fpy/golden/const_fold.json index a77b132..0c29131 100644 --- a/test/fpy/golden/const_fold.json +++ b/test/fpy/golden/const_fold.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00000003", "state": 2, "statementsDispatched": 4 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/empty.json b/test/fpy/golden/empty.json index 5866272..8ca7182 100644 --- a/test/fpy/golden/empty.json +++ b/test/fpy/golden/empty.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff", "state": 2, "statementsDispatched": 1 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/enum_cmp.json b/test/fpy/golden/enum_cmp.json index cbb0dd4..8fe3fb0 100644 --- a/test/fpy/golden/enum_cmp.json +++ b/test/fpy/golden/enum_cmp.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00000002", "state": 2, "statementsDispatched": 15 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/exit_error.json b/test/fpy/golden/exit_error.json index 225611e..bcb3349 100644 --- a/test/fpy/golden/exit_error.json +++ b/test/fpy/golden/exit_error.json @@ -3,7 +3,6 @@ "cmdResponse": 4, "cmds": [], "exitCode": 3, - "frameStart": 0, "reachedRunning": true, "sequencesSucceeded": 0, "serial": [] @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceExitedWithError : Sequence s0.bin exited with error code 3" } ], + "frameStart": 0, "lastDirectiveError": 7, "stack": "ff", "state": 2, @@ -40,8 +40,6 @@ } ], "lastDirectiveError": 0, - "stack": "", - "state": 1, - "statementsDispatched": 0 + "state": 1 } } diff --git a/test/fpy/golden/exit_success.json b/test/fpy/golden/exit_success.json index 70d5ed1..b60bf9e 100644 --- a/test/fpy/golden/exit_success.json +++ b/test/fpy/golden/exit_success.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff", "state": 2, "statementsDispatched": 3 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/for_range.json b/test/fpy/golden/for_range.json index ecbaf86..7e43526 100644 --- a/test/fpy/golden/for_range.json +++ b/test/fpy/golden/for_range.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff000000000000000a00000000000000050000000000000005", "state": 2, "statementsDispatched": 82 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/func_recursive.json b/test/fpy/golden/func_recursive.json index 70c342a..0c36e61 100644 --- a/test/fpy/golden/func_recursive.json +++ b/test/fpy/golden/func_recursive.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff", "state": 2, "statementsDispatched": 1951 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/func_unused.json b/test/fpy/golden/func_unused.json index 3bf47cf..cd3072a 100644 --- a/test/fpy/golden/func_unused.json +++ b/test/fpy/golden/func_unused.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00000005", "state": 2, "statementsDispatched": 4 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/func_used.json b/test/fpy/golden/func_used.json index f77cca6..77ddf2e 100644 --- a/test/fpy/golden/func_used.json +++ b/test/fpy/golden/func_used.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00000006", "state": 2, "statementsDispatched": 13 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/if_else.json b/test/fpy/golden/if_else.json index 256fc8b..5ff5f9e 100644 --- a/test/fpy/golden/if_else.json +++ b/test/fpy/golden/if_else.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00000002", "state": 2, "statementsDispatched": 12 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/if_simple.json b/test/fpy/golden/if_simple.json index 256fc8b..5ff5f9e 100644 --- a/test/fpy/golden/if_simple.json +++ b/test/fpy/golden/if_simple.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00000002", "state": 2, "statementsDispatched": 12 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/log_event.json b/test/fpy/golden/log_event.json index e9fd8e9..5a4659c 100644 --- a/test/fpy/golden/log_event.json +++ b/test/fpy/golden/log_event.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -26,6 +25,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff", "state": 2, "statementsDispatched": 9 @@ -50,8 +50,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/struct_member.json b/test/fpy/golden/struct_member.json index 89a7921..308706f 100644 --- a/test/fpy/golden/struct_member.json +++ b/test/fpy/golden/struct_member.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff4040000041180000", "state": 2, "statementsDispatched": 22 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/unary_ops.json b/test/fpy/golden/unary_ops.json index 69f5cfd..8a65f96 100644 --- a/test/fpy/golden/unary_ops.json +++ b/test/fpy/golden/unary_ops.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff000000000000000500", "state": 2, "statementsDispatched": 23 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/var_bool.json b/test/fpy/golden/var_bool.json index 4b0343b..8543c7d 100644 --- a/test/fpy/golden/var_bool.json +++ b/test/fpy/golden/var_bool.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ffff00", "state": 2, "statementsDispatched": 6 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/var_f32.json b/test/fpy/golden/var_f32.json index 4efa673..1b0cf28 100644 --- a/test/fpy/golden/var_f32.json +++ b/test/fpy/golden/var_f32.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff4048f5c3", "state": 2, "statementsDispatched": 4 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/var_reassign.json b/test/fpy/golden/var_reassign.json index 004b9db..0a92477 100644 --- a/test/fpy/golden/var_reassign.json +++ b/test/fpy/golden/var_reassign.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff00000003", "state": 2, "statementsDispatched": 8 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/var_u32.json b/test/fpy/golden/var_u32.json index c056f19..8b80726 100644 --- a/test/fpy/golden/var_u32.json +++ b/test/fpy/golden/var_u32.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff0000002a", "state": 2, "statementsDispatched": 4 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/golden/while_simple.json b/test/fpy/golden/while_simple.json index c3afa39..216bb27 100644 --- a/test/fpy/golden/while_simple.json +++ b/test/fpy/golden/while_simple.json @@ -2,7 +2,6 @@ "common": { "cmdResponse": 0, "cmds": [], - "frameStart": 0, "lastDirectiveError": 0, "reachedRunning": true, "sequencesSucceeded": 1, @@ -16,6 +15,7 @@ "text": "(FpySequencer) SequenceDone : Completed sequence file s0.bin" } ], + "frameStart": 0, "stack": "ff0000000a", "state": 2, "statementsDispatched": 129 @@ -28,8 +28,6 @@ "text": "(WasmSequencer) SequenceSucceeded : Wasm program completed successfully" } ], - "stack": "", - "state": 3, - "statementsDispatched": 0 + "state": 3 } } diff --git a/test/fpy/test_commands.py b/test/fpy/test_commands.py index 9724ddf..09b8bde 100644 --- a/test/fpy/test_commands.py +++ b/test/fpy/test_commands.py @@ -2,12 +2,23 @@ import fpy.test_helpers as test_helpers from fpy.bytecode.directives import DirectiveErrorCode +from fpy.dictionary import load_dictionary from fpy.test_helpers import ( + CMD_RESPONSE_EXECUTION_ERROR, assert_compile_failure, assert_run_failure, assert_run_success, + default_dictionary, ) +# The failing command these tests dispatch: the harness is told to complete +# Ref.cmdSeq0.RUN with EXECUTION_ERROR. +RUN_FAILS = { + load_dictionary(default_dictionary)["cmd_name_dict"][ + "Ref.cmdSeq0.RUN" + ].opcode: CMD_RESPONSE_EXECUTION_ERROR +} + class TestCommandCalls: @@ -271,6 +282,7 @@ def test_unhandled_fail_flag_true_exits(self, fprime_test_api): fprime_test_api, seq, DirectiveErrorCode.CMD_FAIL, + cmd_responses=RUN_FAILS, ) def test_unhandled_fail_flag_false_continues(self, fprime_test_api): @@ -279,7 +291,7 @@ def test_unhandled_fail_flag_false_continues(self, fprime_test_api): flags.assert_cmd_success = False Ref.cmdSeq0.RUN("", Svc.BlockState.BLOCK) """ - assert_run_success(fprime_test_api, seq) + assert_run_success(fprime_test_api, seq, cmd_responses=RUN_FAILS) # -- Handled (captured) command + failing -- @@ -290,7 +302,7 @@ def test_handled_fail_flag_true_continues(self, fprime_test_api): resp: Fw.CmdResponse = Ref.cmdSeq0.RUN("test", Svc.BlockState.BLOCK) assert resp == Fw.CmdResponse.EXECUTION_ERROR """ - assert_run_success(fprime_test_api, seq) + assert_run_success(fprime_test_api, seq, cmd_responses=RUN_FAILS) def test_handled_fail_flag_false_continues(self, fprime_test_api): """Captured failing command with flag=False should not halt.""" @@ -299,7 +311,7 @@ def test_handled_fail_flag_false_continues(self, fprime_test_api): resp: Fw.CmdResponse = Ref.cmdSeq0.RUN("test", Svc.BlockState.BLOCK) assert resp == Fw.CmdResponse.EXECUTION_ERROR """ - assert_run_success(fprime_test_api, seq) + assert_run_success(fprime_test_api, seq, cmd_responses=RUN_FAILS) # -- Successful commands (should always pass regardless of flag/handling) -- @@ -328,7 +340,7 @@ def test_toggle_off_before_cmd(self, fprime_test_api): flags.assert_cmd_success = False Ref.cmdSeq0.RUN("", Svc.BlockState.BLOCK) """ - assert_run_success(fprime_test_api, seq) + assert_run_success(fprime_test_api, seq, cmd_responses=RUN_FAILS) # -- Bare commands in nested scopes (flag=True) -- @@ -343,6 +355,7 @@ def test_bare_cmd_in_if_block(self, fprime_test_api): fprime_test_api, seq, DirectiveErrorCode.CMD_FAIL, + cmd_responses=RUN_FAILS, ) def test_bare_cmd_in_while_block(self, fprime_test_api): @@ -358,6 +371,7 @@ def test_bare_cmd_in_while_block(self, fprime_test_api): fprime_test_api, seq, DirectiveErrorCode.CMD_FAIL, + cmd_responses=RUN_FAILS, ) def test_bare_cmd_in_function(self, fprime_test_api): @@ -372,6 +386,7 @@ def do_cmd(): fprime_test_api, seq, DirectiveErrorCode.CMD_FAIL, + cmd_responses=RUN_FAILS, ) def test_bare_cmd_in_function_no_fail(self, fprime_test_api): @@ -382,7 +397,7 @@ def do_cmd(): Ref.cmdSeq0.RUN("", Svc.BlockState.BLOCK) do_cmd() """ - assert_run_success(fprime_test_api, seq) + assert_run_success(fprime_test_api, seq, cmd_responses=RUN_FAILS) def test_bare_cmd_in_for_loop(self, fprime_test_api): """Auto-assert fires for bare commands inside for loops.""" @@ -395,4 +410,5 @@ def test_bare_cmd_in_for_loop(self, fprime_test_api): fprime_test_api, seq, DirectiveErrorCode.CMD_FAIL, + cmd_responses=RUN_FAILS, ) diff --git a/test/fpy/test_control_flow.py b/test/fpy/test_control_flow.py index ef7abc9..bb3fb82 100644 --- a/test/fpy/test_control_flow.py +++ b/test/fpy/test_control_flow.py @@ -58,7 +58,7 @@ def test_large_elifs(self, fprime_test_api): assert_run_success( fprime_test_api, seq, - {"CdhCore.cmdDisp.CommandsDispatched": FpyValue(U32, 4).serialize()}, + tlm={"CdhCore.cmdDisp.CommandsDispatched": FpyValue(U32, 4).serialize()}, ) def test_if_true(self, fprime_test_api): diff --git a/test/fpy/test_integration.py b/test/fpy/test_integration.py index a5444a9..e6b50bb 100644 --- a/test/fpy/test_integration.py +++ b/test/fpy/test_integration.py @@ -1,8 +1,14 @@ from fpy.bytecode.directives import DirectiveErrorCode +from fpy.dictionary import load_dictionary from fpy.types import FpyValue, U32 from fpy.error import WarningType -from fpy.test_helpers import assert_run_success, assert_run_failure +from fpy.test_helpers import ( + CMD_RESPONSE_EXECUTION_ERROR, + assert_run_success, + assert_run_failure, + default_dictionary, +) class TestReadmeExamples: @@ -210,12 +216,18 @@ def recurse(limit: U64): assert 1 > 0 exit(0) """ + # The example uses Ref.cmdSeq0.RUN as its failing command (it cannot + # run nested on the same sequencer), so the harness is told to fail it. + run_opcode = load_dictionary(default_dictionary)["cmd_name_dict"][ + "Ref.cmdSeq0.RUN" + ].opcode # The example intentionally re-declares `i` as a loop variable over an # existing global `i`, which shadows it (shadow-value). assert_run_success( fprime_test_api, seq, - {"CdhCore.cmdDisp.CommandsDispatched": FpyValue(U32, 45).serialize()}, + tlm={"CdhCore.cmdDisp.CommandsDispatched": FpyValue(U32, 45).serialize()}, + cmd_responses={run_opcode: CMD_RESPONSE_EXECUTION_ERROR}, timeout_s=20, expected_warnings={WarningType.SHADOW_VALUE}, ) @@ -226,8 +238,12 @@ def test_readme_bare_cmd_fail_exits(self, fprime_test_api): Ref.cmdSeq0.RUN("", Svc.BlockState.BLOCK) # sequence exits with an error """ + run_opcode = load_dictionary(default_dictionary)["cmd_name_dict"][ + "Ref.cmdSeq0.RUN" + ].opcode assert_run_failure( fprime_test_api, seq, DirectiveErrorCode.CMD_FAIL, + cmd_responses={run_opcode: CMD_RESPONSE_EXECUTION_ERROR}, ) diff --git a/test/fpy/test_telemetry.py b/test/fpy/test_telemetry.py index 87b06f3..30a627c 100644 --- a/test/fpy/test_telemetry.py +++ b/test/fpy/test_telemetry.py @@ -21,7 +21,7 @@ def test_geq_tlm(self, fprime_test_api): assert_run_success( fprime_test_api, seq, - {"CdhCore.cmdDisp.CommandsDispatched": FpyValue(U32, 1).serialize()}, + tlm={"CdhCore.cmdDisp.CommandsDispatched": FpyValue(U32, 1).serialize()}, ) def test_get_struct_member_of_tlm(self, fprime_test_api): @@ -35,7 +35,7 @@ def test_get_struct_member_of_tlm(self, fprime_test_api): assert_run_success( fprime_test_api, seq, - { + tlm={ "Ref.typeDemo.ChoicePairCh": FpyValue( lookup_type("Ref.ChoicePair"), {"firstChoice": "ONE", "secondChoice": "ONE"}, diff --git a/test/fpy/test_wasm.py b/test/fpy/test_wasm.py index e071942..f4e6a12 100644 --- a/test/fpy/test_wasm.py +++ b/test/fpy/test_wasm.py @@ -1050,6 +1050,8 @@ def _opcode_int(self, name: str) -> int: return load_dictionary(default_dictionary)["cmd_name_dict"][name].opcode # FIXME rename to opcode bytes + # FIXME please also add a pre commit check that searches for # FIXME or #FIXME in src or test (exclude in submodules, allow easy exclusions for later) and fails commit if there are any + # FIXME actually just searchfor teh FIXME string so it works in all langs def _opcode(self, name: str) -> bytes: return struct.pack(">I", self._opcode_int(name)) diff --git a/test/harness/FpySequencerTester.cpp b/test/harness/FpySequencerTester.cpp index 7a7ce95..591df61 100644 --- a/test/harness/FpySequencerTester.cpp +++ b/test/harness/FpySequencerTester.cpp @@ -68,6 +68,7 @@ harness::HarnessResult FpySequencerTester::run(const harness::HarnessRequest& re // response, so the Python side can cross-check the two. FpySequencer& seq = this->m_sequencer; this->m_result.state = static_cast(seq.sequencer_getState()); + this->m_result.hasVmState = true; this->m_result.statementsDispatched = seq.m_statementsDispatched; this->m_result.lastDirectiveError = static_cast(seq.m_tlm.lastDirectiveError); this->m_result.sequencesSucceeded = seq.m_tlm.sequencesSucceeded; diff --git a/test/harness/Harness.cpp b/test/harness/Harness.cpp index 9a31384..2a486ea 100644 --- a/test/harness/Harness.cpp +++ b/test/harness/Harness.cpp @@ -87,7 +87,9 @@ JsonValue resultToJson(const HarnessResult& result) { } json.set("state", JsonValue::makeInt(result.state)); json.set("reachedRunning", JsonValue::makeBool(result.reachedRunning)); - json.set("statementsDispatched", JsonValue::makeInt(static_cast(result.statementsDispatched))); + if (result.hasVmState) { + json.set("statementsDispatched", JsonValue::makeInt(static_cast(result.statementsDispatched))); + } json.set("lastDirectiveError", JsonValue::makeInt(result.lastDirectiveError)); if (result.exited) { json.set("exitCode", JsonValue::makeInt(result.exitCode)); @@ -121,8 +123,10 @@ JsonValue resultToJson(const HarnessResult& result) { } json.set("serial", serial); - json.set("stack", JsonValue::makeString(hexEncode(result.stack))); - json.set("frameStart", JsonValue::makeInt(result.frameStart)); + if (result.hasVmState) { + json.set("stack", JsonValue::makeString(hexEncode(result.stack))); + json.set("frameStart", JsonValue::makeInt(result.frameStart)); + } json.set("sequencesSucceeded", JsonValue::makeInt(static_cast(result.sequencesSucceeded))); return json; } diff --git a/test/harness/Harness.hpp b/test/harness/Harness.hpp index f457cf0..15fcc4f 100644 --- a/test/harness/Harness.hpp +++ b/test/harness/Harness.hpp @@ -77,7 +77,13 @@ struct HarnessResult { // (false means the sequence failed validation or loading). I32 state = 0; bool reachedRunning = false; + // The FpySequencer's VM state, absent for the wasm harness: statements + // dispatched, the bytes left on the stack after the run, and the frame + // start. + bool hasVmState = false; U64 statementsDispatched = 0; + std::vector stack; + U32 frameStart = 0; // The last directive error the sequencer recorded (its telemetry). I32 lastDirectiveError = 0; // Exit code, present only when the sequence exited with a nonzero code. @@ -89,9 +95,6 @@ struct HarnessResult { // Each command the sequence dispatched: serialized opcode + arguments. std::vector> cmds; std::vector serial; - // The bytes left on the sequencer's stack after the run. - std::vector stack; - U32 frameStart = 0; U64 sequencesSucceeded = 0; }; diff --git a/test/harness/wasm/tester/WasmSequencerTester.cpp b/test/harness/wasm/tester/WasmSequencerTester.cpp index c18ec76..681999c 100644 --- a/test/harness/wasm/tester/WasmSequencerTester.cpp +++ b/test/harness/wasm/tester/WasmSequencerTester.cpp @@ -58,7 +58,6 @@ harness::HarnessResult WasmSequencerTester::run(const harness::HarnessRequest& r WasmSequencer& seq = this->m_sequencer; this->m_result.state = static_cast(seq.sequencer_getState()); this->m_result.sequencesSucceeded = seq.m_tlmSequencesSucceeded; - this->m_result.statementsDispatched = seq.m_tlmCommandsDispatched; this->m_request = nullptr; return this->m_result; From e3b84fee308d7e7eb401340042324758b971c53d Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Thu, 13 Aug 2026 17:33:38 -0400 Subject: [PATCH 09/10] Iterate on test helpers --- .github/workflows/no-fixmes.yml | 25 +++++++++++++++ src/fpy/codegen_fpybc.py | 54 +++++++++++++++++---------------- src/fpy/test_helpers.py | 37 ++++++++++++---------- test/conftest.py | 4 +++ test/fpy/test_wasm.py | 31 +++++++++---------- 5 files changed, 92 insertions(+), 59 deletions(-) create mode 100644 .github/workflows/no-fixmes.yml diff --git a/.github/workflows/no-fixmes.yml b/.github/workflows/no-fixmes.yml new file mode 100644 index 0000000..0bd2a52 --- /dev/null +++ b/.github/workflows/no-fixmes.yml @@ -0,0 +1,25 @@ +name: no fixmes + +on: + pull_request: + push: + branches: [ main, devel ] +# Cancel in-progress runs if a newer run is started on a given PR +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ !contains(github.ref, 'devel') && !contains(github.ref, 'release/')}} + +jobs: + no-fixmes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Fail on FIXME markers in src or test + # The marker string fails the check whatever the language. git grep + # searches only tracked files and does not descend into submodules. + # Exempt a file by adding an ":(exclude)path" pathspec to the command. + run: | + if git grep -n "FIXME" -- src test; then + echo "::error::FIXME markers found (see log); resolve them before merging" + exit 1 + fi diff --git a/src/fpy/codegen_fpybc.py b/src/fpy/codegen_fpybc.py index 9275449..28976b6 100644 --- a/src/fpy/codegen_fpybc.py +++ b/src/fpy/codegen_fpybc.py @@ -148,10 +148,10 @@ class FpybcBackendState(BackendState): """The fpybc backend's view of a program: how its variables are laid out, and what it has emitted so far.""" - frame_offsets: dict[VariableSymbol, int] = field(default_factory=dict) + variable_frame_offsets: dict[VariableSymbol, int] = field(default_factory=dict) """variable to the offset of its storage within its frame""" - frame_sizes: dict[Ast, int] = field(default_factory=dict) + block_frame_sizes: dict[AstBlock, int] = field(default_factory=dict) """the block that owns a frame, to the total size in bytes of that frame's locals""" @@ -187,7 +187,7 @@ def __init__(self, offset: int): self.offset = offset def visit_AstBlock(self, node: AstBlock, state: CompileState): - frame_offsets = state.backend.frame_offsets + frame_offsets = state.backend.variable_frame_offsets for sym in state.enclosing_scope[node].group(NameGroup.VALUE).values(): if is_instance_compat(sym, VariableSymbol) and sym not in frame_offsets: frame_offsets[sym] = self.offset @@ -223,12 +223,25 @@ def run(self, start: Ast, state: CompileState): super().run(start, state) def visit_AstDef(self, node: AstDef, state: CompileState): - self._layout_function_frame(node, state) + # Formal parameters sit before the frame start, at negative offsets. + frame_offsets = state.backend.variable_frame_offsets + func_sym = state.resolved_symbols[node.name] + body_values = state.enclosing_scope[node.body].group(NameGroup.VALUE) + arg_offset = -STACK_FRAME_HEADER_SIZE + for arg_name, arg_type, _default in reversed(func_sym.args): + arg_offset -= arg_type.max_size + arg_var = body_values[arg_name] + assert is_instance_compat(arg_var, VariableSymbol), arg_var + frame_offsets[arg_var] = arg_offset + + state.backend.block_frame_sizes[node.body] = self._layout_locals( + node.body, 0, state + ) def _layout_main_frame(self, state: CompileState): # Sequence args arrive on the stack first, then the flags slot -- which # lives in the base scope but occupies a slot in the main frame here. - frame_offsets = state.backend.frame_offsets + frame_offsets = state.backend.variable_frame_offsets offset = 0 for name, arg_type in state.this_seq_arg_specs: arg_var = state.main_scope.group(NameGroup.VALUE)[name] @@ -237,23 +250,10 @@ def _layout_main_frame(self, state: CompileState): frame_offsets[state.flags_var] = offset offset += state.flags_var.type.max_size - state.backend.frame_sizes[state.main_block] = self._layout_locals( + state.backend.block_frame_sizes[state.main_block] = self._layout_locals( state.main_block, offset, state ) - def _layout_function_frame(self, node: AstDef, state: CompileState): - # FIXME you can inline this func - # Formal parameters sit before the frame start, at negative offsets. - frame_offsets = state.backend.frame_offsets - func = state.resolved_symbols[node.name] - body_values = state.enclosing_scope[node.body].group(NameGroup.VALUE) - arg_offset = -STACK_FRAME_HEADER_SIZE - for arg_name, arg_type, _default in reversed(func.args): - arg_offset -= arg_type.max_size - frame_offsets[body_values[arg_name]] = arg_offset - - state.backend.frame_sizes[node.body] = self._layout_locals(node.body, 0, state) - def _layout_locals(self, frame_block: AstBlock, offset: int, state) -> int: """Lay out every local in *frame_block*'s frame, starting at *offset*, and return the offset past the last one (the frame's total size).""" @@ -280,7 +280,7 @@ def visit_AstDef(self, node: AstDef, state: CompileState): code = [entry_label] # Allocate space for local variables - frame_size_bytes = state.backend.frame_sizes[node.body] + frame_size_bytes = state.backend.block_frame_sizes[node.body] if frame_size_bytes > 0: code.append(AllocateDirective(frame_size_bytes)) @@ -506,7 +506,7 @@ def _emit_assert_cmd_response_ok( dirs.append(not_ok_label) # response was not OK — read flags.assert_cmd_success from the stack # assert_cmd_success is at offset 0 within the flags struct - flag_offset = state.backend.frame_offsets[state.flags_var] + flag_offset = state.backend.variable_frame_offsets[state.flags_var] dirs.append(LoadAbsDirective(flag_offset, BOOL.max_size)) # if flag is false, skip to end (don't exit) dirs.append(IrIf(end_label)) @@ -927,7 +927,7 @@ def emit_AstIdent(self, node: AstIdent, state: CompileState): # a global variable. At top level, stack_frame_start = 0, so a # frame-relative offset is already the absolute one. use_abs = self.in_function and sym.is_global - offset = state.backend.frame_offsets[sym] + offset = state.backend.variable_frame_offsets[sym] if use_abs: dirs = [LoadAbsDirective(offset, sym.type.max_size)] else: @@ -1280,12 +1280,12 @@ def emit_AstAssign(self, node: AstAssign, state: CompileState): dynamic_components = [] if is_instance_compat(lhs, VariableSymbol): - base_frame_offset = state.backend.frame_offsets[lhs] + base_frame_offset = state.backend.variable_frame_offsets[lhs] is_global_var = lhs.is_global else: assert is_instance_compat(lhs, FieldAccess), lhs assert is_instance_compat(lhs.base_sym, VariableSymbol), lhs.base_sym - base_frame_offset = state.backend.frame_offsets[lhs.base_sym] + base_frame_offset = state.backend.variable_frame_offsets[lhs.base_sym] is_global_var = lhs.base_sym.is_global # Walk the field access chain to compute the total offset. @@ -1413,14 +1413,16 @@ def emit_AstBlock(self, node: AstBlock, state: CompileState): flags_type = state.flags_var.type args_size = sum(t.max_size for _, t in state.this_seq_arg_specs) - assert state.backend.frame_offsets[state.flags_var] == args_size + assert state.backend.variable_frame_offsets[state.flags_var] == args_size flags_default = FpyValue(flags_type, dict(flags_type.member_defaults)) main_body.append(PushValDirective(flags_default.serialize())) # we can calc how much space the user-defined lvars take by subtracting # the sequence args size, and the flags size, from the frame size - remaining = state.backend.frame_sizes[node] - flags_type.max_size - args_size + remaining = ( + state.backend.block_frame_sizes[node] - flags_type.max_size - args_size + ) assert remaining >= 0, remaining # allocate space for local variables diff --git a/src/fpy/test_helpers.py b/src/fpy/test_helpers.py index 928d98c..c2efbe7 100644 --- a/src/fpy/test_helpers.py +++ b/src/fpy/test_helpers.py @@ -28,20 +28,20 @@ from fpy.error import WarningType from fpy.harness import HarnessError, fpybc_harness, wasm_harness from fpy.state import CompileState, get_base_compile_state -from fpy.types import CmdDef, FpyType, FpyValue, TypeKind +from fpy.types import CMD_RESPONSE, CmdDef, FpyType, FpyValue, TypeKind default_dictionary = str( Path(__file__).parent.parent.parent / "test" / "fpy" / "RefTopologyDictionary.json" ) -# Flipped to True by conftest's pytest_configure when --wasm is passed, routing -# the assert_* helpers through the LLVM/wasm backend (run on the real -# Svc::WasmSequencer through the wasm harness) instead of the bytecode VM. -USE_WASM = False +# The backend the assert_* helpers compile and run on: "fpybc" (the bytecode +# VM on the real Svc::FpySequencer) or "wasm" (the LLVM backend on the real +# Svc::WasmSequencer). conftest sets it to "wasm" when --wasm is passed. +BACKEND = "fpybc" # Fw.CmdResponse enum values. -CMD_RESPONSE_OK = 0 -CMD_RESPONSE_EXECUTION_ERROR = 4 +CMD_RESPONSE_OK = CMD_RESPONSE.enum_dict["OK"] +CMD_RESPONSE_EXECUTION_ERROR = CMD_RESPONSE.enum_dict["EXECUTION_ERROR"] class CompilationFailed(Exception): @@ -57,14 +57,11 @@ class ValidationError(Exception): # Compiling # --------------------------------------------------------------------------- -# Every known warning type. Tests fail on ANY warning by default: the compile -# helpers promote every warning to a hard error unless the caller declares it in -# `expected_warnings` (kept as a collected warning) or `ignored_warnings` -# (dropped). This surfaces stray warnings -- e.g. an accidental shadow -- that a -# test did not mean to trigger. +# Every known warning type. Tests fail on ANY warning by default ALL_WARNINGS = frozenset(WarningType) +# FIXME inline this func def _default_error_warnings(error_warnings, ignored_warnings, expected_warnings): """The set of warnings to promote to errors. An explicit *error_warnings* wins; otherwise it is every warning except those expected or ignored.""" @@ -73,6 +70,7 @@ def _default_error_warnings(error_warnings, ignored_warnings, expected_warnings) return ALL_WARNINGS - set(expected_warnings or ()) - set(ignored_warnings or ()) +# FIXME inline this func def _assert_expected_emitted(state, expected_warnings): """A warning in *expected_warnings* must actually be emitted, not merely allowed -- so declaring it both permits it and asserts it. (Unexpected @@ -86,6 +84,7 @@ def _assert_expected_emitted(state, expected_warnings): def _compile( seq: str, + # FIXME this should be a "backend" str, either wasm or fpybc. to_wasm: bool, ground_binary_dir: str = None, ignored_warnings=None, @@ -128,6 +127,7 @@ def _compile( return state, output +# FIXME inline this func def compile_seq( seq: str, **kwargs ) -> tuple[CompileState, list[Directive], list[tuple[str, FpyType]]]: @@ -137,6 +137,7 @@ def compile_seq( return state, directives, arg_types +# FIXME inline this func def compile_seq_wasm(seq: str, **kwargs) -> bytes: """Compile a sequence string to a runnable wasm binary (the LLVM backend). See _compile for the keyword args.""" @@ -197,6 +198,7 @@ def _serialize_args(args: list[FpyValue] | None) -> bytes | None: # --------------------------------------------------------------------------- +# FIXME rename to make run request def _run_request( seq_file: str, seq_dir: str, @@ -208,6 +210,7 @@ def _run_request( args: bytes = None, cmd_responses: dict[int, int] = None, ) -> dict: + # FIXME explain that this builds a run request """The run request fields common to both sequencer harnesses. *tlm* and *prms* map channel/parameter names to the serialized values the harness answers reads with; every command completes OK unless *cmd_responses* @@ -241,9 +244,7 @@ def _run_request( def _seq_args_buffer_len(d: dict) -> int: - """The dictionary's Svc.SeqArgs buffer length. The harness needs it to - parse seq-run commands, and it can differ from the flight build's own - Svc::SeqArgs size.""" + """The dictionary's Svc.SeqArgs buffer length.""" (buffer_member,) = [ m for m in d["type_defs"]["Svc.SeqArgs"].members if m.name == "buffer" ] @@ -253,7 +254,10 @@ def _seq_args_buffer_len(d: dict) -> int: def _expected_stack_bytes(directives: list[Directive], args: bytes | None) -> int: """The exact stack size a successful run must end with: the sequence arguments plus the frame setup (PushVal for the flags default, then - optionally Allocate for the remaining locals). If functions are present + optionally Allocate for the remaining locals). + + # FIXME instead of having this in docstring, it should be a comment: + If functions are present the first directive is a Goto that jumps past them; the setup starts at its target.""" setup_start = 0 @@ -277,6 +281,7 @@ def _as_int(v) -> int: return v.value if isinstance(v, DirectiveErrorCode) else v +# FIXME can't we fold this into run_seq? def run_seq_raw( directives: list[Directive], tlm: dict[str, bytes] = None, diff --git a/test/conftest.py b/test/conftest.py index df9b52b..a35c77d 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -29,6 +29,7 @@ def pytest_addoption(parser): _wasm_harness_built = False +# FIXME why is this only here for the wasm harness? shouldn't we do this for the fpybc harness too? def _build_wasm_harness_once(): """Build the wasm harness once per session, exiting with an actionable message on setup gaps (submodule missing, tools missing).""" @@ -60,6 +61,9 @@ def pytest_configure(config): # runs a sequence through it (fpy.harness.fpybc_harness), so runs that # never touch it -- compiler unit tests, --collect-only -- skip the # build entirely. + # FIXME I think we should do the same thing for both harnesses probably. + # just build them lazily. why wouldn't that work for the test_wasm files? + # i think for the test_wasm tests you should just pass a backend="wasm" kw to the assert_xyz funcs, then you could remove the marker system def pytest_unconfigure(config): diff --git a/test/fpy/test_wasm.py b/test/fpy/test_wasm.py index f4e6a12..7e3a5ba 100644 --- a/test/fpy/test_wasm.py +++ b/test/fpy/test_wasm.py @@ -1049,10 +1049,7 @@ class TestWasmCommands: def _opcode_int(self, name: str) -> int: return load_dictionary(default_dictionary)["cmd_name_dict"][name].opcode - # FIXME rename to opcode bytes - # FIXME please also add a pre commit check that searches for # FIXME or #FIXME in src or test (exclude in submodules, allow easy exclusions for later) and fails commit if there are any - # FIXME actually just searchfor teh FIXME string so it works in all langs - def _opcode(self, name: str) -> bytes: + def _opcode_bytes(self, name: str) -> bytes: return struct.pack(">I", self._opcode_int(name)) def test_cmd_emits_fprime_cmd_import(self): @@ -1065,14 +1062,14 @@ def test_cmd_emits_fprime_cmd_import(self): def test_const_no_arg_command(self): code, cmds = run_seq_wasm_with_cmds("CdhCore.cmdDisp.CMD_NO_OP()\n") assert code == NO_ERROR - assert cmds == [self._opcode("CdhCore.cmdDisp.CMD_NO_OP")] + assert cmds == [self._opcode_bytes("CdhCore.cmdDisp.CMD_NO_OP")] def test_const_string_arg_is_compact(self): # A constant string serializes at its actual length (u16 big-endian # prefix + bytes), not its declared capacity. code, cmds = run_seq_wasm_with_cmds('CdhCore.cmdDisp.CMD_NO_OP_STRING("hi")\n') assert code == NO_ERROR - expected = self._opcode("CdhCore.cmdDisp.CMD_NO_OP_STRING") + expected = self._opcode_bytes("CdhCore.cmdDisp.CMD_NO_OP_STRING") expected += struct.pack(">H", 2) + b"hi" assert cmds == [expected] @@ -1080,7 +1077,7 @@ def test_empty_string_arg(self): # An empty string is just the zero length prefix. code, cmds = run_seq_wasm_with_cmds('CdhCore.cmdDisp.CMD_NO_OP_STRING("")\n') assert code == NO_ERROR - expected = self._opcode("CdhCore.cmdDisp.CMD_NO_OP_STRING") + expected = self._opcode_bytes("CdhCore.cmdDisp.CMD_NO_OP_STRING") expected += struct.pack(">H", 0) assert cmds == [expected] @@ -1093,7 +1090,7 @@ def test_utf8_string_arg_prefix_counts_bytes(self): assert code == NO_ERROR data = "héllo✓".encode("utf-8") assert len(data) == 9 - expected = self._opcode("CdhCore.cmdDisp.CMD_NO_OP_STRING") + expected = self._opcode_bytes("CdhCore.cmdDisp.CMD_NO_OP_STRING") expected += struct.pack(">H", len(data)) + data assert cmds == [expected] @@ -1107,7 +1104,7 @@ def test_runtime_scalar_args(self): "CdhCore.cmdDisp.CMD_TEST_CMD_1(var1, var2, var3)\n" ) assert code == NO_ERROR - expected = self._opcode("CdhCore.cmdDisp.CMD_TEST_CMD_1") + expected = self._opcode_bytes("CdhCore.cmdDisp.CMD_TEST_CMD_1") expected += struct.pack(">ifB", -2, 1.5, 8) assert cmds == [expected] @@ -1120,7 +1117,7 @@ def test_runtime_bool_arg(self, flag, byte): "Ref.cmdSeq0.SET_BREAKPOINT(idx, flag)\n" ) assert code == NO_ERROR - expected = self._opcode("Ref.cmdSeq0.SET_BREAKPOINT") + expected = self._opcode_bytes("Ref.cmdSeq0.SET_BREAKPOINT") expected += struct.pack(">I", 3) + byte assert cmds == [expected] @@ -1132,7 +1129,7 @@ def test_mixed_const_and_runtime_args(self): 'CdhCore.health.HLTH_PING_ENABLE("task1", en)\n' ) assert code == NO_ERROR - expected = self._opcode("CdhCore.health.HLTH_PING_ENABLE") + expected = self._opcode_bytes("CdhCore.health.HLTH_PING_ENABLE") expected += struct.pack(">H", 5) + b"task1" # entry: String_40 expected += struct.pack(">B", 1) # enable: Fw.Enabled (u8 rep), ENABLED assert cmds == [expected] @@ -1146,7 +1143,7 @@ def test_runtime_struct_arg_all_scalar_widths(self): "Ref.typeDemo.SEND_SCALARS(s)\n" ) assert code == NO_ERROR - expected = self._opcode("Ref.typeDemo.SEND_SCALARS") + expected = self._opcode_bytes("Ref.typeDemo.SEND_SCALARS") expected += struct.pack(">bhiqBHIQfd", -1, -2, -3, -4, 1, 2, 3, 4, 1.5, -2.5) assert cmds == [expected] @@ -1157,7 +1154,7 @@ def test_runtime_array_arg(self): "Ref.typeDemo.CHOICES(c)\n" ) assert code == NO_ERROR - expected = self._opcode("Ref.typeDemo.CHOICES") + expected = self._opcode_bytes("Ref.typeDemo.CHOICES") expected += struct.pack(">ii", 1, 2) # TWO = 1, RED = 2 assert cmds == [expected] @@ -1167,10 +1164,10 @@ def test_multiple_commands_in_call_order(self): ) assert code == NO_ERROR assert cmds == [ - self._opcode("CdhCore.cmdDisp.CMD_NO_OP_STRING") + self._opcode_bytes("CdhCore.cmdDisp.CMD_NO_OP_STRING") + struct.pack(">H", 1) + b"a", - self._opcode("CdhCore.cmdDisp.CMD_NO_OP"), + self._opcode_bytes("CdhCore.cmdDisp.CMD_NO_OP"), ] def test_captured_response_compares_ok(self): @@ -1209,7 +1206,7 @@ def test_bare_failing_command_exits_cmd_fail(self): ) assert code == DirectiveErrorCode.CMD_FAIL.value # The command was dispatched; the sequence ended on its response. - assert cmds == [self._opcode("CdhCore.cmdDisp.CMD_NO_OP")] + assert cmds == [self._opcode_bytes("CdhCore.cmdDisp.CMD_NO_OP")] def test_assert_cmd_success_flag_disables_check(self): # With the flag cleared, failing bare commands don't end the sequence; @@ -1223,7 +1220,7 @@ def test_assert_cmd_success_flag_disables_check(self): }, ) assert code == NO_ERROR - assert cmds == [self._opcode("CdhCore.cmdDisp.CMD_NO_OP")] * 2 + assert cmds == [self._opcode_bytes("CdhCore.cmdDisp.CMD_NO_OP")] * 2 def test_bare_command_inside_if_block(self): # The response check applies to bare commands in nested blocks too. From 0dd3dc1dc73eacc3636f020ee6e6258652e69f26 Mon Sep 17 00:00:00 2001 From: zimri-leisher Date: Fri, 14 Aug 2026 11:56:14 -0400 Subject: [PATCH 10/10] Test all cmds --- src/fpy/harness.py | 15 +- src/fpy/test_helpers.py | 210 ++++++++++-------------- test/conftest.py | 46 +----- test/fpy/test_arithmetic.py | 2 +- test/fpy/test_check.py | 6 +- test/fpy/test_commands.py | 4 +- test/fpy/test_golden.py | 9 +- test/fpy/test_imports.py | 12 +- test/fpy/test_logging.py | 10 +- test/fpy/test_types_and_constructors.py | 4 +- test/fpy/test_warnings.py | 8 +- test/fpy/test_wasm.py | 12 +- test/fpy/test_write_to_port.py | 2 +- 13 files changed, 134 insertions(+), 206 deletions(-) diff --git a/src/fpy/harness.py b/src/fpy/harness.py index 4832a87..7f70e10 100644 --- a/src/fpy/harness.py +++ b/src/fpy/harness.py @@ -162,9 +162,10 @@ def close(self) -> None: _fpybc_harness: SequencerHarness | None = None _wasm_harness: SequencerHarness | None = None -# The first failed build, re-raised on later calls: retrying the build once +# The first failed builds, re-raised on later calls: retrying a build once # it has failed only repeats the same slow failure. _fpybc_build_error: HarnessError | None = None +_wasm_build_error: HarnessError | None = None def fpybc_harness() -> SequencerHarness: @@ -184,9 +185,17 @@ def fpybc_harness() -> SequencerHarness: def wasm_harness() -> SequencerHarness: - """The shared harness for the LLVM/wasm backend.""" - global _wasm_harness + """The shared harness for the LLVM/wasm backend, building its binary on + first use.""" + global _wasm_harness, _wasm_build_error + if _wasm_build_error is not None: + raise _wasm_build_error if _wasm_harness is None: + try: + build_wasm_harness() + except HarnessError as e: + _wasm_build_error = e + raise _wasm_harness = SequencerHarness(WASM_HARNESS_BINARY) return _wasm_harness diff --git a/src/fpy/test_helpers.py b/src/fpy/test_helpers.py index c2efbe7..caa2b8a 100644 --- a/src/fpy/test_helpers.py +++ b/src/fpy/test_helpers.py @@ -61,31 +61,9 @@ class ValidationError(Exception): ALL_WARNINGS = frozenset(WarningType) -# FIXME inline this func -def _default_error_warnings(error_warnings, ignored_warnings, expected_warnings): - """The set of warnings to promote to errors. An explicit *error_warnings* - wins; otherwise it is every warning except those expected or ignored.""" - if error_warnings is not None: - return error_warnings - return ALL_WARNINGS - set(expected_warnings or ()) - set(ignored_warnings or ()) - - -# FIXME inline this func -def _assert_expected_emitted(state, expected_warnings): - """A warning in *expected_warnings* must actually be emitted, not merely - allowed -- so declaring it both permits it and asserts it. (Unexpected - warnings already fail via promotion to errors.)""" - if not expected_warnings: - return - emitted = {w.type for w in state.warnings} - missing = set(expected_warnings) - emitted - assert not missing, f"expected warnings not emitted: {missing} (got {emitted})" - - -def _compile( +def compile_seq( seq: str, - # FIXME this should be a "backend" str, either wasm or fpybc. - to_wasm: bool, + backend: str = "fpybc", ground_binary_dir: str = None, ignored_warnings=None, error_warnings=None, @@ -93,21 +71,27 @@ def _compile( import_directories: list[str] | None = None, main_file_dir: str | None = None, main_file_path: str | None = None, -): - """Compile a sequence string and return (state, backend output): the wasm - binary bytes when *to_wasm*, else (directives, arg_types). +) -> tuple[CompileState, tuple[list[Directive], list[tuple[str, FpyType]]] | bytes]: + """Compile a sequence string on *backend* and return (state, output): + (directives, arg_types) for "fpybc", the runnable wasm binary bytes for + "wasm". By default every warning is a hard error; pass *expected_warnings* to allow (and still collect) specific ones.""" fpy.error.file_name = "" + # Warnings not explicitly expected or ignored are promoted to errors, so + # a stray warning a test did not mean to trigger fails it. + if error_warnings is None: + error_warnings = ( + ALL_WARNINGS - set(expected_warnings or ()) - set(ignored_warnings or ()) + ) + state = get_base_compile_state( default_dictionary, ground_binary_dir, ignored_warnings=ignored_warnings, - error_warnings=_default_error_warnings( - error_warnings, ignored_warnings, expected_warnings - ), + error_warnings=error_warnings, import_directories=import_directories, main_file_dir=main_file_dir, main_file_path=main_file_path, @@ -116,33 +100,22 @@ def _compile( try: body = text_to_ast(seq) state = analyze_ast(body, state) - if to_wasm: + if backend == "wasm": output, _ = analysis_to_wasm(state) else: + assert backend == "fpybc", backend output = analysis_to_fpybc_directives(state) except (fpy.error.CompileError, fpy.error.BackendError) as e: raise CompilationFailed(f"Compilation failed:\n{e}") - _assert_expected_emitted(state, expected_warnings) - return state, output - - -# FIXME inline this func -def compile_seq( - seq: str, **kwargs -) -> tuple[CompileState, list[Directive], list[tuple[str, FpyType]]]: - """Compile a sequence string to fpy bytecode. Returns - (state, directives, arg_types). See _compile for the keyword args.""" - state, (directives, arg_types) = _compile(seq, to_wasm=False, **kwargs) - return state, directives, arg_types - + # A warning in *expected_warnings* must actually be emitted, not merely + # allowed -- so declaring it both permits it and asserts it. + if expected_warnings: + emitted = {w.type for w in state.warnings} + missing = set(expected_warnings) - emitted + assert not missing, f"expected warnings not emitted: {missing} (got {emitted})" -# FIXME inline this func -def compile_seq_wasm(seq: str, **kwargs) -> bytes: - """Compile a sequence string to a runnable wasm binary (the LLVM backend). - See _compile for the keyword args.""" - _, wasm = _compile(seq, to_wasm=True, **kwargs) - return wasm + return state, output # --------------------------------------------------------------------------- @@ -198,8 +171,7 @@ def _serialize_args(args: list[FpyValue] | None) -> bytes | None: # --------------------------------------------------------------------------- -# FIXME rename to make run request -def _run_request( +def _make_run_request( seq_file: str, seq_dir: str, tlm: dict[str, bytes] = None, @@ -210,11 +182,10 @@ def _run_request( args: bytes = None, cmd_responses: dict[int, int] = None, ) -> dict: - # FIXME explain that this builds a run request - """The run request fields common to both sequencer harnesses. *tlm* and - *prms* map channel/parameter names to the serialized values the harness - answers reads with; every command completes OK unless *cmd_responses* - maps its opcode to another Fw.CmdResponse value.""" + """Build a run request, with the fields common to both sequencer + harnesses. *tlm* and *prms* map channel/parameter names to the serialized + values the harness answers reads with; every command completes OK unless + *cmd_responses* maps its opcode to another Fw.CmdResponse value.""" d = load_dictionary(default_dictionary) responses = cmd_responses or {} request = { @@ -254,12 +225,9 @@ def _seq_args_buffer_len(d: dict) -> int: def _expected_stack_bytes(directives: list[Directive], args: bytes | None) -> int: """The exact stack size a successful run must end with: the sequence arguments plus the frame setup (PushVal for the flags default, then - optionally Allocate for the remaining locals). - - # FIXME instead of having this in docstring, it should be a comment: - If functions are present - the first directive is a Goto that jumps past them; the setup starts at - its target.""" + optionally Allocate for the remaining locals).""" + # If functions are present the first directive is a Goto that jumps past + # them; the setup starts at its target. setup_start = 0 if directives and isinstance(directives[0], GotoDirective): setup_start = directives[0].dir_idx @@ -281,8 +249,7 @@ def _as_int(v) -> int: return v.value if isinstance(v, DirectiveErrorCode) else v -# FIXME can't we fold this into run_seq? -def run_seq_raw( +def run_seq( directives: list[Directive], tlm: dict[str, bytes] = None, time_base: int = 0, @@ -294,10 +261,19 @@ def run_seq_raw( ground_binary_dir: str = None, prms: dict[str, bytes] = None, cmd_responses: dict[int, int] = None, -) -> dict: + raw: bool = False, +) -> list[bytes] | dict: """Run a list of directives on a real Svc::FpySequencer through the test - harness (test/harness) and return the harness's raw JSON reply. See - _run_request for the inputs.""" + harness (test/harness). Returns the command buffers the sequence + dispatched (the big-endian serialized FwOpcodeType + arguments), in call + order -- or the harness's raw JSON reply, uninterpreted, with *raw*. See + _make_run_request for the inputs. + + Raises ValidationError when the sequencer rejects the sequence before + running it, and RuntimeError when the sequence fails: with the + DirectiveErrorCode for a trap, or the raw error code int for a nonzero + exit. + """ d = load_dictionary(default_dictionary) # When the test provides a ground_binary_dir, that directory doubles as @@ -309,7 +285,7 @@ def run_seq_raw( if seq_run_opcodes is None and ground_binary_dir is not None: seq_run_opcodes = {d["cmd_name_dict"]["Ref.seqDisp.RUN_ARGS"].opcode} - request = _run_request( + request = _make_run_request( seq_file, seq_dir, tlm=tlm, @@ -324,21 +300,9 @@ def run_seq_raw( request["seqRunOpcodes"] = sorted(seq_run_opcodes) request["seqArgsBufferSize"] = _seq_args_buffer_len(d) - return fpybc_harness().run(request) - - -def run_seq(directives: list[Directive], **run_kwargs) -> list[bytes]: - """Run a list of directives on a real Svc::FpySequencer through the test - harness (test/harness). Returns the command buffers the sequence - dispatched (the big-endian serialized FwOpcodeType + arguments), in call - order. See run_seq_raw for the keyword args. - - Raises ValidationError when the sequencer rejects the sequence before - running it, and RuntimeError when the sequence fails: with the - DirectiveErrorCode for a trap, or the raw error code int for a nonzero - exit. - """ - result = run_seq_raw(directives, **run_kwargs) + result = fpybc_harness().run(request) + if raw: + return result if "error" in result: raise HarnessError(result["error"]) @@ -361,7 +325,7 @@ def run_seq(directives: list[Directive], **run_kwargs) -> list[bytes]: ) # A finished run must leave exactly the stack bytes the compiler # expected; a leak of even one byte is a failure. - expected_stack = _expected_stack_bytes(directives, run_kwargs.get("args")) + expected_stack = _expected_stack_bytes(directives, args) actual_stack = len(bytes.fromhex(result["stack"])) if actual_stack != expected_stack: raise RuntimeError(f"Sequence leaked {actual_stack - expected_stack} bytes") @@ -385,7 +349,7 @@ def run_seq(directives: list[Directive], **run_kwargs) -> list[bytes]: raise RuntimeError(DirectiveErrorCode(result["lastDirectiveError"])) -def run_wasm_raw( +def run_wasm( wasm: bytes, tlm: dict[str, bytes] = None, prms: dict[str, bytes] = None, @@ -394,12 +358,14 @@ def run_wasm_raw( initial_time_us: int = 0, args: bytes = None, cmd_responses: dict[int, int] = None, -) -> dict: + raw: bool = False, +) -> tuple[int, list[tuple[int, str]], list[bytes]] | dict: """Run an already-linked wasm module on a real Svc::WasmSequencer through - the wasm harness and return the harness's raw JSON reply. See _run_request - for the inputs.""" + the wasm harness and return (error code, reported events, dispatched + command buffers) -- or the harness's raw JSON reply, uninterpreted, with + *raw*. See _make_run_request for the inputs.""" seq_dir, seq_file = _write_for_harness(wasm, "m0.wasm") - request = _run_request( + request = _make_run_request( seq_file, seq_dir, tlm=tlm, @@ -410,16 +376,9 @@ def run_wasm_raw( args=args, cmd_responses=cmd_responses, ) - return wasm_harness().run(request) - - -def run_wasm( - wasm: bytes, **run_kwargs -) -> tuple[int, list[tuple[int, str]], list[bytes]]: - """Run an already-linked wasm module on a real Svc::WasmSequencer through - the wasm harness and return (error code, reported events, dispatched - command buffers). See run_wasm_raw for the keyword args.""" - result = run_wasm_raw(wasm, **run_kwargs) + result = wasm_harness().run(request) + if raw: + return result if "error" in result: raise HarnessError(result["error"]) @@ -449,9 +408,9 @@ def _run_seq_wasm( **kwargs, ) -> tuple[int, list[tuple[int, str]], list[bytes]]: """Compile *seq* to wasm and run it through the wasm harness. Returns - (error code, reported events, dispatched command buffers). See _compile + (error code, reported events, dispatched command buffers). See compile_seq for the remaining keyword args.""" - wasm = compile_seq_wasm(seq, **kwargs) + _, wasm = compile_seq(seq, "wasm", **kwargs) return run_wasm(wasm, cmd_responses=cmd_responses) @@ -521,13 +480,10 @@ def lookup_type(type_name: str) -> FpyType: return load_dictionary(default_dictionary)["type_defs"][type_name] -def assert_compile_success(fprime_test_api, seq: str, **kwargs): - """Compile *seq* on the current backend. See _compile for the keyword - args.""" - if USE_WASM: - compile_seq_wasm(seq, **kwargs) - else: - compile_seq(seq, **kwargs) +def assert_compile_success(fprime_test_api, seq: str, backend: str = None, **kwargs): + """Compile *seq* on *backend* (the session backend by default). See + compile_seq for the keyword args.""" + compile_seq(seq, backend or BACKEND, **kwargs) def assert_compile_failure(fprime_test_api, seq: str, match: str = None, **kwargs): @@ -545,6 +501,7 @@ def assert_compile_failure(fprime_test_api, seq: str, match: str = None, **kwarg def assert_run_success( fprime_test_api, seq: str, + backend: str = None, timeout_s: int = 4, args: list[FpyValue] = None, ground_binary_dir: str = None, @@ -553,21 +510,23 @@ def assert_run_success( main_file_dir: str | None = None, **run_kwargs, ) -> list[bytes] | None: - """Compile *seq* on the current backend, run it, and assert it succeeds. - Returns the command buffers the sequence dispatched, or None when running - against a live GDS deployment. The remaining keyword args (tlm, prms, - time, cmd_responses, ...) are the current backend's run_*_raw inputs. + """Compile *seq* on *backend* (the session backend by default), run it, + and assert it succeeds. Returns the command buffers the sequence + dispatched, or None when running against a live GDS deployment. The + remaining keyword args (tlm, prms, time, cmd_responses, ...) are the + backend's run_seq / run_wasm inputs. Runs on the test harness by default, or against a live GDS deployment when fprime_test_api is not None (--use-gds).""" + backend = backend or BACKEND compile_kwargs = dict( ground_binary_dir=ground_binary_dir, import_directories=import_directories, expected_warnings=expected_warnings, main_file_dir=main_file_dir, ) - if USE_WASM: - wasm = compile_seq_wasm(seq, **compile_kwargs) + if backend == "wasm": + _, wasm = compile_seq(seq, "wasm", **compile_kwargs) if fprime_test_api is not None: _run_gds( fprime_test_api, @@ -583,7 +542,7 @@ def assert_run_success( raise RuntimeError(f"wasm sequence returned error code {code}") return cmds - _, directives, arg_types = compile_seq(seq, **compile_kwargs) + _, (directives, arg_types) = compile_seq(seq, **compile_kwargs) args_bytes = _serialize_args(args) if fprime_test_api is not None: _run_gds( @@ -609,16 +568,18 @@ def assert_run_failure( seq: str, error_code: DirectiveErrorCode | int = None, validation_error: bool = False, + backend: str = None, args: list[FpyValue] = None, ground_binary_dir: str = None, import_directories: list[str] | None = None, **run_kwargs, ): - """Compile *seq* on the current backend, run it, and assert it fails: - with *error_code* (a DirectiveErrorCode trap or a raw exit code int), or - with *validation_error* when the sequencer must reject the sequence - before running it. The remaining keyword args (tlm, prms, time, - cmd_responses, ...) are the current backend's run_*_raw inputs.""" + """Compile *seq* on *backend* (the session backend by default), run it, + and assert it fails: with *error_code* (a DirectiveErrorCode trap or a + raw exit code int), or with *validation_error* when the sequencer must + reject the sequence before running it. The remaining keyword args (tlm, + prms, time, cmd_responses, ...) are the backend's run_seq / run_wasm + inputs.""" assert not ( error_code is not None and validation_error ), "Cannot specify both error_code and validation_error" @@ -626,11 +587,12 @@ def assert_run_failure( error_code is not None or validation_error ), "Must specify either error_code or validation_error" + backend = backend or BACKEND compile_kwargs = dict( ground_binary_dir=ground_binary_dir, import_directories=import_directories ) - if USE_WASM: - wasm = compile_seq_wasm(seq, **compile_kwargs) + if backend == "wasm": + _, wasm = compile_seq(seq, "wasm", **compile_kwargs) if fprime_test_api is not None: _run_gds( fprime_test_api, @@ -650,7 +612,7 @@ def assert_run_failure( raise RuntimeError(f"wasm sequence returned {code}, expected {error_code}") return - _, directives, arg_types = compile_seq(seq, **compile_kwargs) + _, (directives, arg_types) = compile_seq(seq, **compile_kwargs) args_bytes = _serialize_args(args) if fprime_test_api is not None: _run_gds( diff --git a/test/conftest.py b/test/conftest.py index a35c77d..3ed2b78 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -26,44 +26,16 @@ def pytest_addoption(parser): ) -_wasm_harness_built = False - - -# FIXME why is this only here for the wasm harness? shouldn't we do this for the fpybc harness too? -def _build_wasm_harness_once(): - """Build the wasm harness once per session, exiting with an actionable - message on setup gaps (submodule missing, tools missing).""" - global _wasm_harness_built - if _wasm_harness_built: - return - try: - fpy.harness.build_wasm_harness() - except fpy.harness.HarnessError as e: - pytest.exit(str(e), returncode=1) - _wasm_harness_built = True - - def pytest_configure(config): - config.addinivalue_line( - "markers", - "wasm: end-to-end LLVM/wasm tests; always run on the wasm backend, " - "even without --wasm (requires the fprime-wasm submodule and Rust)", - ) - # Flip the test helpers over to the LLVM/wasm backend for the whole run. import fpy.test_helpers as test_helpers - test_helpers.USE_WASM = config.getoption("--wasm") - if test_helpers.USE_WASM and not config.getoption("--use-gds"): - _build_wasm_harness_once() + test_helpers.BACKEND = "wasm" if config.getoption("--wasm") else "fpybc" - # The FpySequencer harness builds itself lazily, on the first test that - # runs a sequence through it (fpy.harness.fpybc_harness), so runs that - # never touch it -- compiler unit tests, --collect-only -- skip the - # build entirely. - # FIXME I think we should do the same thing for both harnesses probably. - # just build them lazily. why wouldn't that work for the test_wasm files? - # i think for the test_wasm tests you should just pass a backend="wasm" kw to the assert_xyz funcs, then you could remove the marker system + # Both harnesses build themselves lazily, on the first test that runs a + # sequence through them (fpy.harness.fpybc_harness / wasm_harness), so + # runs that never touch one -- compiler unit tests, --collect-only -- + # skip its build entirely. def pytest_unconfigure(config): @@ -75,14 +47,6 @@ def update_goldens(request): return request.config.getoption("--update-goldens") -@pytest.fixture(autouse=True) -def _ensure_wasm_harness(request): - # wasm-marked tests always run on the wasm backend, regardless of --wasm, - # so make sure the wasm harness is built before any of them run. - if "wasm" in request.keywords: - _build_wasm_harness_once() - - # When --use-gds is NOT passed (the default), override fprime_test_api with None # so tests run against the harness instead of a live GDS. # When --use-gds IS passed, delegate to the fprime-gds plugin's session fixture diff --git a/test/fpy/test_arithmetic.py b/test/fpy/test_arithmetic.py index eee77a0..0ae4148 100644 --- a/test/fpy/test_arithmetic.py +++ b/test/fpy/test_arithmetic.py @@ -550,7 +550,7 @@ def test_abs_i64_edge_cases(self, fprime_test_api): def test_abs_i64_int_min_overflows(self, fprime_test_api): """abs(I64 min) is not representable in I64, so the sequence ends with ARITHMETIC_OVERFLOW rather than wrapping.""" - if test_helpers.USE_WASM: + if test_helpers.BACKEND == "wasm": pytest.skip("wasm backend does not implement arithmetic traps yet") seq = """ val: I64 = iabs(I64(-2**63)) diff --git a/test/fpy/test_check.py b/test/fpy/test_check.py index 72532b9..0547a4f 100644 --- a/test/fpy/test_check.py +++ b/test/fpy/test_check.py @@ -291,7 +291,7 @@ class TestCheckUnreachableTimeoutBody: """ def test_never_with_timeout_body_warns(self): - state, _, _ = compile_seq( + state, _ = compile_seq( self.NEVER_WITH_TIMEOUT_BODY_SEQ, expected_warnings={WarningType.UNREACHABLE_TIMEOUT_BODY}, ) @@ -307,14 +307,14 @@ def test_never_with_timeout_body_still_compiles(self): ) def test_never_without_timeout_body_does_not_warn(self): - state, _, _ = compile_seq("check True timeout never:\n pass\n") + state, _ = compile_seq("check True timeout never:\n pass\n") assert not any( w.type == WarningType.UNREACHABLE_TIMEOUT_BODY for w in state.warnings ) def test_finite_timeout_with_timeout_body_does_not_warn(self): # A real timeout with a timeout body is the normal, reachable case. - state, _, _ = compile_seq( + state, _ = compile_seq( "check True timeout Fw.TimeIntervalValue(1, 0):\n" " pass\n" "timeout:\n" diff --git a/test/fpy/test_commands.py b/test/fpy/test_commands.py index 09b8bde..6789998 100644 --- a/test/fpy/test_commands.py +++ b/test/fpy/test_commands.py @@ -71,8 +71,8 @@ def test_cmd_return_val(self, fprime_test_api): def test_too_many_dirs(self, fprime_test_api): from fpy.types import MAX_DIRECTIVES_COUNT - # read through the module: conftest sets USE_WASM from the --wasm flag - if test_helpers.USE_WASM: + # read through the module: conftest sets BACKEND from the --wasm flag + if test_helpers.BACKEND == "wasm": pytest.skip("the directive-count limit is bytecode-specific") seq = "CdhCore.cmdDisp.CMD_NO_OP()\n" * (MAX_DIRECTIVES_COUNT + 1) assert_compile_failure(fprime_test_api, seq) diff --git a/test/fpy/test_golden.py b/test/fpy/test_golden.py index 316e157..60a097c 100644 --- a/test/fpy/test_golden.py +++ b/test/fpy/test_golden.py @@ -42,7 +42,7 @@ from fpy.dictionary import load_dictionary from fpy.error import BackendError from fpy.state import get_base_compile_state -from fpy.test_helpers import run_seq_raw, run_wasm_raw +from fpy.test_helpers import run_seq, run_wasm GOLDEN_DIR = Path(__file__).parent / "golden" @@ -58,7 +58,7 @@ def parse_harness_inputs(source: str) -> dict: """The harness run inputs declared in the sequence's "# harness:" comments (see the module docstring for the directives), as keyword - arguments for run_seq_raw / run_wasm_raw.""" + arguments for run_seq / run_wasm.""" d = load_dictionary(DEFAULT_DICTIONARY) inputs = {"tlm": {}, "prms": {}, "cmd_responses": {}} args = b"" @@ -126,7 +126,7 @@ def run_on_fpybc_harness(name: str, source: str) -> dict: returning the raw JSON reply.""" directives, arg_types = analysis_to_fpybc_directives(_analyze(source)) inputs = parse_harness_inputs(source) - reply = run_seq_raw(directives, arg_types=arg_types, **inputs) + reply = run_seq(directives, arg_types=arg_types, raw=True, **inputs) assert "error" not in reply, f"harness failed to run {name}: {reply}" return reply @@ -140,7 +140,7 @@ def run_on_wasm_harness(name: str, source: str) -> dict: wasm, _ = analysis_to_wasm(state) except (BackendError, NotImplementedError) as e: return {"compileError": f"{type(e).__name__}: {e}"} - reply = run_wasm_raw(wasm, **parse_harness_inputs(source)) + reply = run_wasm(wasm, raw=True, **parse_harness_inputs(source)) assert "error" not in reply, f"harness failed to run {name}: {reply}" return reply @@ -244,7 +244,6 @@ def test_golden_run_fpybc(test_name: str, update_goldens: bool): ) -@pytest.mark.wasm @pytest.mark.parametrize("test_name", get_golden_test_cases()) def test_golden_run_wasm(test_name: str, update_goldens: bool): """Run the compiled sequence on the WasmSequencer harness and compare the diff --git a/test/fpy/test_imports.py b/test/fpy/test_imports.py index 3200e44..3f2dd1f 100644 --- a/test/fpy/test_imports.py +++ b/test/fpy/test_imports.py @@ -124,7 +124,7 @@ def add_one(x: U32) -> U32: assert result == 42 """ # Funcs-only sequence: compiles cleanly with no side-effect warning... - state, _, _ = compile_seq(main, import_directories=[str(tmp_path)]) + state, _ = compile_seq(main, import_directories=[str(tmp_path)]) assert state.warnings == [] # ...and the embedded assert holds at run time. assert_run_success(fprime_test_api, main, import_directories=[str(tmp_path)]) @@ -305,7 +305,7 @@ def test_underscore_alias_statement_warns_but_uses_do_not( assert y == 7 """ expected = {WarningType.IMPORT_UNDERSCORE} - state, _, _ = compile_seq( + state, _ = compile_seq( main, import_directories=[str(tmp_path)], expected_warnings=expected ) underscore_warnings = [ @@ -422,7 +422,7 @@ def add_one(x: U32) -> U32: result: U32 = lib.add_one(n) assert result == 42 """ - state, _, _ = compile_seq(main, import_directories=[str(tmp_path)]) + state, _ = compile_seq(main, import_directories=[str(tmp_path)]) assert state.warnings == [] assert_run_success( fprime_test_api, @@ -1172,7 +1172,7 @@ def b() -> U32: x: U32 = U32(cyc_a.a() + cyc_b.b()) assert x == 3 """ - state, _, _ = compile_seq(main, import_directories=[str(tmp_path)]) + state, _ = compile_seq(main, import_directories=[str(tmp_path)]) loaded = {Path(p).stem for p in state.loaded_sequences} assert loaded == {"cyc_a", "cyc_b"} assert len(state.imported_blocks) == 2 @@ -1196,7 +1196,7 @@ def f() -> U32: assert x == 3 """ main_file.write_text(main) - state, _, _ = compile_seq( + state, _ = compile_seq( main, import_directories=[], main_file_dir=str(tmp_path), @@ -1868,7 +1868,7 @@ def a() -> U32: assert x == 1 """ expected = {WarningType.IMPORT_DUPLICATE} - state, _, _ = compile_seq( + state, _ = compile_seq( main, import_directories=[str(tmp_path)], expected_warnings=expected ) duplicate_warnings = [ diff --git a/test/fpy/test_logging.py b/test/fpy/test_logging.py index 120d81e..7bc4486 100644 --- a/test/fpy/test_logging.py +++ b/test/fpy/test_logging.py @@ -29,7 +29,7 @@ def test_default_severity_is_activity_hi(self, fprime_test_api): seq = """ log("test message") """ - _, directives, _ = compile_seq(seq) + _, (directives, _) = compile_seq(seq) push_vals = [d for d in directives if isinstance(d, PushValDirective)] assert len(push_vals) >= 3 # ACTIVITY_HI = 5 @@ -40,7 +40,7 @@ def test_explicit_fatal(self, fprime_test_api): seq = """ log("critical", Fw.LogSeverity.FATAL) """ - _, directives, _ = compile_seq(seq) + _, (directives, _) = compile_seq(seq) push_vals = [d for d in directives if isinstance(d, PushValDirective)] assert len(push_vals) >= 3 # FATAL = 1 @@ -51,7 +51,7 @@ def test_explicit_warning_hi(self, fprime_test_api): seq = """ log("watch out", Fw.LogSeverity.WARNING_HI) """ - _, directives, _ = compile_seq(seq) + _, (directives, _) = compile_seq(seq) push_vals = [d for d in directives if isinstance(d, PushValDirective)] assert len(push_vals) >= 3 # WARNING_HI = 2 @@ -61,7 +61,7 @@ def test_emits_pop_event_directive(self, fprime_test_api): seq = """ log("test") """ - _, directives, _ = compile_seq(seq) + _, (directives, _) = compile_seq(seq) pop_dirs = [d for d in directives if isinstance(d, PopEventDirective)] assert len(pop_dirs) == 1 # message_size should be pushed onto the stack before POP_EVENT @@ -73,7 +73,7 @@ def test_serialization_roundtrip(self, fprime_test_api): seq = """ log("roundtrip test") """ - _, directives, _ = compile_seq(seq) + _, (directives, _) = compile_seq(seq) pop_dirs = [d for d in directives if isinstance(d, PopEventDirective)] assert len(pop_dirs) == 1 diff --git a/test/fpy/test_types_and_constructors.py b/test/fpy/test_types_and_constructors.py index 8c18cef..094385d 100644 --- a/test/fpy/test_types_and_constructors.py +++ b/test/fpy/test_types_and_constructors.py @@ -14,8 +14,8 @@ def _oor_float_to_int(saturated, wrapped): backend: the LLVM/wasm backend saturates at the target width (Rust `as` semantics -- clamp to the target type's min/max), while the bytecode VM saturates at 64 bits and then wrap-truncates to the target width. Reads - test_helpers.USE_WASM at call time (conftest sets it from the --wasm flag).""" - return saturated if test_helpers.USE_WASM else wrapped + test_helpers.BACKEND at call time (conftest sets it from the --wasm flag).""" + return saturated if test_helpers.BACKEND == "wasm" else wrapped class TestEnums: diff --git a/test/fpy/test_warnings.py b/test/fpy/test_warnings.py index 3a31523..59cdd62 100644 --- a/test/fpy/test_warnings.py +++ b/test/fpy/test_warnings.py @@ -72,17 +72,17 @@ class TestIgnoreWarnings: """--ignore silently drops the warning.""" def test_ignore_suppresses_warning(self): - state, _, _ = compile_seq( + state, _ = compile_seq( EMPTY_RANGE_SEQ, ignored_warnings={WarningType.EMPTY_RANGE} ) assert state.warnings == [] def test_ignore_all_suppresses_warning(self): - state, _, _ = compile_seq(EMPTY_RANGE_SEQ, ignored_warnings=set(WarningType)) + state, _ = compile_seq(EMPTY_RANGE_SEQ, ignored_warnings=set(WarningType)) assert state.warnings == [] def test_ignore_unrelated_type_keeps_warning(self): - state, _, _ = compile_seq( + state, _ = compile_seq( EMPTY_RANGE_SEQ, ignored_warnings={WarningType.IMPORT_UNDERSCORE}, expected_warnings={WarningType.EMPTY_RANGE}, @@ -103,7 +103,7 @@ def test_escalated_message_mentions_type(self): def test_unrelated_error_type_does_not_fail(self): # Escalating a different warning type must not affect the empty-range warning. - state, _, _ = compile_seq( + state, _ = compile_seq( EMPTY_RANGE_SEQ, error_warnings={WarningType.IMPORT_UNDERSCORE} ) assert any(w.type == WarningType.EMPTY_RANGE for w in state.warnings) diff --git a/test/fpy/test_wasm.py b/test/fpy/test_wasm.py index 7e3a5ba..fed93b3 100644 --- a/test/fpy/test_wasm.py +++ b/test/fpy/test_wasm.py @@ -37,7 +37,7 @@ from fpy.bytecode.directives import DirectiveErrorCode from fpy.state import get_base_compile_state from fpy.test_helpers import ( - compile_seq_wasm, + compile_seq, default_dictionary, run_seq_wasm, run_seq_wasm_with_cmds, @@ -59,12 +59,6 @@ U64, ) -# Every test in this module drives the LLVM/wasm backend end-to-end. The wasm -# marker makes conftest build the spacewasm runner on demand, so these always -# run on the wasm backend even when --wasm isn't passed. -pytestmark = pytest.mark.wasm - - NO_ERROR = DirectiveErrorCode.NO_ERROR.value EXIT_WITH_ERROR = DirectiveErrorCode.EXIT_WITH_ERROR.value ARRAY_OOB = DirectiveErrorCode.ARRAY_OUT_OF_BOUNDS.value @@ -544,7 +538,7 @@ def test_exponent_emits_pow_import(self): # Document the host-call contract: the linked module imports env.pow. # An import-section entry encodes as module name , so a # function import of env.pow is exactly this byte run. - wasm = compile_seq_wasm("x: F64 = 2.0\nassert x ** 3.0 == 8.0\n") + _, wasm = compile_seq("x: F64 = 2.0\nassert x ** 3.0 == 8.0\n", "wasm") assert b"\x03env\x03pow\x00" in wasm @@ -1056,7 +1050,7 @@ def test_cmd_emits_fprime_cmd_import(self): # Document the host-call contract: the linked module imports # fprime_v1.cmd. An import-section entry encodes as # module name , so this byte run is exactly that entry. - wasm = compile_seq_wasm("CdhCore.cmdDisp.CMD_NO_OP()\n") + _, wasm = compile_seq("CdhCore.cmdDisp.CMD_NO_OP()\n", "wasm") assert b"\x09fprime_v1\x03cmd\x00" in wasm def test_const_no_arg_command(self): diff --git a/test/fpy/test_write_to_port.py b/test/fpy/test_write_to_port.py index d18ac2b..b599cd2 100644 --- a/test/fpy/test_write_to_port.py +++ b/test/fpy/test_write_to_port.py @@ -27,7 +27,7 @@ def pop_dirs(seq: str) -> list[PopSerializableDirective]: - _, directives, _ = compile_seq(seq) + _, (directives, _) = compile_seq(seq) return [d for d in directives if isinstance(d, PopSerializableDirective)]