From dcd79a8d2d87c22ee99cc86bd68d63ac45ac3108 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 9 Apr 2026 21:41:59 -0400 Subject: [PATCH 1/3] test(replay): Git history replay test Replays a git repository's history: 1. Parse each commit's diff output and applying patches with diffy 2. Compare against the actual file content at each commit. See the module level doc comment for more. --- Cargo.lock | 52 +++++ Cargo.toml | 1 + tests/replay.rs | 509 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 562 insertions(+) create mode 100644 tests/replay.rs diff --git a/Cargo.lock b/Cargo.lock index 6dcb098..ad290e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,11 +79,37 @@ dependencies = [ "memchr", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "diffy" version = "0.4.2" dependencies = [ "anstyle", + "rayon", "snapbox", ] @@ -93,6 +119,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "errno" version = "0.3.14" @@ -198,6 +230,26 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index cb8a2bf..25c6e51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ color = ["dep:anstyle"] anstyle = { version = "1.0.13", optional = true } [dev-dependencies] +rayon = "1.10.0" snapbox = { version = "0.6.24", features = ["dir"] } [[example]] diff --git a/tests/replay.rs b/tests/replay.rs new file mode 100644 index 0000000..9560dc6 --- /dev/null +++ b/tests/replay.rs @@ -0,0 +1,509 @@ +//! Validate PatchSet parsing and application by replaying a git repository's history. +//! +//! Note: Git extended header paths (rename/copy) don't have a/b prefixes, +//! while ---/+++ paths do. This test handles both cases appropriately. +//! +//! ## Usage +//! +//! ```console +//! $ cargo test --test replay -- --nocapture +//! ``` +//! +//! ## Environment Variables +//! +//! * `DIFFY_TEST_REPO`: Path to the git repository to test against. +//! Defaults to the package directory (`CARGO_MANIFEST_DIR`). +//! * `DIFFY_TEST_COMMITS`: Commits to verify. Accepts either: +//! * A number (e.g., `200`) for the last N commits from HEAD +//! * A range (e.g., `abc123..def456`) for a specific commit range +//! +//! Defaults to 200. Use `0` to verify entire history. +//! * `DIFFY_TEST_PARSE_MODE`: Parse mode to use. +//! Currently only `unidiff` is supported. +//! Defaults to `unidiff`. +//! +//! ## Requirements +//! +//! * Git must be installed and available in the system's PATH. +//! +//! ## Runbook +//! +//! Repo history for upstream projects (e.g., rust-lang/cargo, rust-lang/rust) +//! is too long to run at full depth on every PR. +//! +//! This runbook guide you how run the workflow manually. +//! +//! Replay rust-lang/cargo with deeper history: +//! +//! ```console +//! $ gh workflow run Replay -f repo_url=https://github.com/rust-lang/cargo -f commits=2000 +//! ``` +//! +//! Replay rust-lang/rust with a smaller depth first: +//! +//! ```console +//! $ gh workflow run Replay -f repo_url=https://github.com/rust-lang/rust -f commits=200 +//! ``` +//! +//! Monitor: +//! +//! ```console +//! $ gh run list -w Replay --limit 5 +//! $ gh run view --log-failed +//! ``` + +use std::{ + env, + path::{Path, PathBuf}, + process::Command, + sync::Mutex, +}; + +use diffy::patch_set::{FileOperation, ParseOptions, PatchKind, PatchSet}; +use rayon::prelude::*; + +/// Local enum for test configuration (maps to ParseOptions). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TestMode { + UniDiff, +} + +impl From for ParseOptions { + fn from(value: TestMode) -> Self { + match value { + TestMode::UniDiff => ParseOptions::unidiff(), + } + } +} + +/// Commit selection for replay testing. +enum CommitSelection { + /// Last N commits from HEAD. + Last(usize), + /// Specific commit range (from..to). + Range { from: String, to: String }, +} + +/// Result of processing a single commit pair. +struct CommitResult { + parent_short: String, + child_short: String, + files: Vec, + applied: usize, + skipped: usize, +} + +/// Get the repository path from environment variable. +/// +/// Defaults to package directory if `DIFFY_TEST_REPO` is not set. +fn repo_path() -> PathBuf { + env::var("DIFFY_TEST_REPO") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR"))) +} + +fn commit_selection() -> CommitSelection { + let Ok(val) = env::var("DIFFY_TEST_COMMITS") else { + return CommitSelection::Last(200); + }; + let val = val.trim(); + + // Check for range syntax (from..to) + if let Some((from, to)) = val.split_once("..") { + return CommitSelection::Range { + from: from.to_string(), + to: to.to_string(), + }; + } + + // Parse as number + if val == "0" { + CommitSelection::Last(usize::MAX) + } else { + let n = val + .parse() + .unwrap_or_else(|e| panic!("invalid DIFFY_TEST_COMMITS='{val}': {e}")); + CommitSelection::Last(n) + } +} + +fn test_mode() -> TestMode { + let Ok(val) = env::var("DIFFY_TEST_PARSE_MODE") else { + return TestMode::UniDiff; + }; + match val.trim().to_lowercase().as_str() { + "unidiff" => TestMode::UniDiff, + _ => panic!("invalid DIFFY_TEST_PARSE_MODE='{val}': expected 'unidiff'"), + } +} + +fn git(repo: &Path, args: &[&str]) -> String { + let mut cmd = Command::new("git"); + cmd.env("GIT_CONFIG_NOSYSTEM", "1"); + cmd.env("GIT_CONFIG_GLOBAL", "/dev/null"); + cmd.arg("-C").arg(repo); + cmd.args(args); + + let output = cmd.output().expect("failed to execute git"); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("git {args:?} failed: {stderr}"); + } + + String::from_utf8_lossy(&output.stdout).into_owned() +} + +/// Check if a path is a submodule at a specific commit. +fn is_submodule(repo: &Path, commit: &str, path: &str) -> bool { + let mut cmd = Command::new("git"); + cmd.env("GIT_CONFIG_NOSYSTEM", "1"); + cmd.env("GIT_CONFIG_GLOBAL", "/dev/null"); + cmd.arg("-C").arg(repo); + cmd.args(["ls-tree", "--format=%(objectmode)", commit, "--", path]); + + let output = cmd.output().expect("failed to execute git ls-tree"); + + if !output.status.success() { + return false; + } + + String::from_utf8_lossy(&output.stdout).trim() == "160000" +} + +/// Get file content at a specific commit as bytes. +/// +/// Returns `None` if the path is a submodule. +fn file_at_commit_bytes(repo: &Path, commit: &str, path: &str) -> Option> { + if is_submodule(repo, commit, path) { + return None; + } + + let mut cmd = Command::new("git"); + cmd.env("GIT_CONFIG_NOSYSTEM", "1"); + cmd.env("GIT_CONFIG_GLOBAL", "/dev/null"); + cmd.arg("-C").arg(repo); + cmd.args(["show", &format!("{commit}:{path}")]); + + let output = cmd.output().expect("failed to execute git show"); + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + panic!("file {path} doesn't exist at {commit}: {stderr}"); + } + + Some(output.stdout) +} + +/// Get file content at a specific commit as text. +/// +/// Returns `None` if: +/// +/// * The path is a submodule +/// * The file is binary (not valid UTF-8) +fn file_at_commit(repo: &Path, commit: &str, path: &str) -> Option { + file_at_commit_bytes(repo, commit, path).and_then(|b| String::from_utf8(b).ok()) +} + +/// Get the list of commits from oldest to newest. +fn commit_history(repo: &Path, selection: &CommitSelection) -> Vec { + match selection { + CommitSelection::Last(max) => { + // We want newest N in chronological order, so: fetch newest, then reverse. + // Use --first-parent to ensure consecutive commits are actual parent-child pairs, + // not unrelated commits from different branches before a merge. + let output = if *max == usize::MAX { + git(repo, &["rev-list", "--first-parent", "--reverse", "HEAD"]) + } else { + // fetches only the most recent `max + 1` commits + // to have `max` commit pairs for diffing. + let n = (max + 1).to_string(); + git(repo, &["rev-list", "--first-parent", "-n", &n, "HEAD"]) + }; + let mut commits: Vec<_> = output.lines().map(String::from).collect(); + if *max != usize::MAX { + commits.reverse(); + } + commits + } + CommitSelection::Range { from, to } => { + let range = format!("{from}..{to}"); + let output = git(repo, &["rev-list", "--first-parent", "--reverse", &range]); + let mut commits: Vec<_> = output.lines().map(String::from).collect(); + // Include 'from' commit as the base for diffing + commits.insert(0, from.clone()); + commits + } + } +} + +/// Count type-change entries (`T` status) in `git diff --raw` output. +/// +/// Type changes (e.g., symlink → regular file) produce two patches +/// (delete + create) but only one `--raw` line. +/// +/// Example from llvm/llvm-project 3fa3e65d..caaaf2ee: +/// +/// ```text +/// $ git diff --raw 3fa3e65d caaaf2ee +/// :120000 100644 ca10bf54 dda5db9c T clang/tools/scan-build/c++-analyzer +/// :100755 100755 2b07d6b6 35f852e7 M clang/tools/scan-build/scan-build +/// :000000 100644 00000000 77be6746 A clang/tools/scan-build/scan-build.bat +/// ``` +/// +/// The `T` entry (symlink 120000 → regular file 100644) produces two +/// patches in `git diff` output, while `M` and `A` produce one each. +/// +/// See for +/// the `--raw` format specification. +fn count_type_changes(raw: &str) -> usize { + raw.lines() + .filter(|l| !l.is_empty()) + .filter(|line| { + // --raw format: `:old_mode new_mode old_hash new_hash status\tpath` + line.split('\t') + .next() + .is_some_and(|meta| meta.ends_with(" T")) + }) + .count() +} + +fn process_commit(repo: &Path, parent: &str, child: &str, mode: TestMode) -> CommitResult { + let parent_short = parent[..8].to_string(); + let child_short = child[..8].to_string(); + let mut files = Vec::new(); + let mut applied = 0; + let mut skipped = 0; + + // UniDiff format cannot express pure renames (no ---/+++ headers). + // Use `--no-renames` to represent them as delete + create instead. + let diff_output = match mode { + TestMode::UniDiff => git(repo, &["diff", "--no-renames", parent, child]), + }; + + if diff_output.is_empty() { + // No changes (could be metadata-only commit) + return CommitResult { + parent_short, + child_short, + files, + applied, + skipped, + }; + } + + // Calculate expected file count BEFORE parsing. + // This allows early return for binary-only commits. + // + // Type changes (status `T`, e.g., symlink → regular file) produce two + // patches (delete + create) for one `--raw`/`--numstat` entry, so we + // count them separately and add to the expected total. + // See llvm/llvm-project commits 3fa3e65d..caaaf2ee, d069d2f6..3a7f73d9, + // 2b08718b..06c93976 for examples. + let expected_file_count = match mode { + TestMode::UniDiff => { + // `--numstat` format: + // - `added\tdeleted\tpath` for text files + // - `-\t-\tpath` for binary files (skipped - no patch data in unidiff) + // - `0\t0\tpath` for empty/no-content changes (skipped) + let numstat = git(repo, &["diff", "--numstat", "--no-renames", parent, child]); + let text_files = numstat + .lines() + .filter(|l| !l.is_empty()) + .fold(0, |count, line| { + if line.starts_with("-\t-\t") || line.starts_with("0\t0\t") { + skipped += 1; + count + } else { + count + 1 + } + }); + let raw = git(repo, &["diff", "--raw", "--no-renames", parent, child]); + let type_changes = count_type_changes(&raw); + text_files + type_changes + } + }; + + if expected_file_count == 0 { + return CommitResult { + parent_short, + child_short, + files, + applied, + skipped, + }; + } + + let patchset: Vec<_> = match PatchSet::parse(&diff_output, mode.into()).collect() { + Ok(ps) => ps, + Err(e) => { + panic!( + "Failed to parse patch for {parent_short}..{child_short}: {e}\n\n\ + Diff:\n{diff_output}" + ); + } + }; + + // Verify we parsed the same number of patches as git reports files changed. + // This catches both missing and spurious patches. + if patchset.len() != expected_file_count { + let n = patchset.len(); + panic!( + "Patch count mismatch for {parent_short}..{child_short}: \ + expected {expected_file_count} files, parsed {n} patches\n\n\ + Diff:\n{diff_output}", + ); + } + + for file_patch in patchset.iter() { + // Paths from ---/+++ headers have a/b prefixes that need stripping. + // Paths from git extended headers (rename/copy) are already clean. + let operation = file_patch.operation(); + let strip = match &operation { + FileOperation::Rename { .. } | FileOperation::Copy { .. } => 0, + _ => 1, + }; + let operation = operation.strip_prefix(strip); + + let (base_path, target_path, desc): (Option<&str>, Option<&str>, _) = match &operation { + FileOperation::Create(path) => (None, Some(path.as_ref()), format!("create {path}")), + FileOperation::Delete(path) => (Some(path.as_ref()), None, format!("delete {path}")), + FileOperation::Modify { original, modified } => { + let desc = if original == modified { + format!("modify {original}") + } else { + format!("modify {original} -> {modified}") + }; + (Some(original.as_ref()), Some(modified.as_ref()), desc) + } + FileOperation::Rename { from, to } => ( + Some(from.as_ref()), + Some(to.as_ref()), + format!("rename {from} -> {to}"), + ), + FileOperation::Copy { from, to } => ( + Some(from.as_ref()), + Some(to.as_ref()), + format!("copy {from} -> {to}"), + ), + }; + + match file_patch.patch() { + PatchKind::Text(patch) => { + let base_content = if let Some(path) = base_path { + let Some(content) = file_at_commit(repo, parent, path) else { + skipped += 1; + continue; + }; + content + } else { + String::new() + }; + + let expected_content = if let Some(path) = target_path { + let Some(content) = file_at_commit(repo, child, path) else { + skipped += 1; + continue; + }; + content + } else { + String::new() + }; + + let result = match diffy::apply(&base_content, patch) { + Ok(r) => r, + Err(e) => { + panic!( + "Failed to apply patch at {parent_short}..{child_short} for {desc}: {e}\n\n\ + Patch:\n{patch}\n\n\ + Base content:\n{base_content}" + ); + } + }; + + if result != expected_content { + panic!( + "Content mismatch at {parent_short}..{child_short} for {desc}\n\n\ + --- Expected ---\n{expected_content}\n\n\ + --- Got ---\n{result}\n\n\ + --- Patch ---\n{patch}" + ); + } + } + } + + applied += 1; + files.push(desc); + } + + CommitResult { + parent_short, + child_short, + files, + applied, + skipped, + } +} + +#[test] +fn replay() { + let repo = repo_path(); + let selection = commit_selection(); + let mode = test_mode(); + let commits = commit_history(&repo, &selection); + + if commits.len() < 2 { + panic!("Not enough commits to test"); + } + + let total_diffs = commits.len() - 1; + let repo_name = repo + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| ".".to_string()); + let mode_name = match mode { + TestMode::UniDiff => "unidiff", + }; + + // Shared state for progress reporting + struct Progress { + completed: usize, + total_applied: usize, + total_skipped: usize, + } + + let progress = Mutex::new(Progress { + completed: 0, + total_applied: 0, + total_skipped: 0, + }); + + (0..total_diffs).into_par_iter().for_each(|i| { + let result = process_commit(&repo, &commits[i], &commits[i + 1], mode); + + let completed = { + let mut p = progress.lock().unwrap(); + p.completed += 1; + p.total_applied += result.applied; + p.total_skipped += result.skipped; + p.completed + }; + + eprintln!( + "[{completed}/{total_diffs}] ({repo_name}, {mode_name}) Processing {}..{}", + result.parent_short, result.child_short + ); + for desc in &result.files { + eprintln!(" ✓ {desc}"); + } + }); + + let p = progress.lock().unwrap(); + eprintln!( + "History replay completed: {} patches applied, {} skipped", + p.total_applied, p.total_skipped + ); + + // Sanity check: we should have applied at least some patches + assert!(p.total_applied > 0, "No patches were applied"); +} From 5c21bf00d0f9994775217b4e85974c6011513bba Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Thu, 9 Apr 2026 20:47:00 -0400 Subject: [PATCH 2/3] chore(ci): add replay CI workflows - replay.yml: reusable workflow for `replay` test against any git repo - replay-full.yml: full history replay for selected repos on master push - ci.yml: add replay job matrix against selected repos on PR push Also exclude replay test from default test execution since it requires more args to run correctly --- .github/workflows/ci.yml | 20 +++++++++ .github/workflows/replay-full.yml | 29 ++++++++++++ .github/workflows/replay.yml | 74 +++++++++++++++++++++++++++++++ Cargo.toml | 4 ++ 4 files changed, 127 insertions(+) create mode 100644 .github/workflows/replay-full.yml create mode 100644 .github/workflows/replay.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 388051c..e78a24f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,3 +48,23 @@ jobs: - uses: actions/checkout@v6 - name: Check semver uses: obi1kenobi/cargo-semver-checks-action@v2 + + replay: + strategy: + matrix: + include: + - name: diffy + repo_url: '' + commits: '0' + - name: rust-lang/cargo + repo_url: https://github.com/rust-lang/cargo + commits: '200' + - name: rust-lang/rust + repo_url: https://github.com/rust-lang/rust + commits: '30' + name: replay (${{ matrix.name }}) + uses: ./.github/workflows/replay.yml + with: + name: ${{ matrix.name }} + repo_url: ${{ matrix.repo_url }} + commits: ${{ matrix.commits }} diff --git a/.github/workflows/replay-full.yml b/.github/workflows/replay-full.yml new file mode 100644 index 0000000..3f45e9f --- /dev/null +++ b/.github/workflows/replay-full.yml @@ -0,0 +1,29 @@ +name: Replay (Full History) + +permissions: + contents: read + +on: + push: + branches: + - master + +jobs: + replay-full: + strategy: + matrix: + include: + - name: rust-lang/cargo + repo_url: https://github.com/rust-lang/cargo + - name: rust-lang/rustup + repo_url: https://github.com/rust-lang/rustup + - name: rust-lang/rust-analyzer + repo_url: https://github.com/rust-lang/rust-analyzer + - name: rust-lang/rust-clippy + repo_url: https://github.com/rust-lang/rust-clippy + name: ${{ matrix.name }} + uses: ./.github/workflows/replay.yml + with: + name: ${{ matrix.name }} + repo_url: ${{ matrix.repo_url }} + commits: '0' diff --git a/.github/workflows/replay.yml b/.github/workflows/replay.yml new file mode 100644 index 0000000..5e81742 --- /dev/null +++ b/.github/workflows/replay.yml @@ -0,0 +1,74 @@ +name: Replay + +permissions: + contents: read + +on: + workflow_call: + inputs: + name: + description: 'Display name (if set, job shows only parse_mode)' + required: false + default: '' + type: string + repo_url: + description: 'Git repository URL to clone and test against' + required: false + default: '' + type: string + commits: + description: 'Commits to replay: number (e.g., 200), range (e.g., abc..def), or 0 for all' + required: true + type: string + + workflow_dispatch: + inputs: + repo_url: + description: 'Git repository URL to clone and test against' + required: true + type: string + commits: + description: 'Commits to replay: number (e.g., 200), range (e.g., abc..def), or 0 for all' + required: true + default: '200' + type: string + +env: + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always + CLICOLOR: 1 + CI: 1 + +jobs: + replay: + runs-on: ubuntu-latest + strategy: + matrix: + parse_mode: [unidiff] + name: ${{ inputs.name && matrix.parse_mode || format('{0} ({1}, {2})', inputs.repo_url, matrix.parse_mode, inputs.commits) }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: ${{ inputs.repo_url && 1 || 0 }} + - if: inputs.repo_url != '' + env: + REPO_URL: ${{ inputs.repo_url }} + COMMITS: ${{ inputs.commits }} + run: | + set -euo pipefail + # Guard against non-numeric values when computing --depth. + # Range syntax (a..b) and "0" both require full history. + if [[ "$COMMITS" == *".."* || "$COMMITS" == "0" ]]; then + git clone "$REPO_URL" target/test-repo + elif [[ "$COMMITS" =~ ^[0-9]+$ ]]; then + git clone "$REPO_URL" --depth "$((COMMITS + 1))" target/test-repo + else + echo "invalid commits value: $COMMITS" >&2 + exit 1 + fi + - run: rustup toolchain install stable --profile minimal + - run: cargo test --release --test replay -- --nocapture + env: + DIFFY_TEST_REPO: ${{ inputs.repo_url == '' && '.' || 'target/test-repo' }} + DIFFY_TEST_COMMITS: ${{ inputs.commits }} + DIFFY_TEST_PARSE_MODE: ${{ matrix.parse_mode }} diff --git a/Cargo.toml b/Cargo.toml index 25c6e51..2adbabc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,3 +25,7 @@ snapbox = { version = "0.6.24", features = ["dir"] } [[example]] name = "patch_formatter" required-features = ["color"] + +[[test]] +name = "replay" +test = false From 12507699734b30a970521c92f9fddff414cfb0a2 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Sat, 11 Apr 2026 10:03:56 -0500 Subject: [PATCH 3/3] chore(ci): gate replay test with #[ignore] instead of test = false Previously, `[[test]] test = false` excluded `tests/replay.rs` from `cargo check --tests` and `cargo clippy --all-targets`, so clippy and the type checker silently stopped looking at the file. With this commit, the replay test uses `#[ignore]` to stay out of default `cargo test` runs while remaining visible to lints and compilation. The replay workflow now opts in with `cargo test --test replay -- --ignored --nocapture`. --- .github/workflows/replay.yml | 2 +- Cargo.toml | 4 ---- tests/replay.rs | 4 ++++ 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/replay.yml b/.github/workflows/replay.yml index 5e81742..a9f8f99 100644 --- a/.github/workflows/replay.yml +++ b/.github/workflows/replay.yml @@ -67,7 +67,7 @@ jobs: exit 1 fi - run: rustup toolchain install stable --profile minimal - - run: cargo test --release --test replay -- --nocapture + - run: cargo test --release --test replay -- --ignored --nocapture env: DIFFY_TEST_REPO: ${{ inputs.repo_url == '' && '.' || 'target/test-repo' }} DIFFY_TEST_COMMITS: ${{ inputs.commits }} diff --git a/Cargo.toml b/Cargo.toml index 2adbabc..25c6e51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,3 @@ snapbox = { version = "0.6.24", features = ["dir"] } [[example]] name = "patch_formatter" required-features = ["color"] - -[[test]] -name = "replay" -test = false diff --git a/tests/replay.rs b/tests/replay.rs index 9560dc6..1a7a51f 100644 --- a/tests/replay.rs +++ b/tests/replay.rs @@ -445,7 +445,11 @@ fn process_commit(repo: &Path, parent: &str, child: &str, mode: TestMode) -> Com } } +// Ignored by default so `cargo test` stays fast; CI opts in via `--ignored`. +// Using `#[ignore]` instead of `test = false` keeps the file in clippy's +// `--all-targets` view so lints still fire here. #[test] +#[ignore = "replay test runs git subprocesses; opt in via --ignored"] fn replay() { let repo = repo_path(); let selection = commit_selection();