MoonLive runs on every board: the Xtensa frame fix, and a compiler bounded by memory #221
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |