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
105 changes: 80 additions & 25 deletions .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
Expand All @@ -27,23 +25,28 @@ jobs:
coverage:
runs-on: ${{ matrix.os }}
timeout-minutes: ${{ matrix.timeout }}
# 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.
permissions:
contents: read
# Codecov's tokenless upload uses an OIDC id-token instead of a repository upload token.
id-token: write
# 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
Expand Down Expand Up @@ -197,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)
Expand All @@ -229,13 +229,68 @@ 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-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() && hashFiles('target/pycov/junit-*.xml') != '' }}
uses: codecov/codecov-action@v7
with:
use_oidc: true
report_type: test_results
files: target/nextest/ci/junit.xml
# The pytest-cov task writes one junit-<module>.xml per module here, which the CLI's search picks up.
directory: target/pycov
# 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.
# 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
62 changes: 43 additions & 19 deletions .mise/config.coverage.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,19 @@ 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
# against the mise-managed toolchain.
# `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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -378,26 +387,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-<module>.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
"""
Expand All @@ -407,6 +414,23 @@ 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 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"
reports=(target/nextest/ci-unit/junit-unit.xml target/nextest/ci-integration/junit-integration.xml
target/pycov/junit-*.xml)
count="$(rg --count-matches --no-filename '<testcase ' "${reports[@]}" |
goawk '{ n += $1 } END { print n }')"
printf '{"schemaVersion":1,"label":"tests","message":"%s","color":"blue"}' "$count" >"$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"
Expand Down
1 change: 1 addition & 0 deletions .mise/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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})"'
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
23 changes: 23 additions & 0 deletions config/deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading