From b9821a3554cd98a750ceaa522178d4dd5a20fff0 Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 30 Sep 2026 08:46:40 +0800 Subject: [PATCH 1/2] Add badges --- .github/workflows/coverage.yaml | 52 ++++++++++++++++++++-- .mise/config.coverage.toml | 39 ++++++++++------ .mise/config.toml | 1 + README.md | 9 ++++ config/deny.toml | 23 ++++++++++ config/osv-scanner.toml | 4 ++ config/semgrep/no-check-tool-mentions.yaml | 7 ++- 7 files changed, 117 insertions(+), 18 deletions(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 6fbb1ab2..589f914b 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -7,10 +7,8 @@ name: coverage branches: [main] workflow_dispatch: -permissions: - contents: read - # Codecov's tokenless upload uses an OIDC id-token instead of a repository upload token. - id-token: write +# Nothing at workflow scope: each job declares its own, so the Pages and OIDC grants stay with the job needing them. +permissions: {} concurrency: group: "${{ github.workflow }}-${{ github.ref }}" @@ -27,6 +25,10 @@ jobs: coverage: runs-on: ${{ matrix.os }} timeout-minutes: ${{ matrix.timeout }} + permissions: + contents: read + # Codecov's tokenless upload uses an OIDC id-token instead of a repository upload token. + id-token: write # Two lanes, with only one of them reporting outward. # ubuntu-latest stays the canonical lane: it is the only one that can run the wasm/browser half of the # pipeline, and it alone uploads to Codecov and DeepSource, so the published numbers keep meaning exactly @@ -239,3 +241,45 @@ jobs: report_type: test_results files: target/nextest/ci/junit.xml fail_ci_if_error: true + + - name: Upload Python test results to Codecov (OIDC) + if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('target/pycov/junit-*.xml') != '' }} + uses: codecov/codecov-action@v7 + with: + use_oidc: true + report_type: test_results + # The pytest-cov task writes one junit-.xml per module here, which the CLI's search picks up. + directory: target/pycov + flags: python + fail_ci_if_error: true + + # Only a fully green main run republishes the README's test-count badge. + # Deliberately unguarded, unlike the uploads above: a failed run can stop partway through the pytest modules + # and would publish a count that silently drops the ones that never ran, so the badge keeps the last good one. + - name: Count tests for the README badge + if: ${{ runner.os == 'Linux' && github.event_name == 'push' && github.ref == 'refs/heads/main' }} + run: mise run test-count-badge + + - name: Upload the test-count badge as the Pages artifact + if: ${{ runner.os == 'Linux' && github.event_name == 'push' && github.ref == 'refs/heads/main' }} + uses: actions/upload-pages-artifact@v5 + with: + path: target/test-badge + + # Separate from the coverage job so only this one holds the Pages write permissions. + # `needs` waits on both matrix lanes, and the artifact exists only when the Linux lane got through the steps above. + pages: + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + needs: coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - name: Deploy the test-count badge to GitHub Pages + id: deploy + uses: actions/deploy-pages@v5 diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 8f87d8ad..fb84f3d0 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -378,26 +378,24 @@ mise run _cov-branch-assert "$report" "{{ vars.wasm_cov_libs }}" shell = "{{ vars.task_shell }}" [tasks.pytest-cov] -description = "Combined Python coverage (pytest + web-runner Pyodide runs) -> Cobertura for Codecov/DeepSource" +description = "Combined Python coverage (pytest + web-runner Pyodide runs) -> Cobertura, plus pytest JUnit results" # One Python report is combined from several data sources: the pytest runs plus the web-runner Pyodide runs. # The Python workflow pytest runs and the web-runner integration test (which drops the modules' .coverage files # into target/pycov/ when ET_TEST_COVERAGE is set) all feed `coverage combine`. The paths in # config/coverage.toml remap them into a single Cobertura report. +# Each pytest run also writes target/pycov/junit-.xml for the Codecov test-results upload. The path is +# absolute because `uv run --directory` moves pytest into the module dir, and `junit_family=legacy` is the +# schema that records each test's file, which Codecov's test analytics reads. run = """ -pyface=services/ws-modules/pyface1 -pydemo=services/ws-modules/pydemo1 -pyeye=services/ws-modules/pyeye1 -pyspeech=services/ws-modules/pyspeech1 pycov=target/pycov mkdir -p "$pycov" -uv run --directory "$pyface" pytest --cov=pyface1 --cov-report= -cp "$pyface/.coverage" "$pycov/.coverage.pyface1_pytest" -uv run --directory "$pydemo" pytest --cov=pydemo1 --cov-report= -cp "$pydemo/.coverage" "$pycov/.coverage.pydemo1_pytest" -uv run --directory "$pyeye" pytest --cov=pyeye1 --cov-report= -cp "$pyeye/.coverage" "$pycov/.coverage.pyeye1_pytest" -uv run --directory "$pyspeech" pytest --cov=pyspeech1 --cov-report= -cp "$pyspeech/.coverage" "$pycov/.coverage.pyspeech1_pytest" +for module in pyface1 pydemo1 pyeye1 pyspeech1; do + dir="services/ws-modules/$module" + junit="$PWD/$pycov/junit-$module.xml" + uv run --directory "$dir" pytest --cov="$module" --cov-report= --junitxml="$junit" -o junit_family=legacy + cp "$dir/.coverage" "$pycov/.coverage.${module}_pytest" +done +pyface=services/ws-modules/pyface1 uv run --project "$pyface" coverage combine "$pycov" uv run --project "$pyface" coverage xml -o coverage-python.xml """ @@ -407,6 +405,21 @@ shell = "{{ vars.task_shell }}" COVERAGE_RCFILE = "{{ config_root }}/config/coverage.toml" UV_PYTHON = "{% if os() == 'windows' %}{{ vars.py3_win }}{% else %}{{ vars.py3_unix }}{% endif %}" +[tasks.test-count-badge] +description = "Count the tests in the JUnit reports into a shields.io endpoint badge at target/test-badge/tests.json" +# Counts the same JUnit reports the coverage workflow uploads, so the badge matches the per-commit test count. +# A missing report fails the task rather than publishing a count that silently leaves a language out. Matches are +# counted rather than lines because pytest writes its whole report on a single line. +run = """ +site=target/test-badge +coreutils mkdir -p "$site" +count="$(rg --count-matches --no-filename '"$site/tests.json" +coreutils cat "$site/tests.json" +""" +shell = "{{ vars.task_shell }}" + [tasks.wasm-agent-cov] depends = ["build-wasm-cov-wrapper"] description = "Coverage for the browser ws-wasm-agent: run its wasm-bindgen tests headless, emit lcov into lcov.info" diff --git a/.mise/config.toml b/.mise/config.toml index 8984b423..223509c7 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -1768,6 +1768,7 @@ run = """ # backticks in the echoed line are literal markdown, not command substitution. # shellcheck disable=SC2016 echo '# AUTO-GENERATED from config/deny.toml by `mise run gen:osv-scanner`.' + echo '# The list spans every language, so "has unused ignores" warnings are expected: agents must ignore them.' echo 'IgnoredVulns = [' id_re='"(RUSTSEC-[0-9]{4}-[0-9]{4}|GHSA(?:-[0-9a-z]{4}){3})"' date_re='reason = "expires ([0-9]{4}-[0-9]{2}-[0-9]{2})"' diff --git a/README.md b/README.md index 79e12a01..e222ec6f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # edge-toolkit core +[![Tests][tests-badge]][tests] [![Coverage][coverage-badge]][coverage] [![et-ws-server][crate-badge]][crate] + +[tests-badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Fedge-toolkit.github.io%2Fcore%2Ftests.json +[tests]: https://github.com/edge-toolkit/core/actions/workflows/coverage.yaml?query=branch%3Amain +[coverage-badge]: https://codecov.io/gh/edge-toolkit/core/graph/badge.svg?branch=main +[coverage]: https://codecov.io/gh/edge-toolkit/core +[crate-badge]: https://img.shields.io/crates/v/et-ws-server +[crate]: https://crates.io/crates/et-ws-server + edge-toolkit is a WebSocket-based edge-computing framework that runs AI on hardware you control, so nothing has to leave your network. A lightweight server acts as a hub that serves small AI modules -- written in Rust, Python, Dart, C#, Java, Kotlin and more, each compiled to WebAssembly or transpiled to JavaScript -- straight to a browser, where they run diff --git a/config/deny.toml b/config/deny.toml index 50f40105..ebebaab3 100644 --- a/config/deny.toml +++ b/config/deny.toml @@ -9,6 +9,14 @@ yanked = "deny" # Advisories we've reviewed and consciously accept until upstream Deno migrates. # Each entry names the vulnerable crate, the path that pulls it, and the runtime reason it isn't exploitable # here. Drop entries as upstream catches up; never silence one without a written reason. +# +# This one list serves every language, so no single command uses all of it -- expect, and ignore, the noise. +# It is also the source of config/osv-scanner.toml, so it holds npm and Maven ids next to the Rust ones, and +# each scan (cargo-deny, osv-scanner over the lockfiles, osv-scanner-npm) sees only its own slice. cargo-deny +# therefore warns `unknown-advisory` ("advisory not found in any advisory database") and `advisory-not-detected` +# for the ids outside its view, and osv-scanner lists the same kind under "has unused ignores". Those warnings are +# expected: agents must ignore them, and must never drop an entry because of one. An entry goes only when its own +# reason says upstream has caught up. ignore = [ # bincode 1.3.3 (unmaintained; development ceased permanently, so no version is patched). # Pulled by deno_core for snapshot / cache serialization -- its only consumer, requiring ^1 through its @@ -38,6 +46,21 @@ ignore = [ # When the expiry below lapses the answer is one of two things: deno_tls has relaxed the pin, so drop this # entry; or it has not, so carry a fork of deno_tls rather than extend the date again. { id = "RUSTSEC-2026-0285", reason = "expires 2026-10-15" }, + # wasmtime / wasmtime-wasi 47.0.4 -- three guest-to-host advisories, all fixed in 48.0.3 and absent from 47. + # Pulled by et-ws-wasi-runner. No bump reaches the fix: wasi-webgpu-wasmtime 0.2.0, its newest release, + # requires `wasmtime ^47` and `wasmtime-wasi ^47`, and the runner is published, so a git dependency on + # upstream's unreleased 0.3.0 (already on wasmtime 48) cannot stand in. Real vulnerabilities, so time-boxed: + # - 0314, a guest panicking the host through a filesystem datetime overflow. Unreachable in a release build, + # whose WasiCtx grants stdio and env only; the one preopen, /cov, compiles in only under `coverage`. + # - 0315, fuel accounting lost across `call_ref` and exception `catch`. The runner configures no fuel, so + # there is no limit for a guest to escape. + # - 0316, dynamic record lifting allocating past the hostcall fuel limit. That default cap does apply, so a + # hostile guest can make the host over-allocate; the guests are the modules our own ws-server serves. + # When the expiry lapses, wasi-webgpu-wasmtime 0.3.0 is either published, so move to wasmtime 48.0.3 and drop + # these, or it is not, and the question is whether the runner can live without wasi:webgpu until it is. + { id = "RUSTSEC-2026-0314", reason = "expires 2026-10-15" }, + { id = "RUSTSEC-2026-0315", reason = "expires 2026-10-15" }, + { id = "RUSTSEC-2026-0316", reason = "expires 2026-10-15" }, # smartstring 1.0.1 (unmaintained; repo archived 2026-05-03). # Pulled only by the swc stack (swc_ecma_lexer/parser 26/27) under deno_ast -> deno_runtime. Maintenance # notice, not a vulnerability. Upstream swc migrated to compact_str at swc_ecma_lexer 41.0.0 and diff --git a/config/osv-scanner.toml b/config/osv-scanner.toml index 97244477..7c5658c3 100644 --- a/config/osv-scanner.toml +++ b/config/osv-scanner.toml @@ -1,4 +1,5 @@ # AUTO-GENERATED from config/deny.toml by `mise run gen:osv-scanner`. +# The list spans every language, so "has unused ignores" warnings are expected: agents must ignore them. IgnoredVulns = [ { id = "GHSA-3w8q-xq97-5j7x", ignoreUntil = 2026-10-15 }, { id = "GHSA-f88m-g3jw-g9cj", ignoreUntil = 2026-10-15 }, @@ -15,4 +16,7 @@ IgnoredVulns = [ { id = "RUSTSEC-2026-0249", ignoreUntil = 2026-10-15 }, { id = "RUSTSEC-2026-0258", ignoreUntil = 2026-10-15 }, { id = "RUSTSEC-2026-0285", ignoreUntil = 2026-10-15 }, + { id = "RUSTSEC-2026-0314", ignoreUntil = 2026-10-15 }, + { id = "RUSTSEC-2026-0315", ignoreUntil = 2026-10-15 }, + { id = "RUSTSEC-2026-0316", ignoreUntil = 2026-10-15 }, ] diff --git a/config/semgrep/no-check-tool-mentions.yaml b/config/semgrep/no-check-tool-mentions.yaml index 43bdc0bb..8e4570be 100644 --- a/config/semgrep/no-check-tool-mentions.yaml +++ b/config/semgrep/no-check-tool-mentions.yaml @@ -160,7 +160,12 @@ rules: - "/.dockerignore" # The same ignore, generated from it. - "/verification/**" # Generator-owned output, carrying whatever the generator wrote. - "/config/semgrep/no-*-mentions.yaml" # The mention bans, which cannot help but name what they ban. - pattern-regex: "(?i)codecov" + patterns: + - pattern-regex: "(?i)codecov" + # The README's coverage badge and its link, which display the service's output rather than explain code. + # Matched as whole reference definitions, so any other mention in the README is still caught. + - pattern-not-regex: >- + (?m)^\[coverage(-badge)?\]: https://codecov\.io/gh/edge-toolkit/core(/graph/badge\.svg\?branch=main)?$ message: >- Do not name the coverage service outside the jobs that upload to it. A test exists to pin behaviour, and saying it was added for a coverage percentage tells the next reader to delete it once the number moves. From e8510c7fdb639225ba0e340ad22c1f189c0f7d3a Mon Sep 17 00:00:00 2001 From: John Vandenberg Date: Wed, 30 Sep 2026 09:46:03 +0800 Subject: [PATCH 2/2] Split coverage between unit vs integration --- .github/workflows/coverage.yaml | 57 ++++++++++++++++++++------------- .mise/config.coverage.toml | 27 +++++++++++----- config/nextest.toml | 30 +++++++++++++++-- 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 589f914b..1c9c99c9 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -29,23 +29,24 @@ jobs: contents: read # Codecov's tokenless upload uses an OIDC id-token instead of a repository upload token. id-token: write - # Two lanes, with only one of them reporting outward. - # ubuntu-latest stays the canonical lane: it is the only one that can run the wasm/browser half of the - # pipeline, and it alone uploads to Codecov and DeepSource, so the published numbers keep meaning exactly - # what they did before. macos-latest re-runs the native half as a second platform, which is what catches a - # branch reachable only under a different target's cfg. Its report is kept as an artifact rather than - # uploaded: both lanes would otherwise land on the same commit under the same `rust` flag / `--key rust`, - # and the two partial pictures would fight rather than merge. + # Two lanes, both uploading to Codecov and only one to DeepSource. + # ubuntu-latest is the canonical lane: it is the only one that can run the wasm/browser half of the pipeline. + # macos-latest re-runs the native half as a second platform, which is what catches a branch reachable only + # under a different target's cfg. Codecov merges every upload on a commit, so the macOS report adds the paths + # only it reaches, and `os_flag` tags each upload with its lane. DeepSource instead keeps one report per + # `--key`, where the macOS lane's partial picture would replace the Linux one, so only Linux sends it there. strategy: fail-fast: false matrix: include: - os: ubuntu-latest + os_flag: linux timeout: 150 # Longer than the Linux lane, despite doing less. # The macOS runners are slower per-core, and this one still pays the full instrumented rebuild even # though it skips the wasm/browser steps. - os: macos-latest + os_flag: macos timeout: 180 env: MISE_ENV: dart,dotnet,java,js,kotlin,python,r,rust,zig,coverage @@ -199,27 +200,24 @@ jobs: # second, misleading failure: when an earlier step (a tool install, say) means nothing was ever produced, # these skip instead of erroring on a missing file and burying the real cause. # - # All four carry `runner.os == 'Linux'` so only the canonical lane reports outward. - # Both lanes land on the same commit, so an unguarded upload would send two partial pictures under one - # `rust` flag / one `--key rust`: the macOS lcov.info covers the native half only, and whichever arrived - # second would read as a coverage collapse rather than as a second platform's result. The macOS numbers - # are kept as that lane's artifact instead, and its branch gate fails the job directly when they slip. + # Every upload names its language and lane in `flags`, so Codecov can report each on its own. The two + # DeepSource uploads carry `runner.os == 'Linux'`, for the one-report-per-key reason given on the job. - name: Upload Rust coverage to Codecov (OIDC) - if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('lcov.info') != '' }} + if: ${{ !cancelled() && hashFiles('lcov.info') != '' }} uses: codecov/codecov-action@v7 with: use_oidc: true files: lcov.info - flags: rust + flags: rust,${{ matrix.os_flag }} fail_ci_if_error: true - name: Upload Python coverage to Codecov (OIDC) - if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('coverage-python.xml') != '' }} + if: ${{ !cancelled() && hashFiles('coverage-python.xml') != '' }} uses: codecov/codecov-action@v7 with: use_oidc: true files: coverage-python.xml - flags: python + flags: python,${{ matrix.os_flag }} fail_ci_if_error: true - name: Upload Rust coverage to DeepSource (OIDC) @@ -231,26 +229,39 @@ jobs: run: deepsource report --analyzer test-coverage --key python --value-file coverage-python.xml --use-oidc # Upload test results even when tests failed. - # nextest still wrote the JUnit report, and a failing run is exactly when the per-test results on Codecov - # matter most; `report_type: test_results` selects the JUnit upload path. - - name: Upload test results to Codecov (OIDC) - if: ${{ !cancelled() && runner.os == 'Linux' }} + # nextest still wrote the JUnit reports, and a failing run is exactly when the per-test results on Codecov + # matter most; `report_type: test_results` selects the JUnit upload path. A flag applies to a whole upload, + # so each report goes up on its own to say which language, lane and kind of test it holds. + - name: Upload Rust unit test results to Codecov (OIDC) + if: ${{ !cancelled() && hashFiles('target/nextest/ci-unit/junit-unit.xml') != '' }} + uses: codecov/codecov-action@v7 + with: + use_oidc: true + report_type: test_results + files: target/nextest/ci-unit/junit-unit.xml + flags: rust,${{ matrix.os_flag }},unit + fail_ci_if_error: true + + - name: Upload Rust integration test results to Codecov (OIDC) + if: ${{ !cancelled() && hashFiles('target/nextest/ci-integration/junit-integration.xml') != '' }} uses: codecov/codecov-action@v7 with: use_oidc: true report_type: test_results - files: target/nextest/ci/junit.xml + files: target/nextest/ci-integration/junit-integration.xml + flags: rust,${{ matrix.os_flag }},integration fail_ci_if_error: true - name: Upload Python test results to Codecov (OIDC) - if: ${{ !cancelled() && runner.os == 'Linux' && hashFiles('target/pycov/junit-*.xml') != '' }} + if: ${{ !cancelled() && hashFiles('target/pycov/junit-*.xml') != '' }} uses: codecov/codecov-action@v7 with: use_oidc: true report_type: test_results # The pytest-cov task writes one junit-.xml per module here, which the CLI's search picks up. directory: target/pycov - flags: python + # pytest exercises each module's own code in process, with nothing spawned or served. + flags: python,${{ matrix.os_flag }},unit fail_ci_if_error: true # Only a fully green main run republishes the README's test-count badge. diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index fb84f3d0..f09979b0 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -84,7 +84,7 @@ LD_LIBRARY_PATH = "{{ vars.ort_loc_unix }}/lib" _.path = "{{ env.HOME }}/.local/share/mise/installs/conda-clang/latest/bin" [tasks.cargo-llvm-cov] -description = "Rust coverage (cargo-llvm-cov) + JUnit test results (nextest); emits lcov.info + junit.xml" +description = "Rust coverage (cargo-llvm-cov) + JUnit test results (nextest); emits lcov.info + JUnit reports" # cargo-llvm-cov is deliberately NOT a mise [tool] (like cargo-unmaintained). # The coverage workflow installs it via taiki-e/install-action; cargo-nextest comes from config.toml's mise tool. # This task just runs them. cargo-llvm-cov needs the `llvm-tools-preview` rustup component, which it adds on demand @@ -92,12 +92,11 @@ description = "Rust coverage (cargo-llvm-cov) + JUnit test results (nextest); em # `show-env` exports the coverage instrumentation env (rustc wrapper, LLVM_PROFILE_FILE, target dir) so BOTH the # nextest run and the mise regen tasks build + run instrumented into one profile set, then a single `report` # merges them. The regens -- `gen-specs` (et-int-gen) and `regen-verification` (et-cli) -- exercise the code -# generators, which otherwise get only their unit-test coverage. nextest (NEXTEST_PROFILE=ci) also writes -# target/nextest/ci/junit.xml for the Codecov test-results upload, and serializes et-ws-web-runner via the +# generators, which otherwise get only their unit-test coverage. nextest also writes a JUnit report per pass +# under target/nextest/ for the Codecov test-results uploads, and serializes et-ws-web-runner via the # `web-runner` test-group in config/nextest.toml. No `--doc` pass: the workspace sets `[lib] doctest = false` # almost everywhere (doc comments are prose), and cargo-llvm-cov's `--doc` ignores that and chokes on the # generated et-rest-client's backtick prose, so coverage stays tests-only. -env = { NEXTEST_PROFILE = "ci" } run = """ cargo llvm-cov clean --workspace # Drop the native build dirs so every native crate rebuilds under cargo-llvm-cov's rustc wrapper. @@ -164,9 +163,19 @@ export __CARGO_LLVM_COV_RUSTC_WRAPPER_RUSTFLAGS="$flags" # lcov.info is never produced -- the coverage upload steps then have nothing to send and the run reports no # coverage at all. A failing run is exactly when the per-test results and the coverage delta matter most, so the # profile data nextest has already written gets turned into a report either way. +# The unit and integration passes run separately so each writes its own JUnit report, and both instrument into +# the one profile set. The integration pass runs even when the unit pass failed, and the first failure is kept. +nextest() { + local features=et-ws-web-runner/coverage,et-ws-wasi-runner/coverage status=0 + cargo nextest run --config-file config/nextest.toml --features "$features" --profile "$1" -E "$2" || status=$? + if [ "$test_status" -eq 0 ]; then + test_status=$status + fi +} +integration='group(integration) or group(web-runner)' test_status=0 -cargo nextest run --config-file config/nextest.toml --features et-ws-web-runner/coverage,et-ws-wasi-runner/coverage || - test_status=$? +nextest ci-unit "not ($integration)" +nextest ci-integration "$integration" mise run gen-specs mise run regen-verification # The runner's host bindings are excluded from the report, not just from analysis. @@ -407,13 +416,15 @@ UV_PYTHON = "{% if os() == 'windows' %}{{ vars.py3_win }}{% else %}{{ vars.py3_u [tasks.test-count-badge] description = "Count the tests in the JUnit reports into a shields.io endpoint badge at target/test-badge/tests.json" -# Counts the same JUnit reports the coverage workflow uploads, so the badge matches the per-commit test count. +# Counts one lane's JUnit reports, so the badge is the number of tests rather than a per-commit sum across lanes. # A missing report fails the task rather than publishing a count that silently leaves a language out. Matches are # counted rather than lines because pytest writes its whole report on a single line. run = """ site=target/test-badge coreutils mkdir -p "$site" -count="$(rg --count-matches --no-filename '"$site/tests.json" coreutils cat "$site/tests.json" diff --git a/config/nextest.toml b/config/nextest.toml index 7074d634..42b486b6 100644 --- a/config/nextest.toml +++ b/config/nextest.toml @@ -1,6 +1,11 @@ -# The coverage mise task selects the `ci` profile to emit target/nextest/ci/junit.xml. -[profile.ci.junit] -path = "junit.xml" +# The coverage mise task runs its unit and integration passes under these, one JUnit report per profile dir. +# A test is an integration test when it is in either group below; every other test is a unit test. The task +# selects them with `-E`, because nextest does not allow group() in a profile's `default-filter`. +[profile.ci-unit.junit] +path = "junit-unit.xml" + +[profile.ci-integration.junit] +path = "junit-integration.xml" # Serialize et-ws-web-runner's tests (under `default`, so every profile inherits it). # Each web-runner test launches a heavy deno_runtime MainWorker, and several at once exhaust fd/port budgets. @@ -8,9 +13,28 @@ path = "junit.xml" # et-cli's scenario_runners test joins the same group for the same reason: it starts two web runners of its own, # so run alongside the web-runner tests it multiplies exactly the load this group exists to bound. It also holds # the hub's fixed port for its duration, which nothing else may take while it runs. +# +# `integration` limits nothing; it exists to name the integration tests for the ci profiles above. [test-groups] +integration = { max-threads = "num-cpus" } web-runner = { max-threads = 1 } [[profile.default.overrides]] filter = 'package(et-ws-web-runner) or binary(scenario_runners)' test-group = 'web-runner' + +# The integration test binaries outside the web-runner group, which already holds only integration tests. +# A binary belongs here when it spawns a child process, listens on a real socket, or drives a runner end to end. +# Serving an app through actix's in-process `test::init_service` does neither, so those stay unit tests. A whole +# package is named only where every one of its test binaries qualifies. Add a new binary here when it qualifies: +# the unit profile takes everything left out, so a missed entry is misfiled rather than dropped. +[[profile.default.overrides]] +filter = ''' +package(et-test-otlp) or package(et-websockify-service) or package(et-ws-test-server) +or package(et-ws-wasi-runner) +or binary_id(edge-toolkit::pipx_site_packages) +or binary_id(et-test-helpers::child_guard) or binary_id(et-test-helpers::path_prefix) +or binary_id(et-ws-runner-common::fetch_main_field) or binary_id(et-ws-runner-common::module_file_missing) +or binary_id(et-ws-runner-common::register_frames) +or binary_id(et-storage-service::s3_backend) or binary_id(et-ws-pyo3-runner::modules)''' +test-group = 'integration'