diff --git a/.agents/skills/build-cadmus-native/SKILL.md b/.agents/skills/build-cadmus-native/SKILL.md
index 301d1fad..a97bc275 100644
--- a/.agents/skills/build-cadmus-native/SKILL.md
+++ b/.agents/skills/build-cadmus-native/SKILL.md
@@ -49,6 +49,9 @@ automatically by `build.rs` when you run any Cargo command that compiles
| Check formatting | `cargo xtask fmt` |
| Run clippy | `cargo xtask clippy` |
| Run tests (default features) | `cargo xtask test --features default` |
+| Run tests with coverage | `cadmus-test-coverage --features default` (devenv) |
+| View coverage (project-wide) | `cadmus-coverage-show` (after test-coverage) |
+| View coverage (patch diff) | `cadmus-coverage-diff` (after test-coverage) |
| Run tests with telemetry | `cargo xtask test --features "profiling + test + tracing"` |
| Run the emulator | `cargo xtask run-emulator` (builds the EPUB first if missing) |
| Install the importer CLI | `cargo xtask install-importer` |
@@ -75,11 +78,30 @@ cargo xtask test --features "profiling + test + tracing"
Use `cargo xtask ci matrix` to see all available feature combinations if you
need to verify a specific one.
+### Coverage locally
+
+In a devenv shell (`cadmus-test-coverage`, `cadmus-coverage-show`, `cadmus-coverage-diff`):
+
+```bash
+cadmus-test-coverage
+cadmus-coverage-show
+cadmus-coverage-diff
+```
+
+Without devenv:
+
+```bash
+cargo xtask test --coverage --features default
+```
+
+This writes `target/coverage/lcov.info`. Project HTML uses `cargo llvm-cov report --html`.
+Patch HTML uses `diff-cover` (see `devenv.nix` scripts).
+
## What the xtask wrappers do
- **`fmt`** — runs `cargo fmt --check` (or `--apply` in CI) across the workspace
- **`clippy`** — iterates the full feature matrix; use `--features` to narrow it
-- **`test`** — iterates the test feature matrix
+- **`test`** — iterates the test feature matrix; `--coverage` enables llvm-cov instrumentation
- **`run-emulator`** — ensures the documentation EPUB exists, then runs `cargo run -p emulator`
- **`install-importer`** — runs `cargo install --path crates/importer`
diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 00000000..bdf9c903
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,18 @@
+comment:
+ layout: "condensed_header, condensed_files, condensed_footer"
+ hide_project_coverage: true
+
+ignore:
+ - "thirdparty/**"
+ - "mupdf_wrapper/**"
+ - "**/build.rs"
+
+coverage:
+ status:
+ project:
+ default:
+ informational: true
+ threshold: 1%
+ patch:
+ default:
+ informational: true
diff --git a/.config/nextest.toml b/.config/nextest.toml
new file mode 100644
index 00000000..2927b6fe
--- /dev/null
+++ b/.config/nextest.toml
@@ -0,0 +1,4 @@
+[profile.ci.junit]
+path = "junit.xml"
+report-name = "cadmus"
+store-failure-output = true
diff --git a/.github/actions/cargo-cache/action.yml b/.github/actions/cargo-cache/action.yml
index a5c041a5..97082b8b 100644
--- a/.github/actions/cargo-cache/action.yml
+++ b/.github/actions/cargo-cache/action.yml
@@ -3,7 +3,8 @@ description: >
Restore or save the shared Cargo cache. The cache key incorporates the
Rust toolchain fingerprint, the Cargo lockfile, build scripts, the
mupdf_wrapper C glue, and the gitlink SHAs of every thirdparty
- submodule so that the key changes when any submodule pointer moves.
+ submodule so that the key changes when any submodule pointer moves. The
+ workflow file is hashed so CI build-step changes invalidate stale target/ caches.
Defaults to restore-only. Pass `mode: save` in the cache-warmup job to
write a fresh cache.
@@ -46,7 +47,7 @@ runs:
~/.cargo/git/db/
target/
libs/
- key: ${{ runner.os }}-cargo-shared-${{ inputs.cachekey }}-${{ hashFiles('**/Cargo.lock', 'crates/build-deps/**', 'build-scripts/**', '.gitmodules', 'mupdf_wrapper/mupdf_wrapper.c', 'crates/core/build.rs') }}-${{ steps.submodules.outputs.hash }}
+ key: ${{ runner.os }}-cargo-shared-${{ inputs.cachekey }}-${{ hashFiles('**/Cargo.lock', 'crates/build-deps/**', 'build-scripts/**', '.gitmodules', 'mupdf_wrapper/mupdf_wrapper.c', 'crates/core/build.rs', '.github/workflows/cargo.yml') }}-${{ steps.submodules.outputs.hash }}
restore-keys: |
${{ runner.os }}-cargo-shared-${{ inputs.cachekey }}-
@@ -62,6 +63,6 @@ runs:
~/.cargo/git/db/
target/
libs/
- key: ${{ runner.os }}-cargo-shared-${{ inputs.cachekey }}-${{ hashFiles('**/Cargo.lock', 'crates/build-deps/**', 'build-scripts/**', '.gitmodules', 'mupdf_wrapper/mupdf_wrapper.c', 'crates/core/build.rs') }}-${{ steps.submodules.outputs.hash }}
+ key: ${{ runner.os }}-cargo-shared-${{ inputs.cachekey }}-${{ hashFiles('**/Cargo.lock', 'crates/build-deps/**', 'build-scripts/**', '.gitmodules', 'mupdf_wrapper/mupdf_wrapper.c', 'crates/core/build.rs', '.github/workflows/cargo.yml') }}-${{ steps.submodules.outputs.hash }}
restore-keys: |
${{ runner.os }}-cargo-shared-${{ inputs.cachekey }}-
diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml
index f8939ee2..02433798 100644
--- a/.github/workflows/cargo.yml
+++ b/.github/workflows/cargo.yml
@@ -97,6 +97,7 @@ jobs:
id: rust-toolchain
with:
targets: arm-unknown-linux-gnueabihf
+ components: llvm-tools
- &set-build-metadata
name: Set build metadata
@@ -143,6 +144,17 @@ jobs:
if: steps.cargo-nextest-cache.outputs.cache-hit != 'true'
run: cargo install cargo-nextest --version "$NEXTEST_VERSION" --locked --force
+ - name: Cache cargo-llvm-cov
+ uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae
+ id: cargo-llvm-cov-cache
+ with:
+ path: ~/.cargo/bin/cargo-llvm-cov
+ key: ${{ runner.os }}-cargo-llvm-cov
+
+ - name: Install cargo-llvm-cov
+ if: steps.cargo-llvm-cov-cache.outputs.cache-hit != 'true'
+ uses: taiki-e/install-action@cargo-llvm-cov
+
- name: Download documentation EPUB
if: steps.cache.outputs.cache-hit != 'true'
uses: actions/download-artifact@v8
@@ -163,6 +175,13 @@ jobs:
- name: Warmup build (native)
if: steps.cache.outputs.cache-hit != 'true'
run: cargo check --workspace --all-features --all-targets
+ - name: Warmup llvm-cov build
+ if: steps.cache.outputs.cache-hit != 'true'
+ run: |
+ cargo llvm-cov nextest-archive \
+ --workspace --all-features --all-targets \
+ --archive-file target/llvm-cov-warmup.tar.zst
+ rm -f target/llvm-cov-warmup.tar.zst
- name: Warmup build (Kobo release)
if: steps.cache.outputs.cache-hit != 'true'
run: cargo build --release --target arm-unknown-linux-gnueabihf -p cadmus
@@ -272,6 +291,8 @@ jobs:
fetch-tags: true
- uses: dtolnay/rust-toolchain@stable
id: rust-toolchain
+ with:
+ components: llvm-tools
- *set-build-metadata
- name: Download documentation EPUB
uses: actions/download-artifact@v8
@@ -311,8 +332,83 @@ jobs:
if: steps.cargo-nextest-cache.outputs.cache-hit != 'true'
run: cargo install cargo-nextest --version "$NEXTEST_VERSION" --locked --force
- - name: Run cargo test (${{ matrix.label }})
- run: cargo xtask test --features "${{ matrix.label }}"
+ - name: Restore cargo-llvm-cov
+ uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae
+ id: cargo-llvm-cov-cache
+ with:
+ path: ~/.cargo/bin/cargo-llvm-cov
+ key: ${{ runner.os }}-cargo-llvm-cov
+
+ - name: Install cargo-llvm-cov
+ if: steps.cargo-llvm-cov-cache.outputs.cache-hit != 'true'
+ uses: taiki-e/install-action@cargo-llvm-cov
+
+ - name: Run cargo test with coverage (${{ matrix.label }})
+ id: test
+ continue-on-error: true
+ run: |
+ slug="${{ matrix.label }}"
+ slug="${slug// + /-}"
+ echo "slug=$slug" >> "$GITHUB_OUTPUT"
+ cargo xtask test \
+ --features "${{ matrix.label }}" \
+ --coverage \
+ --save-coverage "coverage-json/${slug}.json" \
+ --save-junit "test-results/${slug}.junit.xml"
+
+ - name: Upload test results to Codecov
+ if: ${{ !cancelled() }}
+ uses: codecov/codecov-action@v7
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ report_type: test_results
+ files: test-results/${{ steps.test.outputs.slug }}.junit.xml
+ flags: ${{ matrix.label }}
+ disable_search: true
+ fail_ci_if_error: false
+
+ - name: Upload coverage artifact
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: coverage-json-${{ steps.test.outputs.slug }}
+ path: coverage-json/
+ retention-days: 1
+
+ - name: Fail if tests failed
+ if: steps.test.outcome == 'failure'
+ run: exit 1
+
+ coverage-report:
+ if: |
+ github.event_name == 'pull_request' ||
+ github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
+ needs: test
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - name: Download all coverage artifacts
+ uses: actions/download-artifact@v8
+ with:
+ pattern: coverage-json-*
+ path: coverage-json
+ merge-multiple: true
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v7
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ report_type: coverage
+ files: coverage-json/*.json
+ disable_search: true
+ fail_ci_if_error: false
build-kobo:
needs: [cache-warmup, build-docs-epub]
diff --git a/.github/workflows/codecov-config.yml b/.github/workflows/codecov-config.yml
new file mode 100644
index 00000000..4708c040
--- /dev/null
+++ b/.github/workflows/codecov-config.yml
@@ -0,0 +1,20 @@
+name: Codecov config validation
+
+on:
+ pull_request:
+ paths:
+ - ".codecov.yml"
+ - ".github/workflows/codecov-config.yml"
+
+permissions:
+ contents: read
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ with:
+ persist-credentials: false
+ - name: Validate Codecov YAML
+ run: curl -sf -X POST --data-binary @.codecov.yml https://codecov.io/validate
diff --git a/README.md b/README.md
index 2f58280d..48fb839a 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,9 @@
+
+
+
diff --git a/devenv.nix b/devenv.nix
index 7626b640..87f553dd 100644
--- a/devenv.nix
+++ b/devenv.nix
@@ -9,6 +9,15 @@ let
inherit (pkgs.stdenv) isLinux;
inherit (pkgs.stdenv) isDarwin;
+ openHtml =
+ path:
+ if isLinux then
+ "xdg-open ${path}"
+ else if isDarwin then
+ "open ${path}"
+ else
+ "echo \"Open ${path} in a browser\"";
+
# Rust 1.94+ changed lib/ in tarballs from a directory to a symlink.
# devenv's rust module calls mk-aggregated.nix directly from the rust-overlay
# source, bypassing pkgs overlays. lndir can't merge components when $out/lib
@@ -341,6 +350,8 @@ in
pkgs.wrangler
pkgs.cargo-llvm-cov
+ pkgs.python3Packages.diff-cover
+ pkgs.xdg-utils
# Linaro ARM cross-compilation toolchain (provides arm-linux-gnueabihf-* commands)
linaroToolchain
@@ -400,6 +411,10 @@ in
PYROSCOPE_SERVER_URL = "http://localhost:4040";
NEXTEST_NO_TESTS = "pass";
+ # Default branch for patch coverage (cadmus-coverage-diff). Override in devenv.local.nix.
+ CADMUS_DEFAULT_BRANCH = "master";
+ CARGO_TARGET_DIR = "${config.devenv.root}/target";
+
# jemalloc configure uses -O0 in debug builds, which triggers _FORTIFY_SOURCE warnings
# treated as errors under Nix hardening. Disable hardening for jemalloc's build script.
NIX_HARDENING_ENABLE = "";
@@ -803,6 +818,48 @@ in
echo "Updated docs/po/messages.pot"
echo "Translate on Crowdin: https://crowdin.com/project/cadmus"
'';
+
+ # Run instrumented tests; writes target/coverage/lcov.info
+ cadmus-test-coverage.exec = ''
+ cd "$DEVENV_ROOT"
+ if [ $# -eq 0 ]; then
+ set -- --features default
+ fi
+ cargo xtask test --coverage "$@"
+ '';
+
+ # Project-wide HTML from last coverage run (no re-test)
+ cadmus-coverage-show.exec = ''
+ set -e
+ cd "$DEVENV_ROOT"
+ if ! cargo llvm-cov report --html \
+ --ignore-filename-regex '(thirdparty/|mupdf_wrapper/|build\.rs)'; then
+ echo "No coverage data. Run 'cadmus-test-coverage' first." >&2
+ exit 1
+ fi
+ ${openHtml "target/llvm-cov/html/index.html"}
+ '';
+
+ # Patch HTML vs base branch (no re-test). Branch from $1 or $CADMUS_DEFAULT_BRANCH.
+ cadmus-coverage-diff.exec = ''
+ set -e
+ cd "$DEVENV_ROOT"
+ BRANCH="''${1:-$CADMUS_DEFAULT_BRANCH}"
+ LCOV="target/coverage/lcov.info"
+ if [ ! -f "$LCOV" ]; then
+ echo "No coverage data. Run 'cadmus-test-coverage' first." >&2
+ exit 1
+ fi
+ case "$BRANCH" in
+ origin/*) COMPARE_BRANCH="$BRANCH" ;;
+ *) COMPARE_BRANCH="origin/$BRANCH" ;;
+ esac
+ mkdir -p target/coverage
+ diff-cover "$LCOV" \
+ --compare-branch "$COMPARE_BRANCH" \
+ --format "html:target/coverage/diff-coverage.html"
+ ${openHtml "target/coverage/diff-coverage.html"}
+ '';
};
enterShell = ''
@@ -817,6 +874,11 @@ in
echo " cargo xtask run-emulator - Run the emulator (after setup)"
echo " cadmus-translate - Regenerate docs/po/messages.pot (translations on Crowdin)"
echo ""
+ echo "Coverage (diff base: $CADMUS_DEFAULT_BRANCH):"
+ echo " cadmus-test-coverage - Run tests with llvm-cov instrumentation"
+ echo " cadmus-coverage-show - Project-wide HTML report (after test-coverage)"
+ echo " cadmus-coverage-diff - Patch HTML vs origin/\$CADMUS_DEFAULT_BRANCH or [branch] arg"
+ echo ""
echo "xtask commands (cargo xtask --help for options):"
echo " cargo xtask fmt - Check formatting"
echo " cadmus-clippy - Lint lines changed vs master (reviewdog)"
diff --git a/docs/src/contributing/devenv-setup.md b/docs/src/contributing/devenv-setup.md
index 7f800548..6a57c230 100644
--- a/docs/src/contributing/devenv-setup.md
+++ b/docs/src/contributing/devenv-setup.md
@@ -46,19 +46,22 @@ This guide covers setup on both Linux and macOS.
Once inside the devenv shell, these commands are available:
-| Command | Description |
-| ----------------------------- | ------------------------------------------------ |
-| `cargo xtask download-assets` | Download packaged Plato runtime assets |
-| `cargo xtask test` | Run the test suite across the feature matrix |
-| `cargo xtask run-emulator` | Run the emulator |
-| `cargo xtask build-kobo` | Cross-compile for Kobo device |
-| `cargo xtask dist` | Assemble the Kobo distribution directory |
-| `cargo xtask bundle` | Package KoboRoot.tgz for installation |
-| `cadmus-dev-otel` | Run emulator with tracing and profiling enabled |
-| `devenv up` | Start observability stack (Grafana, Tempo, Loki) |
-| `cargo xtask docs` | Build docs site (mdBook, API docs, website) |
-| `cadmus-docs-serve` | Serve website locally on port 3000 |
-| `cadmus-translate` | Generate the docs translation template (.pot) |
+| Command | Description |
+| ----------------------------- | ----------------------------------------------------------- |
+| `cargo xtask download-assets` | Download packaged Plato runtime assets |
+| `cargo xtask test` | Run the test suite across the feature matrix |
+| `cargo xtask run-emulator` | Run the emulator |
+| `cargo xtask build-kobo` | Cross-compile for Kobo device |
+| `cargo xtask dist` | Assemble the Kobo distribution directory |
+| `cargo xtask bundle` | Package KoboRoot.tgz for installation |
+| `cadmus-dev-otel` | Run emulator with tracing and profiling enabled |
+| `devenv up` | Start observability stack (Grafana, Tempo, Loki) |
+| `cargo xtask docs` | Build docs site (mdBook, API docs, website) |
+| `cadmus-docs-serve` | Serve website locally on port 3000 |
+| `cadmus-translate` | Generate the docs translation template (.pot) |
+| `cadmus-test-coverage` | Run tests with coverage instrumentation |
+| `cadmus-coverage-show` | Open project-wide HTML coverage report |
+| `cadmus-coverage-diff` | Open patch coverage HTML vs `origin/$CADMUS_DEFAULT_BRANCH` |
Run `cargo xtask --help` to see all available subcommands, or `cargo xtask --help` for
options on a specific command.
@@ -125,6 +128,38 @@ TEST_ROOT_DIR=$(pwd) cargo test
`TEST_ROOT_DIR` is automatically configured in CI but must be set manually when running
`cargo test` directly.
+## Test Coverage
+
+CI runs instrumented tests across the full feature matrix and uploads merged reports to
+[Codecov](https://codecov.io). Coverage status checks are informational only — test
+failures still block merges.
+
+CI also uploads nextest JUnit XML to [Codecov Test Analytics](https://docs.codecov.com/docs/test-analytics)
+(one upload per feature-matrix shard, with Codecov flags). Doctests are not included in
+JUnit output. View results in the Codecov Tests tab and in PR comments.
+
+Locally, use the devenv coverage commands (requires `devenv shell`):
+
+```bash
+# 1. Run instrumented tests (writes target/coverage/lcov.info)
+cadmus-test-coverage
+
+# 2. View results without re-running tests
+cadmus-coverage-show # project-wide HTML in the browser
+cadmus-coverage-diff # patch HTML vs origin/$CADMUS_DEFAULT_BRANCH
+cadmus-coverage-diff main # or pass an explicit base branch
+```
+
+`CADMUS_DEFAULT_BRANCH` defaults to `master`. Override it in `devenv.local.nix` if needed.
+
+Outside devenv, the equivalent xtask invocation is:
+
+```bash
+cargo xtask test --coverage --features default
+```
+
+CI uploads require a `CODECOV_TOKEN` repository secret (one-time setup on codecov.io).
+
## Platform Support
### Linux (Full Support)
diff --git a/xtask/src/tasks/test.rs b/xtask/src/tasks/test.rs
index d371bada..d8056ace 100644
--- a/xtask/src/tasks/test.rs
+++ b/xtask/src/tasks/test.rs
@@ -7,12 +7,26 @@
//! Each matrix entry runs two passes:
//! 1. `cargo nextest run` — parallel test execution with per-test output.
//! 2. `cargo test --doc` — doctests, which nextest does not execute.
+//!
+//! With `--coverage`, nextest runs under `cargo llvm-cov` instrumentation;
+//! doctests always use plain `cargo test --doc`.
+
+use std::path::{Path, PathBuf};
-use anyhow::{Result, bail};
+use anyhow::{Context, Result, bail};
use clap::Args;
use super::util::{cmd, matrix, workspace};
+/// Paths excluded from coverage reports (vendored code, build scripts).
+const COVERAGE_IGNORE_RE: &str = r"(thirdparty/|mupdf_wrapper/|build\.rs)";
+
+/// Default local LCOV output (consumed by devenv `cadmus-coverage-*` scripts).
+const LOCAL_LCOV_PATH: &str = "target/coverage/lcov.info";
+
+/// JUnit report path when nextest runs with `--profile ci`.
+const NEXTEST_JUNIT_PATH: &str = "target/nextest/ci/junit.xml";
+
/// Arguments for `cargo xtask test`.
#[derive(Debug, Args)]
pub struct TestArgs {
@@ -21,6 +35,24 @@ pub struct TestArgs {
/// When omitted, all matrix entries are run in sequence.
#[arg(long)]
pub features: Option,
+
+ /// Wrap nextest and doctests with `cargo llvm-cov` instrumentation.
+ #[arg(long)]
+ pub coverage: bool,
+
+ /// Write a Codecov-format JSON report to this path (requires `--coverage`).
+ ///
+ /// When `--coverage` is set without this flag, writes [`LOCAL_LCOV_PATH`]
+ /// for local diff-cover / llvm-cov HTML workflows.
+ #[arg(long)]
+ pub save_coverage: Option,
+
+ /// Copy the nextest JUnit report (`target/nextest/ci/junit.xml`) to this path.
+ ///
+ /// Enables the `ci` nextest profile, which writes JUnit XML for Codecov Test
+ /// Analytics. Intended for CI — not enabled by default locally.
+ #[arg(long)]
+ pub save_junit: Option,
}
/// Runs `cargo nextest run` and `cargo test --doc` across the feature matrix
@@ -34,6 +66,10 @@ pub struct TestArgs {
///
/// Returns the first test failure encountered.
pub fn run(args: TestArgs) -> Result<()> {
+ if args.save_coverage.is_some() && !args.coverage {
+ bail!("`--save-coverage` requires `--coverage`");
+ }
+
let root = workspace::root()?;
let root_str = root.to_string_lossy().into_owned();
let env = [("TEST_ROOT_DIR", root_str.as_str())];
@@ -41,18 +77,153 @@ pub fn run(args: TestArgs) -> Result<()> {
let entries = matrix::scan(&root, &["local"])?;
let entries = filter(&entries, args.features.as_deref())?;
- for entry in entries {
- println!("\n==> nextest ({})", entry.label);
+ let junit = args.save_junit.is_some();
+
+ for entry in &entries {
+ run_entry(
+ &root,
+ &env,
+ entry,
+ args.coverage,
+ junit,
+ args.save_junit.as_deref(),
+ )?;
+ }
+
+ if args.coverage {
+ match &args.save_coverage {
+ Some(path) => save_coverage_report(&root, path, "--codecov")?,
+ None => save_coverage_report(&root, Path::new(LOCAL_LCOV_PATH), "--lcov")?,
+ }
+ }
+
+ Ok(())
+}
+
+/// Runs nextest and doctests for a single matrix entry.
+fn run_entry(
+ root: &Path,
+ env: &[(&str, &str)],
+ entry: &matrix::MatrixEntry,
+ coverage: bool,
+ junit: bool,
+ save_junit: Option<&Path>,
+) -> Result<()> {
+ println!("\n==> nextest ({})", entry.label);
+ let nextest_result = run_nextest(root, env, entry, coverage, junit);
+ if let Some(dest) = save_junit {
+ match save_junit_report(root, dest) {
+ Ok(()) => {}
+ Err(e) if nextest_result.is_ok() => return Err(e),
+ Err(e) => eprintln!("warning: {e:#}"),
+ }
+ }
+ nextest_result?;
+
+ println!("\n==> doctest ({})", entry.label);
+ run_plain_doctests(root, env, entry)?;
+
+ Ok(())
+}
+
+fn run_nextest(
+ root: &Path,
+ env: &[(&str, &str)],
+ entry: &matrix::MatrixEntry,
+ coverage: bool,
+ junit: bool,
+) -> Result<()> {
+ let mut args = if coverage {
+ llvm_cov_nextest_args(junit)
+ } else {
+ plain_nextest_args(junit)
+ };
+ args.extend(entry.cargo_args().into_iter().map(str::to_owned));
+ run_cargo(&args, root, env)
+}
+
+fn llvm_cov_nextest_args(junit: bool) -> Vec {
+ let mut args = vec![
+ "llvm-cov".to_owned(),
+ "--ignore-filename-regex".to_owned(),
+ COVERAGE_IGNORE_RE.to_owned(),
+ "nextest".to_owned(),
+ "--all-targets".to_owned(),
+ ];
+ if junit {
+ args.extend(["--profile".to_owned(), "ci".to_owned()]);
+ }
+ args
+}
+
+fn plain_nextest_args(junit: bool) -> Vec {
+ let mut args = vec![
+ "nextest".to_owned(),
+ "run".to_owned(),
+ "--all-targets".to_owned(),
+ ];
+ if junit {
+ args.extend(["--profile".to_owned(), "ci".to_owned()]);
+ }
+ args
+}
+
+fn save_junit_report(root: &Path, dest: &Path) -> Result<()> {
+ let src = root.join(NEXTEST_JUNIT_PATH);
+ if !src.is_file() {
+ bail!(
+ "nextest JUnit report missing at {} (is --profile ci configured in .config/nextest.toml?)",
+ src.display()
+ );
+ }
+ ensure_parent_dir(dest)?;
+ std::fs::copy(&src, dest)
+ .with_context(|| format!("failed to copy {} to {}", src.display(), dest.display()))?;
+ Ok(())
+}
- let mut nextest_args = vec!["nextest", "run", "--all-targets"];
- nextest_args.extend_from_slice(&entry.cargo_args());
- cmd::run("cargo", &nextest_args, &root, &env)?;
+/// Runs `cargo test --doc` for a single matrix entry.
+///
+/// doctest does not support coverage instrumentation, so we run it without it.
+fn run_plain_doctests(
+ root: &Path,
+ env: &[(&str, &str)],
+ entry: &matrix::MatrixEntry,
+) -> Result<()> {
+ let mut args = vec!["test".to_owned(), "--doc".to_owned()];
+ args.extend(entry.cargo_args().into_iter().map(str::to_owned));
+ run_cargo(&args, root, env)
+}
- println!("\n==> doctest ({})", entry.label);
+fn run_cargo(args: &[String], root: &Path, env: &[(&str, &str)]) -> Result<()> {
+ let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
+ cmd::run("cargo", &arg_refs, root, env)
+}
- let mut doctest_args = vec!["test", "--doc"];
- doctest_args.extend_from_slice(&entry.cargo_args());
- cmd::run("cargo", &doctest_args, &root, &env)?;
+fn save_coverage_report(root: &Path, output: &Path, format_flag: &str) -> Result<()> {
+ ensure_parent_dir(output)?;
+ let output_str = output.to_string_lossy();
+ run_cargo(
+ &[
+ "llvm-cov".to_owned(),
+ "report".to_owned(),
+ format_flag.to_owned(),
+ "--output-path".to_owned(),
+ output_str.into_owned(),
+ "--ignore-filename-regex".to_owned(),
+ COVERAGE_IGNORE_RE.to_owned(),
+ ],
+ root,
+ &[],
+ )
+}
+
+fn ensure_parent_dir(path: &Path) -> Result<()> {
+ if let Some(parent) = path.parent()
+ && !parent.as_os_str().is_empty()
+ {
+ std::fs::create_dir_all(parent)
+ .with_context(|| format!("failed to create {}", parent.display()))?;
}
Ok(())
@@ -99,3 +270,64 @@ fn filter<'a>(
Ok(matched)
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn plain_nextest_args_match_cargo_nextest_run() {
+ assert_eq!(
+ plain_nextest_args(false),
+ vec![
+ "nextest".to_owned(),
+ "run".to_owned(),
+ "--all-targets".to_owned(),
+ ]
+ );
+ }
+
+ #[test]
+ fn plain_nextest_args_with_junit_use_ci_profile() {
+ assert_eq!(
+ plain_nextest_args(true),
+ vec![
+ "nextest".to_owned(),
+ "run".to_owned(),
+ "--all-targets".to_owned(),
+ "--profile".to_owned(),
+ "ci".to_owned(),
+ ]
+ );
+ }
+
+ #[test]
+ fn llvm_cov_nextest_args_omit_run_subcommand() {
+ assert_eq!(
+ llvm_cov_nextest_args(false),
+ vec![
+ "llvm-cov".to_owned(),
+ "--ignore-filename-regex".to_owned(),
+ COVERAGE_IGNORE_RE.to_owned(),
+ "nextest".to_owned(),
+ "--all-targets".to_owned(),
+ ]
+ );
+ }
+
+ #[test]
+ fn llvm_cov_nextest_args_with_junit_use_ci_profile() {
+ assert_eq!(
+ llvm_cov_nextest_args(true),
+ vec![
+ "llvm-cov".to_owned(),
+ "--ignore-filename-regex".to_owned(),
+ COVERAGE_IGNORE_RE.to_owned(),
+ "nextest".to_owned(),
+ "--all-targets".to_owned(),
+ "--profile".to_owned(),
+ "ci".to_owned(),
+ ]
+ );
+ }
+}