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(), + ] + ); + } +}