test(pi-shell): add minimizer fixture harness with savings gate
Walks tests/fixtures/minimizer/<family>/<case>.{cmd,raw,exit,min}:
fixtures >=500 bytes must minimize to <=40% of raw size; smaller ones
pin exact snapshots. Seeded with three synthetic fixtures. Donor
parity fixtures from rtk/snip land on this harness in later commits.
Op: extend
This commit is contained in:
@@ -0,0 +1 @@
|
||||
cargo test --workspace
|
||||
@@ -0,0 +1,48 @@
|
||||
Compiling proc-macro2 v1.0.86
|
||||
Compiling unicode-ident v1.0.12
|
||||
Compiling libc v0.2.155
|
||||
Compiling autocfg v1.3.0
|
||||
Compiling quote v1.0.36
|
||||
Compiling syn v2.0.72
|
||||
Compiling serde_derive v1.0.204
|
||||
Compiling serde v1.0.204
|
||||
Compiling thiserror-impl v1.0.63
|
||||
Compiling thiserror v1.0.63
|
||||
Compiling pi-shell v0.1.0 (/home/alpha/odin/omp/crates/pi-shell)
|
||||
warning: unused variable: `ctx`
|
||||
--> crates/pi-shell/src/minimizer/filters/mod.rs:134:30
|
||||
|
|
||||
134 | pub fn filter(ctx: &MinimizerCtx, input: &str) -> MinimizerOutput {
|
||||
| ^^^ help: if this is intentional, prefix it with an underscore: `_ctx`
|
||||
|
|
||||
= note: `#[warn(unused_variables)]` on by default
|
||||
Finished `test` profile [unoptimized + debuginfo] target(s) in 12.84s
|
||||
Running unittests src/lib.rs (target/debug/deps/pi_shell-9a1b2c3d4e5f6a7b)
|
||||
|
||||
running 42 tests
|
||||
test minimizer::detect::tests::detects_git_status ... ok
|
||||
test minimizer::detect::tests::detects_cargo_test ... ok
|
||||
test minimizer::engine::tests::enabled_known_filter_minimizes ... ok
|
||||
test minimizer::engine::tests::unknown_command_is_passthrough ... ok
|
||||
test minimizer::filters::cargo::tests::passing_run_summarizes ... ok
|
||||
test minimizer::filters::go::tests::go_test_pass_condenses ... ok
|
||||
test minimizer::plan::tests::single_command_is_single ... ok
|
||||
test minimizer::plan::tests::chain_splits_on_and ... ok
|
||||
|
||||
test result: ok. 42 passed; 0 failed; 0 ignored; 0 measured; 0 filtered; finished in 0.31s
|
||||
|
||||
Running tests/minimizer_fixtures.rs (target/debug/deps/minimizer_fixtures-1122334455667788)
|
||||
|
||||
running 8 tests
|
||||
test fixture_cargo_test_pass ... ok
|
||||
test fixture_npm_install ... ok
|
||||
test fixture_go_test_pass ... ok
|
||||
|
||||
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered; finished in 0.02s
|
||||
|
||||
Doc-tests pi-shell
|
||||
|
||||
running 3 tests
|
||||
test crates/pi-shell/src/minimizer.rs - minimizer (line 12) ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered; finished in 0.45s
|
||||
@@ -0,0 +1 @@
|
||||
go test -v ./...
|
||||
@@ -0,0 +1,61 @@
|
||||
=== RUN TestParseConfig
|
||||
=== PAUSE TestParseConfig
|
||||
=== CONT TestParseConfig
|
||||
=== RUN TestParseConfig/empty_input
|
||||
=== PAUSE TestParseConfig/empty_input
|
||||
=== CONT TestParseConfig/empty_input
|
||||
=== RUN TestParseConfig/with_defaults
|
||||
=== PAUSE TestParseConfig/with_defaults
|
||||
=== CONT TestParseConfig/with_defaults
|
||||
=== RUN TestParseConfig/with_overrides
|
||||
=== PAUSE TestParseConfig/with_overrides
|
||||
=== CONT TestParseConfig/with_overrides
|
||||
--- PASS: TestParseConfig (0.00s)
|
||||
=== RUN TestEncodeDecode
|
||||
=== PAUSE TestEncodeDecode
|
||||
=== CONT TestEncodeDecode
|
||||
=== RUN TestEncodeDecode/utf8
|
||||
=== PAUSE TestEncodeDecode/utf8
|
||||
=== CONT TestEncodeDecode/utf8
|
||||
=== RUN TestEncodeDecode/binary
|
||||
=== PAUSE TestEncodeDecode/binary
|
||||
=== CONT TestEncodeDecode/binary
|
||||
--- PASS: TestEncodeDecode (0.01s)
|
||||
=== RUN TestRoundTrip
|
||||
=== PAUSE TestRoundTrip
|
||||
=== CONT TestRoundTrip
|
||||
--- PASS: TestRoundTrip (0.02s)
|
||||
PASS
|
||||
ok github.com/example/project/internal/config 0.031s
|
||||
=== RUN TestHandlerGet
|
||||
=== PAUSE TestHandlerGet
|
||||
=== CONT TestHandlerGet
|
||||
=== RUN TestHandlerPost
|
||||
=== PAUSE TestHandlerPost
|
||||
=== CONT TestHandlerPost
|
||||
=== RUN TestHandlerDelete
|
||||
=== PAUSE TestHandlerDelete
|
||||
=== CONT TestHandlerDelete
|
||||
--- PASS: TestHandlerGet (0.00s)
|
||||
--- PASS: TestHandlerPost (0.00s)
|
||||
--- PASS: TestHandlerDelete (0.00s)
|
||||
PASS
|
||||
ok github.com/example/project/internal/server 0.044s
|
||||
=== RUN TestRouterMatch
|
||||
=== PAUSE TestRouterMatch
|
||||
=== CONT TestRouterMatch
|
||||
=== RUN TestRouterFallback
|
||||
=== PAUSE TestRouterFallback
|
||||
=== CONT TestRouterFallback
|
||||
--- PASS: TestRouterMatch (0.00s)
|
||||
--- PASS: TestRouterFallback (0.00s)
|
||||
PASS
|
||||
ok github.com/example/project/internal/router 0.012s
|
||||
=== RUN TestMiddlewareChain
|
||||
=== PAUSE TestMiddlewareChain
|
||||
=== CONT TestMiddlewareChain
|
||||
--- PASS: TestMiddlewareChain (0.00s)
|
||||
PASS
|
||||
ok github.com/example/project/internal/middleware 0.009s
|
||||
? github.com/example/project/cmd/cli [no test files]
|
||||
ok github.com/example/project/pkg/util 0.008s
|
||||
@@ -0,0 +1 @@
|
||||
npm install
|
||||
@@ -0,0 +1,29 @@
|
||||
npm http fetch GET 200 https://registry.npmjs.org/react 142ms
|
||||
npm http fetch GET 200 https://registry.npmjs.org/react-dom 138ms
|
||||
npm http fetch GET 200 https://registry.npmjs.org/lodash 95ms
|
||||
npm http fetch GET 200 https://registry.npmjs.org/webpack 210ms
|
||||
npm http fetch GET 200 https://registry.npmjs.org/typescript 188ms
|
||||
npm http fetch GET 200 https://registry.npmjs.org/eslint 176ms
|
||||
npm http fetch GET 200 https://registry.npmjs.org/jest 201ms
|
||||
npm http fetch GET 200 https://registry.npmjs.org/babel-core 165ms
|
||||
Progress: resolved 1 / 1248, reused 0, downloaded 1, added 0
|
||||
Progress: resolved 312 / 1248, reused 120, downloaded 192, added 64
|
||||
Progress: resolved 640 / 1248, reused 280, downloaded 360, added 210
|
||||
Progress: resolved 980 / 1248, reused 510, downloaded 470, added 540
|
||||
Progress: resolved 1248 / 1248, reused 800, downloaded 448, added 1248
|
||||
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it.
|
||||
npm warn deprecated rimraf@2.7.1: Rimraf versions prior to v4 are no longer supported.
|
||||
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported.
|
||||
npm notice
|
||||
npm notice New major version of npm available! 9.8.1 -> 10.2.4
|
||||
npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.2.4
|
||||
npm notice Run npm install -g npm@10.2.4 to update!
|
||||
npm notice
|
||||
resolved 1248 packages in 8s
|
||||
reused 800, downloaded 448, added 1248
|
||||
added 1248 packages, and audited 1249 packages in 14s
|
||||
|
||||
142 packages are looking for funding
|
||||
run `npm fund` for details
|
||||
|
||||
found 0 vulnerabilities
|
||||
@@ -0,0 +1,248 @@
|
||||
//! End-to-end fixture harness for the output minimizer with a token-savings
|
||||
//! gate.
|
||||
//!
|
||||
//! Each fixture is a `<family>/<case>` pair on disk under
|
||||
//! `tests/fixtures/minimizer/`:
|
||||
//!
|
||||
//! - `<case>.cmd` — the full command line (one line; required).
|
||||
//! - `<case>.raw` — the raw captured output fed to the minimizer (required).
|
||||
//! - `<case>.exit` — integer exit code (optional; defaults to 0).
|
||||
//! - `<case>.min` — expected minimized snapshot (optional, see gate below).
|
||||
//!
|
||||
//! Gate per fixture (`raw` measured in bytes):
|
||||
//!
|
||||
//! - `raw.len() >= 500`: assert `minimized.len() <= 0.40 * raw.len()` — the
|
||||
//! savings gate. A short output is not worth a filter round-trip, so the gate
|
||||
//! only applies to buffers large enough to matter.
|
||||
//! - `raw.len() < 500`: a `.min` snapshot is REQUIRED and must match exactly.
|
||||
//! Small buffers cannot meaningfully clear a ratio gate, so they are pinned
|
||||
//! by an exact snapshot instead.
|
||||
//! - `.min` present alongside a `>= 500`-byte raw: assert BOTH the savings gate
|
||||
//! and the exact snapshot.
|
||||
//!
|
||||
//! All fixture failures are collected before the harness panics so a single run
|
||||
//! reports every regression, not just the first.
|
||||
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use pi_shell::minimizer::{self, MinimizerConfig};
|
||||
|
||||
/// Byte-savings gate: minimized output must be at most this fraction of the raw
|
||||
/// input for buffers large enough to be worth filtering.
|
||||
const SAVINGS_RATIO: f64 = 0.40;
|
||||
|
||||
/// Raw buffers below this byte length are pinned by an exact `.min` snapshot
|
||||
/// instead of the ratio gate.
|
||||
const GATE_MIN_BYTES: usize = 500;
|
||||
|
||||
/// A single discovered fixture: the `.cmd`/`.raw` pair plus its optional
|
||||
/// `.exit` and `.min` companions.
|
||||
struct Fixture {
|
||||
/// `<family>/<case>` — used only for human-readable failure reports.
|
||||
name: String,
|
||||
command: String,
|
||||
raw: String,
|
||||
exit: i32,
|
||||
expected: Option<String>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimizer_fixtures_clear_savings_gate() {
|
||||
let root = fixtures_root();
|
||||
let fixtures = discover_fixtures(&root);
|
||||
|
||||
// A silently-empty harness is worse than none: if the fixtures tree is
|
||||
// missing or holds no `.cmd` cases, fail loudly rather than passing on zero
|
||||
// assertions.
|
||||
assert!(
|
||||
!fixtures.is_empty(),
|
||||
"no minimizer fixtures discovered under {}: the harness must run against at least one \
|
||||
fixture (a silently-empty gate is worse than none)",
|
||||
root.display()
|
||||
);
|
||||
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
let mut failures: Vec<String> = Vec::new();
|
||||
|
||||
for fixture in &fixtures {
|
||||
if let Err(report) = check_fixture(fixture, &cfg) {
|
||||
failures.push(report);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
failures.is_empty(),
|
||||
"{} of {} minimizer fixture(s) failed:\n\n{}",
|
||||
failures.len(),
|
||||
fixtures.len(),
|
||||
failures.join("\n\n")
|
||||
);
|
||||
}
|
||||
|
||||
/// Run the minimizer on one fixture and apply the gate. Returns `Err(report)`
|
||||
/// with a human-readable failure description (fixture name, ratio, diff
|
||||
/// excerpt) on any violation so the caller can collect every failure.
|
||||
fn check_fixture(fixture: &Fixture, cfg: &MinimizerConfig) -> Result<(), String> {
|
||||
let out = minimizer::apply(&fixture.command, &fixture.raw, fixture.exit, cfg);
|
||||
let minimized = out.text.as_str();
|
||||
|
||||
let raw_len = fixture.raw.len();
|
||||
let min_len = minimized.len();
|
||||
let ratio = if raw_len == 0 {
|
||||
0.0
|
||||
} else {
|
||||
min_len as f64 / raw_len as f64
|
||||
};
|
||||
|
||||
let mut problems: Vec<String> = Vec::new();
|
||||
|
||||
if raw_len >= GATE_MIN_BYTES {
|
||||
let budget = (SAVINGS_RATIO * raw_len as f64).floor() as usize;
|
||||
if min_len > budget {
|
||||
problems.push(format!(
|
||||
"savings gate: minimized {min_len} B > {budget} B budget ({:.1}% of {raw_len} B raw, \
|
||||
limit {:.0}%)",
|
||||
ratio * 100.0,
|
||||
SAVINGS_RATIO * 100.0
|
||||
));
|
||||
}
|
||||
// A `.min` alongside a large raw pins the exact shape too.
|
||||
if let Some(expected) = &fixture.expected
|
||||
&& expected != minimized
|
||||
{
|
||||
problems.push(format!("snapshot mismatch:\n{}", diff_excerpt(expected, minimized)));
|
||||
}
|
||||
} else {
|
||||
// Small buffers cannot meaningfully clear a ratio gate; require an exact
|
||||
// snapshot instead.
|
||||
match &fixture.expected {
|
||||
None => problems.push(format!(
|
||||
"raw is {raw_len} B (< {GATE_MIN_BYTES} B): a `.min` snapshot is required for \
|
||||
sub-threshold fixtures"
|
||||
)),
|
||||
Some(expected) if expected != minimized => {
|
||||
problems.push(format!("snapshot mismatch:\n{}", diff_excerpt(expected, minimized)));
|
||||
},
|
||||
Some(_) => {},
|
||||
}
|
||||
}
|
||||
|
||||
if problems.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"[{}] cmd={:?} exit={} raw={} B min={} B ratio={:.1}%\n - {}",
|
||||
fixture.name,
|
||||
fixture.command,
|
||||
fixture.exit,
|
||||
raw_len,
|
||||
min_len,
|
||||
ratio * 100.0,
|
||||
problems.join("\n - ")
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a short, line-oriented diff excerpt (first divergence plus a little
|
||||
/// context) so a snapshot failure is legible without dumping both buffers.
|
||||
fn diff_excerpt(expected: &str, actual: &str) -> String {
|
||||
let expected_lines: Vec<&str> = expected.lines().collect();
|
||||
let actual_lines: Vec<&str> = actual.lines().collect();
|
||||
let max = expected_lines.len().max(actual_lines.len());
|
||||
|
||||
let first_diff = (0..max).find(|&i| expected_lines.get(i) != actual_lines.get(i));
|
||||
let Some(start) = first_diff else {
|
||||
// Lines all match: the difference is a trailing newline / final fragment.
|
||||
return format!(" expected {expected:?}\n actual {actual:?}");
|
||||
};
|
||||
|
||||
let mut excerpt = String::new();
|
||||
for i in start..(start + 3).min(max) {
|
||||
excerpt.push_str(&format!(
|
||||
" line {}:\n expected {:?}\n actual {:?}\n",
|
||||
i + 1,
|
||||
expected_lines.get(i).copied().unwrap_or("<missing>"),
|
||||
actual_lines.get(i).copied().unwrap_or("<missing>")
|
||||
));
|
||||
}
|
||||
excerpt.trim_end().to_string()
|
||||
}
|
||||
|
||||
/// Absolute path to the fixtures tree, anchored at the crate manifest dir so
|
||||
/// the harness loads identically whether run as an integration test or under a
|
||||
/// different working directory.
|
||||
fn fixtures_root() -> std::path::PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/minimizer")
|
||||
}
|
||||
|
||||
/// Walk `<root>/<family>/<case>.cmd` and assemble each fixture with its
|
||||
/// `.raw`/`.exit`/`.min` companions. A missing `.raw` for a discovered `.cmd`
|
||||
/// is a hard error: the fixture is malformed, not absent.
|
||||
fn discover_fixtures(root: &Path) -> Vec<Fixture> {
|
||||
let mut fixtures = Vec::new();
|
||||
|
||||
let Ok(families) = fs::read_dir(root) else {
|
||||
// Missing tree → return empty; the test body turns this into a loud
|
||||
// failure with the full path.
|
||||
return fixtures;
|
||||
};
|
||||
|
||||
let mut family_dirs: Vec<_> = families
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir())
|
||||
.collect();
|
||||
family_dirs.sort();
|
||||
|
||||
for family_dir in family_dirs {
|
||||
let family = family_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("?")
|
||||
.to_string();
|
||||
|
||||
let Ok(entries) = fs::read_dir(&family_dir) else {
|
||||
continue;
|
||||
};
|
||||
let mut cmd_paths: Vec<_> = entries
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.extension().is_some_and(|ext| ext == "cmd"))
|
||||
.collect();
|
||||
cmd_paths.sort();
|
||||
|
||||
for cmd_path in cmd_paths {
|
||||
let case = cmd_path
|
||||
.file_stem()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("?")
|
||||
.to_string();
|
||||
let raw_path = cmd_path.with_extension("raw");
|
||||
let raw = fs::read_to_string(&raw_path).unwrap_or_else(|err| {
|
||||
panic!(
|
||||
"fixture {family}/{case}: missing or unreadable .raw ({}): {err}",
|
||||
raw_path.display()
|
||||
)
|
||||
});
|
||||
|
||||
let command = fs::read_to_string(&cmd_path)
|
||||
.unwrap_or_else(|err| panic!("fixture {family}/{case}: unreadable .cmd: {err}"))
|
||||
.trim_end_matches(['\n', '\r'])
|
||||
.to_string();
|
||||
|
||||
let exit = fs::read_to_string(cmd_path.with_extension("exit"))
|
||||
.ok()
|
||||
.map_or(0, |text| {
|
||||
text.trim().parse::<i32>().unwrap_or_else(|err| {
|
||||
panic!("fixture {family}/{case}: invalid .exit ({:?}): {err}", text.trim())
|
||||
})
|
||||
});
|
||||
|
||||
let expected = fs::read_to_string(cmd_path.with_extension("min")).ok();
|
||||
|
||||
fixtures.push(Fixture { name: format!("{family}/{case}"), command, raw, exit, expected });
|
||||
}
|
||||
}
|
||||
|
||||
fixtures
|
||||
}
|
||||
Reference in New Issue
Block a user