Fix first-run onboarding: README/wiki quickstart + StatusWidget MonitorTag crash #672
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: Tests | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| schedule: | |
| # Nightly full-suite run. PRs use path-filtered subsets (see `changes` job); | |
| # nightly runs everything so cross-library regressions surface within 24h. | |
| - cron: '0 6 * * *' | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # Two-tier CI: PRs run only the jobs whose inputs changed, push-to-main and | |
| # the nightly schedule run the full suite. Within MATLAB/Octave jobs the | |
| # `test_pattern` output narrows the test set further when only downstream | |
| # libs (Dashboard, EventDetection, WebBridge) changed. Foundational libs | |
| # (FastSense, SensorThreshold), test infrastructure, and `core` paths | |
| # (install.m, MEX C sources, workflows themselves, lint config, the test | |
| # runner, the coverage script) all force a full run on PRs as a safety net. | |
| changes: | |
| name: Detect changed paths | |
| runs-on: ubuntu-latest | |
| outputs: | |
| core: ${{ steps.filter.outputs.core }} | |
| matlab: ${{ steps.filter.outputs.matlab }} | |
| mex: ${{ steps.filter.outputs.mex }} | |
| concurrency: ${{ steps.filter.outputs.concurrency }} | |
| test_pattern: ${{ steps.test_pattern.outputs.pattern }} | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: dorny/paths-filter@v4 | |
| id: filter | |
| with: | |
| filters: | | |
| core: | |
| - 'install.m' | |
| - 'libs/FastSense/private/mex_src/**' | |
| - 'libs/FastSense/private/.mex-version' | |
| - '.github/workflows/**' | |
| - 'miss_hit.cfg' | |
| - 'tests/run_all_tests.m' | |
| - 'scripts/run_tests_with_coverage.m' | |
| matlab: | |
| - 'libs/**' | |
| - 'tests/**' | |
| - 'examples/**' | |
| - 'scripts/**.m' | |
| - 'install.m' | |
| - 'run_profile.m' | |
| mex: | |
| - 'libs/**/mex_src/**' | |
| - 'libs/**/build_mex.m' | |
| - 'libs/FastSense/mksqlite.c' | |
| - 'libs/FastSense/private/.mex-version' | |
| fastsense: | |
| - 'libs/FastSense/**' | |
| sensorthreshold: | |
| - 'libs/SensorThreshold/**' | |
| eventdetection: | |
| - 'libs/EventDetection/**' | |
| dashboard: | |
| - 'libs/Dashboard/**' | |
| webbridge: | |
| - 'libs/WebBridge/**' | |
| tests: | |
| - 'tests/**' | |
| examples: | |
| - 'examples/**' | |
| # v4.0 Multi-User LAN Concurrency — files whose behaviour is | |
| # platform-divergent at the kernel / filesystem level. Triggers | |
| # the cross-OS smoke job (matlab-concurrency-smoke) which runs | |
| # only on this subset to catch regressions on Linux (OFD locks), | |
| # macOS (F_SETLK fallback), and Windows (LockFileEx). The full | |
| # MATLAB test suite still runs Linux-only via the `matlab:` job. | |
| concurrency: | |
| - 'libs/Concurrency/**' | |
| - 'libs/EventDetection/**' | |
| - 'libs/SensorThreshold/MonitorTag.m' | |
| - 'libs/SensorThreshold/LiveTagPipeline.m' | |
| - 'libs/FastSenseCompanion/FastSenseCompanion.m' | |
| - 'libs/FastSenseCompanion/private/companionDiscoverEventStore.m' | |
| - 'tests/suite/TestFileLock*.m' | |
| - 'tests/suite/TestLockfileMex.m' | |
| - 'tests/suite/TestAtomicWriter.m' | |
| - 'tests/suite/TestCluster*.m' | |
| - 'tests/suite/TestTagWriteCoordinator.m' | |
| - 'tests/suite/TestEventLog*.m' | |
| - 'tests/suite/TestConcurrencyIntegration.m' | |
| - 'tests/suite/TestListenerCannotAcquireLock.m' | |
| - 'tests/suite/TestMonitorTagSingleSource.m' | |
| - 'tests/suite/TestLiveTagPipelineCluster.m' | |
| - 'tests/suite/TestShareLossRecovery.m' | |
| - 'tests/suite/TestEventAcknowledgement.m' | |
| - 'tests/test_ndjson_decode.m' | |
| - 'tests/test_event_log_concurrent.m' | |
| - 'tests/test_user_identity.m' | |
| - 'tests/test_no_raw_save_to_shared.m' | |
| - 'tests/test_mksqlite_extended_codes_probe.m' | |
| - name: Compute test pattern | |
| id: test_pattern | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| F_CORE: ${{ steps.filter.outputs.core }} | |
| F_MEX: ${{ steps.filter.outputs.mex }} | |
| F_FASTSENSE: ${{ steps.filter.outputs.fastsense }} | |
| F_SENSORTHRESHOLD: ${{ steps.filter.outputs.sensorthreshold }} | |
| F_EVENTDETECTION: ${{ steps.filter.outputs.eventdetection }} | |
| F_DASHBOARD: ${{ steps.filter.outputs.dashboard }} | |
| F_WEBBRIDGE: ${{ steps.filter.outputs.webbridge }} | |
| F_TESTS: ${{ steps.filter.outputs.tests }} | |
| F_EXAMPLES: ${{ steps.filter.outputs.examples }} | |
| run: | | |
| # Non-PR events (push, schedule, manual) always run the full suite. | |
| # On PRs, foundational lib changes or any safety-net trigger also | |
| # force the full suite. Only when the change is confined to the | |
| # downstream libs (Dashboard/EventDetection/WebBridge) do we narrow. | |
| if [ "$EVENT_NAME" != "pull_request" ] \ | |
| || [ "$F_CORE" = "true" ] || [ "$F_MEX" = "true" ] \ | |
| || [ "$F_FASTSENSE" = "true" ] || [ "$F_SENSORTHRESHOLD" = "true" ] \ | |
| || [ "$F_TESTS" = "true" ] || [ "$F_EXAMPLES" = "true" ]; then | |
| echo "pattern=" >> "$GITHUB_OUTPUT" | |
| echo "Full suite (no narrowing)." | |
| exit 0 | |
| fi | |
| # Conservative downstream bundles per the dependency graph: | |
| # WebBridge ← only WebBridge tests | |
| # Dashboard ← Dashboard + WebBridge tests | |
| # EventDetection ← EventDetection + Dashboard + WebBridge tests | |
| patterns=() | |
| if [ "$F_EVENTDETECTION" = "true" ]; then | |
| patterns+=("Event" "Notification" "DataSource" "MatFile" "Mock" "Live.*Pipeline" "Incremental") | |
| fi | |
| if [ "$F_EVENTDETECTION" = "true" ] || [ "$F_DASHBOARD" = "true" ]; then | |
| patterns+=("Dashboard" "Widget" "Toolbar" "InfoTooltip" "MarkdownRenderer") | |
| fi | |
| if [ "$F_EVENTDETECTION" = "true" ] || [ "$F_DASHBOARD" = "true" ] || [ "$F_WEBBRIDGE" = "true" ]; then | |
| patterns+=("WebBridge") | |
| fi | |
| if [ "${#patterns[@]}" -eq 0 ]; then | |
| echo "pattern=" >> "$GITHUB_OUTPUT" | |
| echo "No matlab paths matched; full suite (jobs may still skip via job-level if:)." | |
| exit 0 | |
| fi | |
| IFS='|' | |
| pattern="${patterns[*]}" | |
| echo "pattern=${pattern}" >> "$GITHUB_OUTPUT" | |
| echo "Narrowed test pattern: ${pattern}" | |
| lint: | |
| name: MATLAB Lint | |
| needs: changes | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.matlab == 'true' || needs.changes.outputs.core == 'true' | |
| timeout-minutes: 10 | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install MISS_HIT | |
| run: pip install miss_hit | |
| - name: Run style checker | |
| run: mh_style libs/ tests/ examples/ | |
| - name: Run linter | |
| run: mh_lint libs/ tests/ examples/ | |
| - name: Run complexity metrics | |
| run: mh_metric --ci libs/ tests/ examples/ | |
| build-mex: | |
| name: Build MEX (Linux) | |
| needs: changes | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.matlab == 'true' || needs.changes.outputs.core == 'true' || needs.changes.outputs.mex == 'true' | |
| uses: ./.github/workflows/_build-mex-octave.yml | |
| with: | |
| artifact-name: mex-linux | |
| build-mex-matlab: | |
| name: Build MEX (MATLAB Linux) | |
| needs: changes | |
| timeout-minutes: 30 | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.matlab == 'true' || needs.changes.outputs.core == 'true' || needs.changes.outputs.mex == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup MATLAB | |
| uses: matlab-actions/setup-matlab@v3 | |
| with: | |
| # Bumped from R2020b in PR #109 — R2020b's libmex.so chronically | |
| # segfaults on the GitHub xvfb runner during widget-heavy tests | |
| # (TestDashboardInfo web(), TestDemoIndustrialPlant*, | |
| # TestFastSenseWidgetUpdate). Codebase already targets R2021a+ | |
| # (uifigure features, BorderColor, Placeholder, WordWrap). | |
| release: R2021b | |
| cache: true | |
| - name: Cache MATLAB MEX binaries | |
| id: cache-mex-matlab | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| libs/FastSense/private/*.mexa64 | |
| libs/SensorThreshold/private/*.mexa64 | |
| libs/FastSense/mksqlite.mexa64 | |
| # v4.0 Phase 1029: lockfile_mex (MEX) lives in libs/Concurrency/ root | |
| # (NOT private/ — MATLAB classdef files can't access sibling private/). | |
| libs/Concurrency/*.mexa64 | |
| # Cache key includes Concurrency MEX sources + build helper so cache | |
| # invalidates when those change, not just FastSense MEX inputs. | |
| key: mex-matlab-linux-r2021b-${{ hashFiles('libs/FastSense/private/mex_src/**', 'libs/FastSense/build_mex.m', 'libs/FastSense/private/.mex-version', 'libs/Concurrency/private/mex_src/**', 'libs/Concurrency/build_concurrency_mex.m') }} | |
| - name: Compile MEX files (MATLAB) | |
| if: steps.cache-mex-matlab.outputs.cache-hit != 'true' | |
| uses: matlab-actions/run-command@v3 | |
| with: | |
| command: "install();" | |
| - name: Diagnose mksqlite build output | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| echo "=== build-mex-matlab: post-compile mksqlite diagnostic ===" | |
| echo "--- libs/FastSense/mksqlite.* ---" | |
| ls -la libs/FastSense/mksqlite.* 2>&1 || echo "(no mksqlite files in libs/FastSense/)" | |
| echo "--- libs/FastSense/private/*.mexa64 ---" | |
| ls -la libs/FastSense/private/*.mexa64 2>&1 || echo "(no mexa64 in private)" | |
| echo "--- mksqlite source ---" | |
| ls -la libs/FastSense/mksqlite.c 2>&1 || echo "(no mksqlite.c)" | |
| - name: Upload MATLAB MEX artifacts | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: mex-matlab-linux | |
| path: | | |
| libs/FastSense/private/*.mexa64 | |
| libs/SensorThreshold/private/*.mexa64 | |
| libs/FastSense/mksqlite.mexa64 | |
| # v4.0 Phase 1029: lockfile_mex.mexa64 lives in libs/Concurrency/ root | |
| # (the mksqlite pattern: external callers can't reach private/). | |
| # Without this, the matlab: batches fail with "lockfile_mex not on | |
| # path after install()" -> cascading test failures in | |
| # TestConcurrencyIntegration, TestFileLock, TestLockfileMex, etc. | |
| libs/Concurrency/*.mexa64 | |
| retention-days: 1 | |
| octave: | |
| name: Octave Tests | |
| timeout-minutes: 45 | |
| needs: [changes, build-mex] | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.matlab == 'true' || needs.changes.outputs.core == 'true' || needs.changes.outputs.mex == 'true' | |
| runs-on: ubuntu-latest | |
| container: gnuoctave/octave:11.1.0 | |
| env: | |
| FASTSENSE_SKIP_BUILD: "1" | |
| FASTSENSE_RESULTS_FILE: /tmp/test-results.txt | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Download MEX binaries | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: mex-linux | |
| - name: Run tests | |
| env: | |
| TEST_PATTERN: ${{ needs.changes.outputs.test_pattern }} | |
| run: | | |
| # Writes /tmp/test-results.txt for the downstream "Write test summary" step. | |
| # Octave 11.1.0 fixed the break_closure_cycles GC crash (upstream bug #67749) | |
| # that plagued 8.x-10.x, so the old `|| true` workaround dance is gone. | |
| # TEST_PATTERN is empty for full runs (push/schedule/foundational PR | |
| # changes) and a regex for narrowed downstream-only PR changes. | |
| if [ -n "$TEST_PATTERN" ]; then | |
| echo "Running narrowed Octave suite: $TEST_PATTERN" | |
| fi | |
| xvfb-run octave --eval " | |
| cd('tests'); | |
| r = run_all_tests('${TEST_PATTERN}'); | |
| fid = fopen('/tmp/test-results.txt', 'w'); | |
| fprintf(fid, '%d %d\n', r.passed, r.failed); | |
| fclose(fid); | |
| exit(double(r.failed > 0)); | |
| " | |
| - name: Write test summary | |
| if: always() | |
| shell: bash | |
| run: | | |
| if [ -f /tmp/test-results.txt ]; then | |
| read PASSED FAILED < /tmp/test-results.txt | |
| { | |
| echo "### Octave Tests" | |
| echo "" | |
| echo "- Passed: ${PASSED:-0}" | |
| echo "- Failed: ${FAILED:-0}" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "### Octave Tests" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "_Results file not produced (test job likely crashed before completion)._" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # Phase 1028 D-06: smoke-run the 1000-tag harness on every push so | |
| # syntax/regressions in the harness itself surface within the test job | |
| # (the gated full bench runs separately in benchmark.yml). | |
| - name: Phase 1028 harness smoke | |
| run: | | |
| xvfb-run octave --eval "addpath(pwd); install(); bench_tag_pipeline_1k('--smoke');" | |
| # Smoke test: ensure Octave MEX sources still compile on macOS. | |
| # Authoritative prebuilt binaries for releases come from refresh-mex-binaries.yml, | |
| # which commits platform-specific binaries into libs/.../octave-<platform>/. | |
| # This job catches PRs that break the compile path without shipping artifacts. | |
| mex-build-macos: | |
| name: MEX Build (macOS ${{ matrix.os == 'macos-latest' && 'ARM64' || 'x64' }}) | |
| needs: changes | |
| timeout-minutes: 20 | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.mex == 'true' || needs.changes.outputs.core == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-latest] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Octave | |
| run: brew install octave | |
| - name: Compile MEX files | |
| run: octave --eval "install(); fprintf('All MEX files compiled successfully on %s (%s)\n', computer(), getenv('RUNNER_OS'));" | |
| - name: List compiled MEX binaries | |
| run: find libs -name '*.mex*' -type f | sort | |
| # Smoke test: ensure Octave MEX sources still compile on Windows. | |
| # Authoritative prebuilt binaries for releases come from refresh-mex-binaries.yml, | |
| # which commits platform-specific binaries into libs/.../octave-<platform>/. | |
| # This job catches PRs that break the compile path without shipping artifacts. | |
| mex-build-windows: | |
| name: MEX Build (Windows ${{ matrix.os == 'windows-latest' && 'latest' || '2022' }}) | |
| needs: changes | |
| timeout-minutes: 30 | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.mex == 'true' || needs.changes.outputs.core == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [windows-2022, windows-latest] | |
| steps: | |
| - name: Allow paths with colons (wiki files) | |
| run: git config --global core.protectNTFS false | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| libs | |
| tests | |
| benchmarks | |
| examples | |
| install.m | |
| sparse-checkout-cone-mode: false | |
| - name: Install Octave | |
| shell: pwsh | |
| run: | | |
| # Try Chocolatey first | |
| choco install octave.portable --yes --version=9.2.0 --no-progress 2>$null | |
| $octExe = Get-ChildItem -Path "C:\ProgramData\chocolatey\lib\octave.portable\tools" -Recurse -Filter "octave-cli.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if ($octExe) { | |
| Write-Host "Octave installed via Chocolatey at: $($octExe.FullName)" | |
| echo "OCTAVE_EXE=$($octExe.FullName)" >> $env:GITHUB_ENV | |
| exit 0 | |
| } | |
| # Fallback: direct download from mirror | |
| $LASTEXITCODE = 0 | |
| $global:LASTEXITCODE = 0 | |
| Write-Host "::warning::Chocolatey install failed (ftp.gnu.org may be down). Downloading from mirror..." | |
| $url = "https://mirrors.kernel.org/gnu/octave/windows/octave-9.2.0-w64.zip" | |
| $zip = "$env:TEMP\octave.zip" | |
| $dest = "C:\octave" | |
| Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing | |
| Expand-Archive -Path $zip -DestinationPath $dest -Force | |
| $octExe = Get-ChildItem -Path $dest -Recurse -Filter "octave-cli.exe" | Select-Object -First 1 | |
| if (-not $octExe) { | |
| Write-Error "Failed to find octave-cli.exe after direct download" | |
| exit 1 | |
| } | |
| Write-Host "Octave installed via direct download at: $($octExe.FullName)" | |
| echo "OCTAVE_EXE=$($octExe.FullName)" >> $env:GITHUB_ENV | |
| timeout-minutes: 15 | |
| - name: Compile MEX files | |
| shell: pwsh | |
| run: | | |
| $octExe = $env:OCTAVE_EXE | |
| if (-not $octExe -or -not (Test-Path $octExe)) { | |
| Write-Error "OCTAVE_EXE not set or not found: $octExe" | |
| exit 1 | |
| } | |
| Write-Host "Using Octave at: $octExe" | |
| & $octExe --version | |
| & $octExe --no-window-system --eval "install(); fprintf('All MEX files compiled successfully on %s\n', computer());" | |
| - name: List compiled MEX binaries | |
| shell: pwsh | |
| run: Get-ChildItem -Recurse libs -Filter '*.mex' | ForEach-Object { $_.FullName } | |
| matlab: | |
| # Split into 4 alphabetical batches so each MATLAB process sees a | |
| # bounded test load. Background: R2021b headless Linux suffers | |
| # cumulative state corruption (libmwm_dispatcher / libmwlxeindexing | |
| # internals) after the long test suite runs in one process, causing | |
| # segfaults in unrelated tests late in the run. Running each batch | |
| # in a fresh MATLAB process resets the state between groups. | |
| # Batches are matrix-parallel so wall-clock time is similar to the | |
| # old single-process job. | |
| name: MATLAB Tests (${{ matrix.batch.name }}) | |
| needs: [changes, build-mex-matlab] | |
| timeout-minutes: 25 | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.matlab == 'true' || needs.changes.outputs.core == 'true' || needs.changes.outputs.mex == 'true' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Batch budget: empirically ~30 classes per fresh MATLAB process | |
| # on R2021b headless Linux before the dispatcher state corrupts. | |
| # TestDashboard* alone is 23 classes (heaviest cluster), so it | |
| # gets its own batch; the rest of D folds into batch 1. | |
| batch: | |
| # A-D batch: excludes TestConcurrencyIntegration (moved to batch 6) — | |
| # it triggers an R2021b cumulative-state segfault when run after the | |
| # ~17 widget/render tests that precede it in this alphabetical range. | |
| - { id: 1, name: "A-D", pattern: "^Test[AB]|^TestC(?!oncurrencyIntegration)|^TestD(?!ashboard)" } | |
| - { id: 2, name: "Dashboard", pattern: "^TestDashboard" } | |
| - { id: 3, name: "E-I", pattern: "^Test[E-I]" } | |
| # J-P batch: excludes TestMonitorTagSingleSource (moved to batch 6) — | |
| # it triggers the same R2021b cumulative-state segfault as | |
| # TestConcurrencyIntegration when run after ~20 preceding tests in | |
| # the same MATLAB process. | |
| - { id: 4, name: "J-P", pattern: "^Test[J-LN-P]|^TestM(?!onitorTagSingleSource)" } | |
| # Batch 5 also picks up digit-prefixed v4.0 cluster acceptance harness | |
| # (Test50CompanionAcceptance). The test self-gates via FASTSENSE_RUN_ACCEPTANCE | |
| # so it will report as skipped/incomplete in CI rather than running, but | |
| # including it in a batch keeps it discoverable and statused. | |
| - { id: 5, name: "Q-Z", pattern: "^Test([Q-Z]|[0-9])" } | |
| # Batch 6: v4.0 cluster tests that load `lockfile_mex` run alone in a | |
| # fresh MATLAB process. Background: R2021b headless Linux accumulates | |
| # state corruption across the long earlier batches; loading the MEX | |
| # late in the run triggers a segfault inside MATLAB's MEX dispatcher. | |
| # Running in isolation gives the MEX a clean dispatcher state. | |
| - { id: 6, name: "v4-Cluster-Tests", pattern: "^Test(ConcurrencyIntegration|MonitorTagSingleSource)" } | |
| env: | |
| FASTSENSE_SKIP_BUILD: "1" | |
| # v4.0 Phase 1032 SC1: 4-node simulated-cluster smoke. Spawns 4 child | |
| # `matlab -batch` workers that race to emit events through a shared | |
| # FileLock + EventStore, asserting exactly-N events for N rising edges | |
| # (single-source ACK-04 guarantee). Test gates on isunix() && ~ismac() | |
| # so it runs on Ubuntu CI and skips cleanly on macOS / Windows. | |
| # Cost: ~30 s per CI run on top of the standard batch. | |
| FASTSENSE_STRESS_4: "1" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Setup MATLAB | |
| uses: matlab-actions/setup-matlab@v3 | |
| with: | |
| # Matches build-mex-matlab job (PR #109): R2020b libmex segfaults. | |
| release: R2021b | |
| cache: true | |
| - name: Download MATLAB MEX binaries | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: mex-matlab-linux | |
| - name: Diagnose mksqlite availability for tests | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| echo "=== matlab job batch ${{ matrix.batch.name }}: pre-test mksqlite diagnostic ===" | |
| echo "--- files on disk after artifact download ---" | |
| ls -la libs/FastSense/mksqlite.* 2>&1 || echo "(no mksqlite files on disk)" | |
| echo "--- libs/Concurrency/ contents (v4.0 lockfile_mex check) ---" | |
| ls -la libs/Concurrency/ 2>&1 || echo "(no libs/Concurrency/ dir)" | |
| echo "--- repo-rooted alternative paths (if upload-artifact stripped libs/) ---" | |
| ls -la Concurrency/ 2>&1 | head -5 || echo "(no Concurrency/ at workspace root)" | |
| ls -la FastSense/mksqlite.* 2>&1 | head -5 || echo "(no FastSense/ at workspace root)" | |
| - name: Rebuild Concurrency MEX inline | |
| # actions/upload-artifact@v7 strips the LCA `libs/` from paths, and | |
| # actions/download-artifact@v8 doesn't reliably restore libs/Concurrency/ | |
| # contents the way it does libs/FastSense/private/. Unlike mksqlite, the | |
| # lockfile_mex binary isn't committed to the repo, so the test job has | |
| # no copy without rebuild. Compiling lockfile_mex.c is ~5s — cheap. | |
| # FastSense/SensorThreshold MEX binaries continue to come from the | |
| # cached artifact (they're slower to build and have stable inputs). | |
| uses: matlab-actions/run-command@v3 | |
| with: | |
| command: | | |
| addpath('libs/Concurrency'); | |
| build_concurrency_mex(); | |
| fprintf('lockfile_mex on path: %s\n', which('lockfile_mex')); | |
| - name: MATLAB which-mksqlite check | |
| if: always() | |
| continue-on-error: true | |
| uses: matlab-actions/run-command@v3 | |
| with: | |
| command: | | |
| addpath('.'); | |
| install(); | |
| fprintf('which mksqlite: %s\n', which('mksqlite')); | |
| fprintf('exist mksqlite: %d (expect 3 if MEX loadable)\n', exist('mksqlite')); | |
| fprintf('which lockfile_mex: %s\n', which('lockfile_mex')); | |
| try | |
| mksqlite('version'); | |
| fprintf('mksqlite call: OK\n'); | |
| catch e | |
| fprintf('mksqlite call FAILED: %s\n', e.message); | |
| end | |
| - name: Run tests with coverage (batch ${{ matrix.batch.name }}) | |
| id: matlab-tests | |
| continue-on-error: true | |
| uses: matlab-actions/run-command@v3 | |
| with: | |
| # Second arg is the batch regex; first arg remains the | |
| # path-filter pattern. Empty batch regex disables the filter | |
| # (used for non-batched single-job invocations). | |
| command: "addpath('scripts'); run_tests_with_coverage('${{ needs.changes.outputs.test_pattern }}', '${{ matrix.batch.pattern }}');" | |
| - name: Verify MATLAB test pass via sentinel file | |
| # MATLAB R2021b reproducibly segfaults during shutdown on the | |
| # headless Linux runner, even when every test passes. The | |
| # matlab-actions step above is allowed to fail; truth comes from | |
| # the .matlab-tests-passed sentinel that run_tests_with_coverage | |
| # writes only after results.Failed == 0. If the sentinel is | |
| # missing, this step fails the job. | |
| shell: bash | |
| run: | | |
| if [[ -f .matlab-tests-passed ]]; then | |
| echo "MATLAB tests passed in batch ${{ matrix.batch.name }} (sentinel present); ignoring shutdown segfault." | |
| rm -f .matlab-tests-passed | |
| else | |
| echo "MATLAB tests failed in batch ${{ matrix.batch.name }}: .matlab-tests-passed sentinel not written." | |
| echo "matlab-actions step outcome: ${{ steps.matlab-tests.outcome }}" | |
| exit 1 | |
| fi | |
| - name: Write MATLAB test summary | |
| if: always() | |
| shell: bash | |
| run: | | |
| { | |
| echo "### MATLAB Tests (batch ${{ matrix.batch.name }})" | |
| echo "" | |
| echo "Pattern: \`${{ matrix.batch.pattern }}\`" | |
| echo "MATLAB test run completed — see job log for details." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload coverage to Codecov | |
| if: always() | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| files: coverage.xml | |
| # Codecov merges multiple uploads sharing the `matlab` flag | |
| # by SHA. Batch flag is informational so per-batch coverage | |
| # is visible in the Codecov UI. | |
| flags: matlab,batch${{ matrix.batch.id }} | |
| fail_ci_if_error: false | |
| env: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| # v4.0 Multi-User LAN Concurrency cross-platform smoke. | |
| # | |
| # Why a separate job: lockfile_mex.c has three kernel branches (LockFileEx on | |
| # Win32, F_OFD_SETLK on Linux ≥ 3.15, F_SETLK fallback on macOS). The full | |
| # MATLAB test suite only runs on Linux (the `matlab:` job above), so a bad | |
| # #ifdef change could break Windows or macOS silently. This job runs a focused | |
| # subset of concurrency tests on all three OSes to catch platform-specific | |
| # regressions within 24 h. | |
| # | |
| # Scope: ~10 test files, all in libs/Concurrency/ + the cluster-mode paths in | |
| # libs/EventDetection and libs/SensorThreshold. The Dashboard / FastSense / | |
| # WebBridge / UI tests stay Linux-only — they're MATLAB-level functionality | |
| # without kernel divergence. | |
| # | |
| # Cost: ~5 min wall-clock per platform on top of the existing matrix. | |
| # Gated by path-filter so PRs touching unrelated areas don't pay this cost. | |
| matlab-concurrency-smoke: | |
| name: Concurrency Smoke (${{ matrix.os }}) | |
| needs: [changes] | |
| timeout-minutes: 30 | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.concurrency == 'true' || needs.changes.outputs.core == 'true' || needs.changes.outputs.mex == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # ubuntu-latest: kernel 5.x → F_OFD_SETLK branch of lockfile_mex.c | |
| # macos-14: Apple Silicon (ARM64), Darwin → F_SETLK fallback branch | |
| # windows-latest: Windows Server 2022, NTFS → LockFileEx branch | |
| os: [ubuntu-latest, macos-14, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| # Phase 1032 SC1: enable the 4-node simulated-cluster smoke. Test self-gates | |
| # additionally on isunix() && ~ismac() so it runs on Linux only; macOS and | |
| # Windows skip cleanly. Cost ~30s on the Linux job; ~0s on the others. | |
| FASTSENSE_STRESS_4: "1" | |
| steps: | |
| # Windows-only: wiki/ contains filenames with colons (e.g. | |
| # `API-Reference:-Dashboard.md`) which NTFS rejects under git's default | |
| # protectNTFS. Same pattern as mex-build-windows. macOS / Linux are no-ops. | |
| - name: Allow paths with colons (Windows / wiki files) | |
| if: runner.os == 'Windows' | |
| run: git config --global core.protectNTFS false | |
| # Sparse-checkout skips wiki/ entirely on all OSes — fixes Windows checkout | |
| # by avoiding the colon-filename files, and trims clone size on the others. | |
| # Tests need libs/, tests/, scripts/, install.m. Nothing else. | |
| - uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: | | |
| libs | |
| tests | |
| scripts | |
| install.m | |
| run_profile.m | |
| sparse-checkout-cone-mode: false | |
| - name: Setup MATLAB | |
| uses: matlab-actions/setup-matlab@v3 | |
| with: | |
| # Match the main matlab: job. R2021b has best matlab-actions support | |
| # across all three OSes. On macos-14 ARM64 this runs under Rosetta; | |
| # if Rosetta proves flaky we can bump just this platform later. | |
| release: R2021b | |
| cache: true | |
| - name: Build MEX + run concurrency smoke | |
| id: smoke | |
| # matlab-actions occasionally reports nonzero exit on R2021b shutdown | |
| # segfault even when tests pass; the sentinel-file step below is the | |
| # source of truth (same pattern as the main matlab: job). | |
| continue-on-error: true | |
| uses: matlab-actions/run-command@v3 | |
| with: | |
| # install() compiles lockfile_mex on the current platform (LockFileEx | |
| # / F_OFD_SETLK / F_SETLK branch selected by #ifdef in lockfile_mex.c | |
| # + -D_GNU_SOURCE on Linux from build_concurrency_mex.m). | |
| # | |
| # Second arg of run_tests_with_coverage is the batch regex; first arg | |
| # (path-filter pattern) is empty so the batch regex is the sole filter. | |
| # The regex matches the v4.0 concurrency test surface: | |
| # - TestFileLock / TestFileLockStress50 (FASTSENSE_STRESS_50-gated) | |
| # - TestLockfileMex (platform-branch probe) | |
| # - TestAtomicWriter (movefile / rename semantics per FS) | |
| # - TestCluster{Identity,Config,ConfigOplocks,ConfigNfsv3} | |
| # - TestTagWriteCoordinator | |
| # - TestEventLog{,Consolidator,Reader} | |
| # - TestConcurrencyIntegration | |
| # - TestListenerCannotAcquireLock | |
| # - TestMonitorTagSingleSource (FASTSENSE_STRESS_4-gated) | |
| # - TestLiveTagPipelineCluster | |
| # - TestShareLossRecovery | |
| # - TestEventAcknowledgement | |
| command: | | |
| addpath('.'); | |
| install(); | |
| addpath('scripts'); | |
| run_tests_with_coverage('', '^Test(FileLock|LockfileMex|AtomicWriter|Cluster|TagWriteCoordinator|EventLog|Concurrency|ListenerCannotAcquireLock|MonitorTagSingleSource|LiveTagPipelineCluster|ShareLossRecovery|EventAcknowledgement)'); | |
| - name: Verify smoke pass via sentinel file | |
| shell: bash | |
| run: | | |
| if [[ -f .matlab-tests-passed ]]; then | |
| echo "Concurrency smoke passed on ${{ matrix.os }} (sentinel present); ignoring any shutdown-time noise." | |
| rm -f .matlab-tests-passed | |
| else | |
| echo "Concurrency smoke FAILED on ${{ matrix.os }}: .matlab-tests-passed sentinel not written." | |
| echo "matlab-actions step outcome: ${{ steps.smoke.outcome }}" | |
| exit 1 | |
| fi | |
| - name: Write concurrency smoke summary | |
| if: always() | |
| shell: bash | |
| run: | | |
| { | |
| echo "### Concurrency Smoke (${{ matrix.os }})" | |
| echo "" | |
| echo "Validates v4.0 platform-divergent concurrency code paths:" | |
| echo "- **ubuntu-latest** → \`F_OFD_SETLK\` branch (kernel ≥ 3.15, OFD locks)" | |
| echo "- **macos-14** → \`F_SETLK\` fallback branch (Darwin, no OFD support)" | |
| echo "- **windows-latest** → \`LockFileEx\` branch (NTFS, process-scoped advisory locks)" | |
| echo "" | |
| echo "Tests cover: \`FileLock\`, \`AtomicWriter\`, \`ClusterIdentity\`, \`ClusterConfig\`," | |
| echo "\`TagWriteCoordinator\`, \`EventLog\`/\`EventLogReader\`, ack workflow, and" | |
| echo "share-loss recovery. Gated multi-process smokes:" | |
| echo "- \`testTwoProcessMutualExclusion\` / \`testTwoProcessWriteRace\` — Linux only (gate: ~ispc() && ~ismac())" | |
| echo "- \`testFourNodeRisingEdges\` — Linux only (gates: isunix() && ~ismac() + FASTSENSE_STRESS_4=1)" | |
| echo "- \`TestFileLockStress50\` / \`Test50CompanionAcceptance\` — require real SMB share (operator-run; not in CI)" | |
| } >> "$GITHUB_STEP_SUMMARY" |