Skip to content

Commit 9e0e27e

Browse files
author
Martin Taillefer
committed
fix(cargo-gamma): stabilize builds and baseline diagnostics
- Keep whole-workspace Cargo roots stable across staged convergence while physically withdrawing deferred-stage mutants. This preserves dependency feature unification and prevents unrelated mutants from affecting each stage. - Bound Cargo output safely without reserving the full ceiling, and normalize leading ANSI erase sequences so redirected progress remains readable. - Remove the noisy cgroup-containment warning while retaining best-effort process-group cleanup on hosts without delegated cgroups. - Preserve structured evidence for failed baseline runs, including termination, timing, memory, package, target, runner, working directory, and bounded stdout/stderr tails. - Write baseline-failure.json and gamma-diagnostics.json before early cleanup. Artifact failures remain secondary and inherited environment values stay private. - Retain complete failure output only during baseline measurement, leaving successful observations and mutation/confirmation hot paths unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ca9796b-96b2-41c1-b138-4ecbb21aac02
1 parent 7a5daf3 commit 9e0e27e

20 files changed

Lines changed: 941 additions & 219 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ bytes = { version = "1.12.1", default-features = false }
3434
camino = { version = "1.2.5", default-features = false }
3535
# local dependencies
3636
cargo-aprz-lib = { path = "crates/cargo-aprz-lib", default-features = false, version = "1.1.2" }
37-
cargo-gamma-attrs-impl = { path = "crates/cargo-gamma-attrs-impl", default-features = false, version = "0.2.0" }
38-
cargo-gamma-engine = { path = "crates/cargo-gamma-engine", default-features = false, version = "0.2.0" }
39-
cargo-gamma-lib = { path = "crates/cargo-gamma-lib", default-features = false, version = "0.2.0" }
40-
cargo-gamma-process = { path = "crates/cargo-gamma-process", default-features = false, version = "0.2.0" }
41-
cargo-gamma-rt = { path = "crates/cargo-gamma-rt", default-features = false, version = "0.2.0" }
42-
cargo-gamma-unsafe = { path = "crates/cargo-gamma-unsafe", default-features = false, version = "0.2.0" }
37+
cargo-gamma-attrs-impl = { path = "crates/cargo-gamma-attrs-impl", default-features = false, version = "0.2.1" }
38+
cargo-gamma-engine = { path = "crates/cargo-gamma-engine", default-features = false, version = "0.2.1" }
39+
cargo-gamma-lib = { path = "crates/cargo-gamma-lib", default-features = false, version = "0.2.1" }
40+
cargo-gamma-process = { path = "crates/cargo-gamma-process", default-features = false, version = "0.2.1" }
41+
cargo-gamma-rt = { path = "crates/cargo-gamma-rt", default-features = false, version = "0.2.1" }
42+
cargo-gamma-unsafe = { path = "crates/cargo-gamma-unsafe", default-features = false, version = "0.2.1" }
4343
cargo-heather = { path = "crates/cargo-heather", default-features = false, version = "0.2.1" }
4444
# external dependencies
4545
cargo-platform = { version = "0.3.3", default-features = false }

crates/cargo-gamma-attrs-impl/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[package]
55
name = "cargo-gamma-attrs-impl"
66
description = "Implementation of the inert attribute macros exposed by cargo-gamma-attrs"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
readme = "README.md"
99
keywords = ["cargo", "mutation-testing", "testing"]
1010
categories = ["development-tools::testing"]

crates/cargo-gamma-attrs/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[package]
55
name = "cargo-gamma-attrs"
66
description = "Inert attribute macros for suppressing cargo-gamma mutations"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
readme = "README.md"
99
keywords = ["cargo", "mutation-testing", "testing"]
1010
categories = ["development-tools::testing"]

crates/cargo-gamma-engine/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[package]
55
name = "cargo-gamma-engine"
66
description = "Internal Rust source mutation engine for cargo-gamma"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
readme = "README.md"
99
keywords = ["cargo", "mutation-testing", "testing"]
1010
categories = ["development-tools::testing"]

crates/cargo-gamma-lib/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[package]
55
name = "cargo-gamma-lib"
66
description = "Internal library for cargo-gamma"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
readme = "README.md"
99
keywords = ["cargo", "mutation-testing", "testing"]
1010
categories = ["development-tools::testing"]

crates/cargo-gamma-lib/docs/design/README.md renamed to crates/cargo-gamma-lib/docs/DESIGN.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,36 @@ includes both failure to acquire the startup environment and a guard reached
6161
before the runtime constructor installed its selection; either fixed marker
6262
disqualifies the process as mutation-score evidence.
6363

64+
Interactive build progress reuses Cargo's progress text while cargo-gamma owns
65+
the terminal redraw. Cargo's leading erase control is consumed rather than
66+
rendered visibly, and its color styling remains intact.
67+
68+
Each Cargo output stream is retained up to 256 MiB for artifact and diagnostic
69+
processing, and each logical line is bounded at 1 MiB. The buffers grow with
70+
observed output rather than reserving those ceilings for every invocation.
71+
72+
Each failed baseline observation retains a bounded 64 KiB, 200-line tail from
73+
stdout and stderr, along with the process exit code or signal when available.
74+
The baseline error identifies the package, target, runner, executable, working
75+
directory, elapsed time, resource failure, failing or last-observed test, and
76+
the safely encoded output tails. Successful observations retain none of this
77+
output. Before early failure cleanup, the command writes the structured record
78+
to `baseline-failure.json` and writes the ordinary `gamma-diagnostics.json`
79+
bundle; only environment values cargo-gamma explicitly controls are eligible
80+
for diagnostic records, never the inherited process environment.
81+
82+
When Cargo's resolved package selection covers the whole workspace, every stage
83+
checks mutation viability with that constant Cargo root set. Packages with no
84+
mutable files may be omitted from preflight, but do not narrow the staged
85+
checks. This keeps dependency feature unification identical across stages
86+
instead of compiling a new dependency variant for each downstream package
87+
selection. Only the current stage's mutants are instrumented; mutants belonging
88+
to other stages are restored before each ordinary, probe, or isolation build.
89+
Diagnostic blame, isolation, and withdrawal therefore remain limited to the
90+
current stage even though Cargo checks the wider graph. The final test-target
91+
build retains its reachability-based package selection; runs whose original
92+
Cargo selection is a package subset retain their narrowed graph throughout.
93+
6494
Diff paths are resolved to the workspace-relative Rust files discovered by the
6595
survey. Absolute or rooted paths inside the workspace are normalized to those
6696
candidates; one from another checkout is normalized only when its suffix

crates/cargo-gamma-lib/src/commands/run.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ fn run_info(args: &RunArgs, tests: Option<usize>, dropped: &[String]) -> crate::
110110

111111
/// Where a run's documents go, once the defaults and any overrides have been settled.
112112
///
113-
/// Every run writes all five — HTML, JSON and SARIF reports, Markdown advice and a diagnostics bundle.
113+
/// Every completed run writes all five — HTML, JSON and SARIF reports, Markdown advice and a diagnostics bundle.
114114
/// The reason they are produced by default rather than on request is
115115
/// that the flags were a trap: a run that takes an hour and answers a question about your test
116116
/// suite is exactly the run you do not want to repeat because you forgot to ask for the artifact
@@ -817,6 +817,10 @@ fn measured<H: Host>(host: &mut H, args: &RunArgs, progress_when: When, styler:
817817

818818
let log_result = events.finish_verdict_log();
819819

820+
if let Err(failure) = &outcome {
821+
emit_failure_artifacts(&mut events, args, &survey.skeleton(), failure, &artifact_dir, started, styler);
822+
}
823+
820824
let exec::Measured {
821825
plan,
822826
built,
@@ -928,6 +932,44 @@ fn measured<H: Host>(host: &mut H, args: &RunArgs, progress_when: When, styler:
928932
Ok(Executed { plan: Some(plan), stuck })
929933
}
930934

935+
fn emit_failure_artifacts<H: Host>(
936+
events: &mut ConsoleEvents<'_, H>,
937+
args: &RunArgs,
938+
plan: &Plan,
939+
failure: &Error,
940+
artifact_dir: &Utf8Path,
941+
started: Instant,
942+
styler: Styler,
943+
) {
944+
let Some(artifact) = failure.artifact() else {
945+
return;
946+
};
947+
948+
let path = artifact_dir.join(artifact.file_name);
949+
let written = serde_json::to_string_pretty(&artifact.value)
950+
.map_err(|cause| error!("could not encode `{path}`").caused_by(cause))
951+
.and_then(|contents| crate::elements::write(&path, &contents));
952+
953+
match written {
954+
Ok(()) => {
955+
let _announced = writeln!(events.host.error(), "{} {path}", styler.verb("Wrote"));
956+
}
957+
Err(artifact_failure) => {
958+
crate::exec::Events::warn(
959+
events,
960+
&format!("could not preserve the baseline failure record: {artifact_failure}"),
961+
);
962+
}
963+
}
964+
965+
if let Err(diag_failure) = emit_diag(events.host, args, plan, None, started, styler) {
966+
crate::exec::Events::warn(
967+
events,
968+
&format!("could not preserve the diagnostics bundle after the baseline failure: {diag_failure}"),
969+
);
970+
}
971+
}
972+
931973
fn warn_auxiliary<H: Host>(host: &mut H, failure: Option<&Error>, styler: Styler) {
932974
if let Some(failure) = failure {
933975
let _ = writeln!(host.error(), "{} {failure}", styler.warning());
@@ -1739,6 +1781,42 @@ mod tests {
17391781
assert!(String::from_utf8(host.err).expect("utf-8").contains(path.as_str()));
17401782
}
17411783

1784+
#[test]
1785+
fn a_baseline_error_writes_its_record_and_the_diagnostics_bundle() {
1786+
let dir = workdir("run-baseline-failure-artifacts-");
1787+
let root = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).expect("utf8");
1788+
let args = RunArgs {
1789+
artifact_dir: Some(root.clone()),
1790+
..Default::default()
1791+
};
1792+
let plan = plan();
1793+
let failure = Error::new("baseline failed").with_artifact(
1794+
"baseline-failure.json",
1795+
serde_json::json!({
1796+
"schemaVersion": 1,
1797+
"kind": "testFailure",
1798+
"stderrTail": "assertion failed",
1799+
}),
1800+
);
1801+
let mut host = Sink::default();
1802+
let mut events = ConsoleEvents {
1803+
host: &mut host,
1804+
progress: Progress::new(false, Styler::new(false), Some(80)),
1805+
styler: Styler::new(false),
1806+
estimate: false,
1807+
show_build: false,
1808+
verdict_log: VerdictLog::default(),
1809+
};
1810+
1811+
emit_failure_artifacts(&mut events, &args, &plan, &failure, &root, Instant::now(), Styler::new(false));
1812+
1813+
let baseline = fs::read_to_string(root.join("baseline-failure.json")).expect("baseline record");
1814+
let diagnostics = fs::read_to_string(root.join("gamma-diagnostics.json")).expect("diagnostics bundle");
1815+
1816+
assert!(baseline.contains("assertion failed"), "{baseline}");
1817+
assert!(diagnostics.contains("\"schemaVersion\": \"3\""), "{diagnostics}");
1818+
}
1819+
17421820
/// A run that censused says so in its bundle: the phase carries its own elapsed time, the tests
17431821
/// it walked, and the binaries it examined, so the census's cost is legible on its own rather
17441822
/// than folded inside the build's total.

crates/cargo-gamma-lib/src/error.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ use std::backtrace::Backtrace;
99
use std::io;
1010

1111
use cargo_gamma_engine::{Error as EngineError, Parts as EngineParts};
12+
use serde_json::Value;
13+
14+
/// A machine-readable diagnostic that should be persisted before an error unwinds its run state.
15+
#[derive(Debug)]
16+
pub(crate) struct Artifact {
17+
pub(crate) file_name: &'static str,
18+
pub(crate) value: Value,
19+
}
1220

1321
/// An error carrying a human-readable message and an optional cause.
1422
///
@@ -20,6 +28,7 @@ pub struct Error {
2028
cause: Option<Box<dyn StdError + Send + Sync>>,
2129
usage: bool,
2230
skippable: bool,
31+
artifact: Option<Box<Artifact>>,
2332

2433
/// Captured at construction, unconditionally.
2534
///
@@ -39,6 +48,7 @@ impl Error {
3948
cause: None,
4049
usage: false,
4150
skippable: false,
51+
artifact: None,
4252
backtrace: Backtrace::capture(),
4353
}
4454
}
@@ -81,6 +91,18 @@ impl Error {
8191
self.skippable
8292
}
8393

94+
/// Attaches a machine-readable diagnostic for the command layer to persist.
95+
#[must_use]
96+
pub(crate) fn with_artifact(mut self, file_name: &'static str, value: Value) -> Self {
97+
self.artifact = Some(Box::new(Artifact { file_name, value }));
98+
self
99+
}
100+
101+
/// Returns the diagnostic attached to this failure, when it has one.
102+
pub(crate) fn artifact(&self) -> Option<&Artifact> {
103+
self.artifact.as_deref()
104+
}
105+
84106
/// Attaches an underlying cause.
85107
#[must_use]
86108
pub fn caused_by(mut self, cause: impl StdError + Send + Sync + 'static) -> Self {
@@ -148,6 +170,7 @@ impl From<EngineError> for Error {
148170
cause,
149171
usage,
150172
skippable,
173+
artifact: None,
151174
backtrace,
152175
}
153176
}

0 commit comments

Comments
 (0)