Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/excludes.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pyproject.toml
setup.py
^uv\.lock$
^test/harness/config/
ignore$
/html/
(?:^|/)(?i)COPYRIGHT
Expand Down
4 changes: 2 additions & 2 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ ctx
cuz
dedenting
Deframer
deinit
Deser
downcasted
downcasting
Expand Down Expand Up @@ -84,12 +85,11 @@ itrunc
kinda
lalr
libcall
libm
llo
LOGACTIVITYHI
LOGACTIVITYLO
LOGCOMMAND
LOGDIAGNOSTIC
LOGFATAL
LOGWARNINGHI
LOGWARNINGLO
lvar
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/fprime-gds-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ jobs:
steps:
- uses: actions/checkout@v4
# The tests run sequences on the real Svc::FpySequencer and
# Svc::WasmSequencer, through C++ harnesses built from these two
# checkouts of fprime. The harnesses are built by the test session
# itself; see "Running tests" in the README.
- name: Check out the fprime submodules
run: git submodule update --init test/fprime test/fprime-wasm
# Svc::WasmSequencer, through C++ harnesses built from this checkout
# of fprime. The harnesses are built by the test session itself; see
# "Running tests" in the README.
- name: Check out the fprime submodule
run: git submodule update --init test/fprime
- name: Set up uv and Python ${{ matrix.python-version }}
uses: astral-sh/setup-uv@v5
with:
Expand Down
5 changes: 0 additions & 5 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,3 @@
path = test/fprime
url = https://github.com/nasa/fprime.git
shallow = true
[submodule "test/fprime-wasm"]
path = test/fprime-wasm
url = https://github.com/Kronos3/fprime.git
branch = wasm
shallow = true
25 changes: 11 additions & 14 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
####
# FpyHarness:
# Fpy test harnesses:
#
# The test harness executable that runs compiled fpy sequences on the real
# Svc::FpySequencer. The pytest suite starts this program and talks to it over
# stdin/stdout; see test/harness/README.md.
# The test harness executables that run compiled sequences on the real
# sequencers of the fprime submodule: FpySeqHarness runs fpy bytecode on
# Svc::FpySequencer, WasmSeqHarness runs wasm modules on Svc::WasmSequencer.
# The pytest suite starts them and talks to them over stdin/stdout; see
# test/harness/Harness.hpp.
#
# Build with the tools from the `harness` dependency group:
# uv run fprime-util generate
# uv run fprime-util build
# uv run fprime-util build --target FpySeqHarness
# uv run fprime-util build --target WasmSeqHarness
####

cmake_minimum_required(VERSION 3.18)
cmake_policy(SET CMP0048 NEW)
project(FpyHarness VERSION 1.0.0 LANGUAGES C CXX)
project(FpyHarnesses VERSION 1.0.0 LANGUAGES C CXX)

# The harness wires ports together at runtime, which direct port calls
# The harnesses wire ports together at runtime, which direct port calls
# (the default outside unit-test builds) compile away.
add_compile_definitions(FW_DIRECT_PORT_CALLS=0)

include("${FPRIME_FRAMEWORK_PATH}/cmake/FPrime.cmake")
include("${FPRIME_FRAMEWORK_PATH}/cmake/FPrime-Code.cmake")

add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/test/harness/config")
add_fprime_subdirectory("${CMAKE_CURRENT_LIST_DIR}/test/harness")

register_fprime_deployment(
SOURCES
"${CMAKE_CURRENT_LIST_DIR}/test/harness/FpyHarnessMain.cpp"
DEPENDS
test_harness
)
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ log("hello world!") # creates an ACTIVITY_HI event with the text "hello world!"
You can configure the severity of the event:
```py
log("uh oh", Fw.LogSeverity.WARNING_HI)
log("oh no!", Fw.LogSeverity.FATAL)
log("all clear", Fw.LogSeverity.DIAGNOSTIC)
```

All F Prime severity levels are supported.
All F Prime severity levels are supported except `FATAL` and `COMMAND`, which are reserved for the flight software.

At the moment, only constant string arguments are supported. See [Strings](#strings).

Expand Down Expand Up @@ -749,11 +749,11 @@ Some tests compile and run sequences. Those tests will generate both Fpy bytecod

To run these tests, the following steps are required:

* The submodules must be checked out: `git submodule update --init test/fprime test/fprime-wasm`
* The fprime submodule must be checked out: `git submodule update --init test/fprime`
* The harness build tools (cmake, ninja, fprime-util, fpp) and the `wasm` extra. `uv sync` installs them as part of the dev environment; with pip the extra is `pip install -e '.[wasm]'`.
* A Rust toolchain (the wasm sequencer builds the spacewasm interpreter with cargo). Install via [rustup](https://rustup.rs).

Each harness is built automatically when the first test that needs it runs. The wasm harness build applies a small local patch to the submodule first (see `test/harness/patches/README.md`).
Each harness is built automatically when the first test that needs it runs.

### `--backend`

Expand Down
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,14 @@ fprime-fpy-disasm = "fpy.main:disassemble_main"
fprime-fpy-cmd = "fpy.main:cmd_main"

####
# The test suite lives in test/fpy. The fprime submodules and the harness
# The test suite lives in test/fpy. The fprime submodule and the harness
# build trees carry their own (huge) test collections, so keep pytest from
# ever recursing into them.
####
[tool.pytest.ini_options]
testpaths = ["test/fpy"]
norecursedirs = [
"fprime",
"fprime-wasm",
"harness",
"build-*",
".*",
Expand All @@ -95,7 +94,7 @@ norecursedirs = [
####
# The test suite covers both backends, so a development environment picks up
# the wasm extra without having to ask for it. The harness group holds the
# tools that build the C++ test harness from the fprime submodules; the pins
# tools that build the C++ test harnesses from the fprime submodule; the pins
# match test/fprime/requirements.txt.
####
[dependency-groups]
Expand All @@ -107,8 +106,8 @@ dev = [
]
harness = [
"cmake==3.26.0",
"fprime-fpp==3.3.0",
"fprime-tools==4.3.0",
"fprime-fpp==3.4.0",
"fprime-tools==4.4.0",
"ninja==1.11.1.4",
]

Expand Down
2 changes: 2 additions & 0 deletions src/fpy/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
CollectUsedFunctions,
ResolveTransitiveGlobalUses,
CheckGlobalsInitializedBeforeCall,
CheckLogSeverities,
CheckSequenceArgs,
DefineFunctions,
DefineVariables,
Expand Down Expand Up @@ -294,6 +295,7 @@ def analyze_ast(body: AstBlock, state: CompileState) -> CompileState:
CheckConstArrayAccesses(),
WarnRangesAreNotEmpty(),
CheckSequenceArgs(),
CheckLogSeverities(),
# now that semantic analysis is done, we can desugar things.
# Fill in default arguments before desugaring for loops
DesugarDefaultArgs(),
Expand Down
93 changes: 30 additions & 63 deletions src/fpy/harness.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Client for the C++ test harness that runs sequences on the real
Svc::FpySequencer (see test/harness). The harness program is started once and
reused: each request is one JSON line on its stdin, each reply one JSON line
on its stdout, and every request runs on a brand-new sequencer instance
inside the harness."""
"""Client for the C++ test harnesses that run sequences on the real
Svc::FpySequencer and Svc::WasmSequencer (see test/harness). Each harness
program is started once and reused: each request is one JSON line on its
stdin, each reply one JSON line on its stdout, and every request runs on a
brand-new sequencer instance inside the harness."""

from __future__ import annotations

Expand All @@ -12,80 +12,46 @@
from pathlib import Path

REPO_ROOT = Path(__file__).parent.parent.parent
FPY_HARNESS_BINARY = (
REPO_ROOT / "build-artifacts" / "Linux" / "FpyHarness" / "bin" / "FpyHarness"
)
_WASM_HARNESS_ROOT = REPO_ROOT / "test" / "harness" / "wasm"
WASM_HARNESS_BINARY = (
_WASM_HARNESS_ROOT
/ "build-artifacts"
/ "Linux"
/ "WasmSeqHarness"
/ "bin"
/ "WasmSeqHarness"
)
_BUILD_ARTIFACTS = REPO_ROOT / "build-artifacts" / "Linux"
FPY_HARNESS_BINARY = _BUILD_ARTIFACTS / "FpySeqHarness" / "bin" / "FpySeqHarness"
WASM_HARNESS_BINARY = _BUILD_ARTIFACTS / "WasmSeqHarness" / "bin" / "WasmSeqHarness"


class HarnessError(Exception):
"""The harness itself failed: it could not run the sequence at all, gave
a malformed reply, or crashed. Distinct from a sequence failing."""


def build_harness() -> None:
def build_fpy_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():
raise HarnessError(
"the fprime submodule is not checked out. Run:\n"
" git submodule update --init test/fprime"
)
_fprime_util_build(REPO_ROOT)
_build_deployment("FpySeqHarness")


def build_wasm_harness() -> None:
"""Builds the WasmSequencer harness executable from the fprime-wasm
submodule, with the exit-code patch applied (test/harness/patches)."""
if not (REPO_ROOT / "test" / "fprime-wasm" / "CMakeLists.txt").exists():
raise HarnessError(
"the fprime-wasm submodule is not checked out. Run:\n"
" git submodule update --init test/fprime-wasm"
)
_apply_wasm_patch()
_fprime_util_build(_WASM_HARNESS_ROOT, build_args=["--all"])


def _apply_wasm_patch() -> None:
"""Applies the local WasmSequencer fixes to the fprime-wasm submodule if
they are not applied yet (see test/harness/patches/README.md)."""
patch = REPO_ROOT / "test" / "harness" / "patches" / "wasm-sequencer-fixes.patch"
submodule = REPO_ROOT / "test" / "fprime-wasm"
applied = subprocess.run(
["git", "apply", "--check", "--reverse", str(patch)],
cwd=submodule,
capture_output=True,
)
if applied.returncode == 0:
return
result = subprocess.run(
["git", "apply", str(patch)], cwd=submodule, capture_output=True, text=True
)
if result.returncode != 0:
raise HarnessError(
f"could not apply {patch.name} to test/fprime-wasm:\n{result.stderr}"
)
"""Builds the WasmSequencer harness executable from the fprime submodule.
The build is incremental, so this is cheap when nothing changed."""
_build_deployment("WasmSeqHarness")


def _fprime_util_build(deployment: Path, build_args: list[str] = None) -> None:
if not (deployment / "build-fprime-automatic-native").exists():
_run_fprime_util(deployment, ["generate"])
_run_fprime_util(deployment, ["build"] + (build_args or []))
def _build_deployment(target: str) -> None:
"""Builds and installs one of the harness deployments (see the root
CMakeLists.txt), generating the shared build cache first if needed."""
if not (REPO_ROOT / "test" / "fprime" / "CMakeLists.txt").exists():
raise HarnessError(
"the fprime submodule is not checked out. Run:\n"
" git submodule update --init test/fprime"
)
if not (REPO_ROOT / "build-fprime-automatic-native").exists():
_run_fprime_util(["generate"])
_run_fprime_util(["build", "--target", target])


def _run_fprime_util(deployment: Path, args: list[str]) -> None:
def _run_fprime_util(args: list[str]) -> None:
try:
result = subprocess.run(
["fprime-util"] + args,
cwd=deployment,
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
Expand All @@ -96,7 +62,7 @@ def _run_fprime_util(deployment: Path, args: list[str]) -> None:
)
if result.returncode != 0:
raise HarnessError(
f"'fprime-util {' '.join(args)}' in {deployment} failed. Are the "
f"'fprime-util {' '.join(args)}' failed. Are the "
"harness build tools installed (uv sync installs them)?\n"
+ result.stdout[-2000:]
+ result.stderr[-2000:]
Expand Down Expand Up @@ -134,7 +100,8 @@ 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 with "
"build_fpy_harness() or build_wasm_harness()"
)
self._stderr_file = tempfile.TemporaryFile(mode="w+")
self._process = subprocess.Popen(
Expand Down Expand Up @@ -176,7 +143,7 @@ def fpy_harness() -> SequencerHarness:
raise _fpy_build_error
if _fpy_harness is None:
try:
build_harness()
build_fpy_harness()
except HarnessError as e:
_fpy_build_error = e
raise
Expand Down
42 changes: 24 additions & 18 deletions src/fpy/macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,29 @@ def generate_write_to_port_llvm(builder, args):
const_arg_indices=frozenset({0, 1, 2}),
)

LOG_MACRO = BuiltinFuncSymbol(
"log",
NOTHING,
[
("message", INTERNAL_STRING, None),
("severity", LOG_SEVERITY, FpyValue(LOG_SEVERITY, "ACTIVITY_HI")),
],
lambda n, c, t: [
PushValDirective(c[1].serialize()),
PushValDirective(c[0].val.encode("utf-8")),
PushValDirective(
FpyValue(StackSizeType, len(c[0].val.encode("utf-8"))).serialize()
),
PopEventDirective(),
],
generate_log_event_llvm,
const_arg_indices=frozenset({0, 1}),
)

# The Fw.LogSeverity values a sequence may not log at: FATAL and COMMAND are
# reserved for the flight software itself.
RESERVED_LOG_SEVERITIES = frozenset({"FATAL", "COMMAND"})

MACROS: dict[str, BuiltinFuncSymbol] = {
"sleep": MACRO_SLEEP_SECONDS_USECONDS,
"sleep_until": BuiltinFuncSymbol(
Expand Down Expand Up @@ -367,24 +390,7 @@ def generate_write_to_port_llvm(builder, args):
# The generate function should never be called since this is always const-evaluated
"time": TIME_MACRO,
# Event logging builtin — compile-time string + severity, defaults to ACTIVITY_HI
"log": BuiltinFuncSymbol(
"log",
NOTHING,
[
("message", INTERNAL_STRING, None),
("severity", LOG_SEVERITY, FpyValue(LOG_SEVERITY, "ACTIVITY_HI")),
],
lambda n, c, t: [
PushValDirective(c[1].serialize()),
PushValDirective(c[0].val.encode("utf-8")),
PushValDirective(
FpyValue(StackSizeType, len(c[0].val.encode("utf-8"))).serialize()
),
PopEventDirective(),
],
generate_log_event_llvm,
const_arg_indices=frozenset({0, 1}),
),
"log": LOG_MACRO,
# Serial write: port typed by the dictionary-backed Svc.Fpy.SerialPortIndex enum; value typed SIZED
"write_to_port": BuiltinFuncSymbol(
"write_to_port",
Expand Down
Loading
Loading