Skip to content

MoonLive runs on every board: the Xtensa frame fix, and a compiler bounded by memory #221

MoonLive runs on every board: the Xtensa frame fix, and a compiler bounded by memory

MoonLive runs on every board: the Xtensa frame fix, and a compiler bounded by memory #221

Workflow file for this run

name: Test
# Fast host-side unit tests for the Python (MoonDeck / build scripts) and JS
# (web installer) code that the C++ ctest/scenario suites can't reach. Runs on
# every PR and on pushes to main/next-iteration. Scoped to the paths these tests
# cover so a docs-only or pure-firmware change doesn't spend a runner here.
#
# Today this pins the Improv frame wire format (test/python + test/js assert a
# shared golden vector so the device C++, Python, and JS builders can't drift).
# New Python/JS unit suites land under test/python and test/js and run here.
# Paths cover every input to the host-side tests: the Python/JS sources under test
# (scripts, web-installer), the test files themselves, AND the device-side C++ frame
# contract (src/core/Improv*.h + the platform handler) — a wire-format change in the
# firmware must run the cross-language golden-vector tests so it can't drift from the
# Python/JS builders silently. pull_request gates every PR; push runs main only (a
# direct-to-main hotfix). A PR branch is covered by pull_request alone — listing
# feature branches under push too would double-run every PR (push + pull_request).
on:
pull_request:
paths: &test-paths
- 'moondeck/**'
- 'web-installer/**'
- 'src/core/ImprovFrame.h'
- 'src/core/ImprovOpReassembler.h'
- 'src/platform/esp32/platform_esp32_improv.cpp'
- 'test/python/**'
- 'test/js/**'
- '.github/workflows/test.yml'
push:
branches:
- main
paths: *test-paths
permissions:
contents: read
jobs:
python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v3
# pytest + pyserial come from the test file's inline PEP-723 block; passing
# them via --with is the explicit, discovery-friendly form (a bare `pytest
# <dir>` doesn't honour a test file's own inline deps). `markdown` (a MkDocs
# dep, not in the base env) is needed by test_mkdocs_slug.py, which pins _slug()
# against Python-Markdown's real toc slugify. `wled` is the frenck/python-wled
# library HA's WLED integration uses; test_wled_json_shape.py parses the
# device /json vector through its Device.from_dict to pin the wire contract.
- name: pytest
run: uv run --with pytest --with pyserial --with markdown --with wled pytest test/python -q
js:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# Node's built-in test runner — no npm install, no package.json. The glob
# form is required: a bare directory arg is treated as a module to execute.
- name: node --test
run: node --test "test/js/**/*.test.mjs"
# Sanitizers — the safety net for the multicore render/encode split. A second thread now sits
# beside code that was single-threaded for its whole life: a shared output buffer handed between
# cores, an atomic stop flag, a try-lock over the WebSocket sender, and a child array that can be
# reallocated while the worker walks it. Hand-written tests prove the races we THOUGHT of; ASan
# (use-after-free) and TSan (data races) find the ones we didn't — ASan is what would have caught
# the delete-a-driver-mid-encode bug automatically.
#
# Linux-only, and that is not laziness: on macOS/arm64 Apple's ASan runtime HANGS at startup and
# TSan SEGFAULTS on a hello-world (both reproduced with minimal programs — toolchain bugs, not our
# code). Linux is the only place TSan runs at all, which makes this job the sole race gate.
# Locally: `uv run moondeck/check/check_sanitizers.py` (uses Homebrew LLVM, ASan only on macOS).
sanitizers:
runs-on: ubuntu-latest
strategy:
fail-fast: false # a TSan race and an ASan UAF are independent signals — report both
matrix:
# `realtime` is the runtime half of the hot-path check: MM_NONBLOCKING marks the tick
# methods, -Wfunction-effects proves what it can at compile time, and RTSan catches what
# it cannot — allocation or blocking reached through virtual dispatch or a function
# pointer. Same attribute drives both, so this lane costs one matrix entry.
kind: [address, thread, realtime]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# The CMake configure needs uv on PATH — it resolves UV_EXECUTABLE for the UI-embed step
# (CLAUDE.md § Use uv for every Python invocation), so a build without it fails at configure.
- uses: astral-sh/setup-uv@v3
# RTSan needs Clang 20+; ubuntu-latest ships Clang 18, which rejects
# `-fsanitize=realtime` at the compiler-probe stage. Install a new enough one for that
# lane only — ASan/TSan run on the runner's default compiler.
- name: install clang for RealtimeSanitizer
if: matrix.kind == 'realtime'
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/llvm.asc
sudo add-apt-repository -y "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-20 main"
sudo apt-get install -y clang-20
- name: build + run unit tests under ${{ matrix.kind }}sanitizer
run: |
# RTSan is clang-only — GCC rejects `-fsanitize=realtime` outright — and the attribute
# it keys off is [[clang::nonblocking]], which GCC ignores anyway. Passed as a CMake
# variable on this lane only, rather than exporting CXX: an empty CXX="" on the other
# lanes is not the same as unset, and some probes read it as a broken compiler path.
compiler=""
if [ "${{ matrix.kind }}" = "realtime" ]; then
compiler="-DCMAKE_CXX_COMPILER=clang++-20"
fi
cmake -S . -B build/san -DCMAKE_BUILD_TYPE=Debug $compiler \
-DCMAKE_CXX_FLAGS="-fsanitize=${{ matrix.kind }} -fno-omit-frame-pointer -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=${{ matrix.kind }}"
cmake --build build/san --target mm_tests -j"$(nproc)"
# Leak detection is a different concern from the races/UAF this gate hunts, and it fires on
# third-party static init; keep the signal on what we're actually looking for.
#
# RTSan does NOT halt: the render path's known blocking calls are frozen in
# docs/metrics/hotpath-baseline.txt and backlogged as architecture work, so halting would
# fail this lane on every run. It reports; the log is the signal.
ASAN_OPTIONS=detect_leaks=0 TSAN_OPTIONS=halt_on_error=1 \
RTSAN_OPTIONS=halt_on_error=0 ./build/san/test/mm_tests