From 12d6fedda16d6dd183e0d2b6febc39ca3d1125c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 15:25:52 +0000 Subject: [PATCH 01/11] fix: apply --max-depth, --min-ratio, and --no-sort when --json-input is used These options were silently ignored on the --json-input code path, which rendered the deserialized tree verbatim. --max-depth regressed in 0.12.0 when depth limiting moved out of the visualizer, while --min-ratio and --no-sort were never honored there. Apply the same depth retention, insignificant-data culling, and sorting that the filesystem scan performs. https://claude.ai/code/session_01Nj3xEp1eoDKRp1MUCxNQRQ --- src/app.rs | 39 +++++++++++++++++- tests/json.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/app.rs b/src/app.rs index 8c48afd0..ccfdd886 100644 --- a/src/app.rs +++ b/src/app.rs @@ -30,6 +30,17 @@ pub struct App { args: Args, } +/// Tree-shaping options applied to a deserialized `--json-input` tree before visualization. +#[derive(Clone, Copy)] +struct JsonInputShaping { + /// Maximum number of levels to display. + max_depth: u64, + /// Minimal size proportion required to appear. + min_ratio: f32, + /// Whether to preserve the input order of the entries. + no_sort: bool, +} + impl App { /// Initialize the application from the environment. pub fn from_env() -> Self { @@ -58,10 +69,18 @@ impl App { bytes_format, top_down, align_right, + max_depth, + min_ratio, + no_sort, .. } = self.args; let direction = Direction::from_top_down(top_down); let bar_alignment = BarAlignment::from_align_right(align_right); + let shaping = JsonInputShaping { + max_depth: max_depth.get(), + min_ratio: min_ratio.into(), + no_sort, + }; let body = stdin() .pipe(serde_json::from_reader::<_, JsonData>) @@ -75,12 +94,27 @@ impl App { column_width_distribution: ColumnWidthDistribution, direction: Direction, bar_alignment: BarAlignment, + shaping: JsonInputShaping, ) -> Result { let JsonTree { tree, shared } = tree; + let JsonInputShaping { + max_depth, + min_ratio, + no_sort, + } = shaping; - let data_tree = tree + let mut data_tree = tree .par_try_into_tree() - .map_err(|error| RuntimeError::InvalidInputReflection(error.to_string()))?; + .map_err(|error| RuntimeError::InvalidInputReflection(error.to_string()))? + .into_par_retained(|_, depth| depth + 1 < max_depth); + if min_ratio > 0.0 { + data_tree.par_cull_insignificant_data(min_ratio); + } + if !no_sort { + data_tree + .par_sort_by(|left, right| left.size().cmp(&right.size()).reverse()); + } + let visualizer = Visualizer { data_tree: &data_tree, bytes_format, @@ -114,6 +148,7 @@ impl App { column_width_distribution, direction, bar_alignment, + shaping, ) }; } diff --git a/tests/json.rs b/tests/json.rs index 74689526..964af7d1 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -58,6 +58,84 @@ fn sample_tree() -> SampleTree { .into_par_sorted(|left, right| left.size().cmp(&right.size()).reverse()) } +/// Sample tree whose entries are deliberately stored in ascending order of size, +/// which is the opposite of the descending order produced by the default sorting. +fn ascending_sample_tree() -> SampleTree { + let file = + |name: &'static str, size: u64| SampleTree::file(name.to_string(), Bytes::from(size)); + SampleTree::dir( + "root".to_string(), + 1024.into(), + vec![file("a", 50), file("b", 500), file("c", 5000)], + ) +} + +/// Apply the same post-deserialization pipeline that `--json-input` performs, +/// so that the expected visualization can be derived directly from a tree. +fn apply_pipeline(tree: SampleTree, max_depth: u64, min_ratio: f32, no_sort: bool) -> SampleTree { + let mut tree = tree.into_par_retained(|_, depth| depth + 1 < max_depth); + if min_ratio > 0.0 { + tree.par_cull_insignificant_data(min_ratio); + } + if !no_sort { + tree.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse()); + } + tree +} + +/// Render a tree the same way the `--json-input` code path does. +fn visualize(tree: &SampleTree) -> String { + let visualizer = Visualizer { + data_tree: tree, + bytes_format: BytesFormat::MetricUnits, + direction: Direction::BottomUp, + bar_alignment: BarAlignment::Left, + column_width_distribution: ColumnWidthDistribution::total(100), + }; + format!("{visualizer}").trim_end().to_string() +} + +/// Feed a tree to `pdu --json-input` and return its trimmed stdout. +fn run_json_input(tree: SampleTree, extra_args: &[&str]) -> String { + let json_tree = JsonTree { + tree: tree.into_reflection(), + shared: Default::default(), + }; + let json_data = JsonData { + schema_version: SchemaVersion, + binary_version: None, + body: json_tree.into(), + }; + let json = serde_json::to_string_pretty(&json_data).expect("convert sample tree to JSON"); + let workspace = Temp::new_dir().expect("create temporary directory"); + let mut command = Command::new(PDU) + .with_current_dir(&workspace) + .with_arg("--json-input") + .with_arg("--bytes-format=metric") + .with_arg("--total-width=100"); + for arg in extra_args { + command = command.with_arg(*arg); + } + let mut child = command + .with_stdin(Stdio::piped()) + .with_stdout(Stdio::piped()) + .with_stderr(Stdio::piped()) + .spawn() + .expect("spawn command"); + child + .stdin + .as_mut() + .expect("get stdin of child process") + .write_all(json.as_bytes()) + .expect("write JSON string to child process's stdin"); + child + .wait_with_output() + .expect("wait for output of child process") + .pipe(stdout_text) + .trim_end() + .to_string() +} + #[test] fn json_output() { let workspace = SampleWorkspace::default(); @@ -119,6 +197,7 @@ fn json_input() { .with_arg("--bytes-format=metric") .with_arg("--total-width=100") .with_arg("--max-depth=10") + .with_arg("--min-ratio=0") .with_stdin(Stdio::piped()) .with_stdout(Stdio::piped()) .with_stderr(Stdio::piped()) @@ -151,6 +230,39 @@ fn json_input() { assert_eq!(actual, expected); } +#[test] +fn json_input_max_depth() { + let actual = run_json_input(sample_tree(), &["--max-depth=2", "--min-ratio=0"]); + let expected = visualize(&apply_pipeline(sample_tree(), 2, 0.0, false)); + assert_eq!(actual, expected); + + // The truncation must actually drop the deeper levels of the tree. + let untruncated = visualize(&apply_pipeline(sample_tree(), u64::MAX, 0.0, false)); + assert_ne!(expected, untruncated); +} + +#[test] +fn json_input_min_ratio() { + let actual = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0.1"]); + let expected = visualize(&apply_pipeline(sample_tree(), 10, 0.1, false)); + assert_eq!(actual, expected); + + // The culling must actually drop the insignificant entries. + let unculled = visualize(&apply_pipeline(sample_tree(), 10, 0.0, false)); + assert_ne!(expected, unculled); +} + +#[test] +fn json_input_no_sort() { + let actual = run_json_input(ascending_sample_tree(), &["--no-sort", "--min-ratio=0"]); + let expected = visualize(&apply_pipeline(ascending_sample_tree(), 10, 0.0, true)); + assert_eq!(actual, expected); + + // Without `--no-sort` the entries are reordered, proving the flag is honored. + let sorted = run_json_input(ascending_sample_tree(), &["--min-ratio=0"]); + assert_ne!(actual, sorted); +} + #[test] fn json_output_json_input() { let workspace = SampleWorkspace::default(); From e064c3aa87d3e61c99d8c848b9b97ed313922b60 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 26 May 2026 15:50:17 +0000 Subject: [PATCH 02/11] test: add implementation-independent oracle for --json-input --max-depth The existing assertion derives the expected output from the same pipeline the production code runs, so a correlated off-by-one in the depth predicate would pass undetected. Pin the depth boundary by asserting which node names are present and absent, independently of that pipeline. https://claude.ai/code/session_01Nj3xEp1eoDKRp1MUCxNQRQ --- tests/json.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/json.rs b/tests/json.rs index 964af7d1..54070244 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -239,6 +239,12 @@ fn json_input_max_depth() { // The truncation must actually drop the deeper levels of the tree. let untruncated = visualize(&apply_pipeline(sample_tree(), u64::MAX, 0.0, false)); assert_ne!(expected, untruncated); + + // Implementation-independent oracle: with two levels, the root's direct + // children appear while their descendants do not. This pins the depth + // boundary without reusing the pipeline that produces `expected`. + assert!(actual.contains("foo")); + assert!(!actual.contains("subdirectory with a really long name")); } #[test] From f6c6a117e3146d2cd521badeca4ea8c90e3f2925 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 08:46:01 +0000 Subject: [PATCH 03/11] refactor(data_tree): guard against non-positive min-ratio inside par_cull_insignificant_data Move the early-out for non-positive ratios into the method so callers no longer need to wrap it in an `if min_ratio > 0.0` guard. The guard also covers NaN so the method is a no-op for every ratio that is not greater than zero, matching the behavior the call-site guards provided. https://claude.ai/code/session_01Nj3xEp1eoDKRp1MUCxNQRQ --- src/app.rs | 4 +--- src/app/sub.rs | 4 +--- src/data_tree/retain.rs | 5 +++++ tests/json.rs | 4 +--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/app.rs b/src/app.rs index ccfdd886..f48e9870 100644 --- a/src/app.rs +++ b/src/app.rs @@ -107,9 +107,7 @@ impl App { .par_try_into_tree() .map_err(|error| RuntimeError::InvalidInputReflection(error.to_string()))? .into_par_retained(|_, depth| depth + 1 < max_depth); - if min_ratio > 0.0 { - data_tree.par_cull_insignificant_data(min_ratio); - } + data_tree.par_cull_insignificant_data(min_ratio); if !no_sort { data_tree .par_sort_by(|left, right| left.size().cmp(&right.size()).reverse()); diff --git a/src/app/sub.rs b/src/app/sub.rs index f1f72447..577dd4e5 100644 --- a/src/app/sub.rs +++ b/src/app/sub.rs @@ -129,9 +129,7 @@ where let min_ratio: f32 = min_ratio.into(); let (data_tree, deduplication_record) = { let mut data_tree = data_tree; - if min_ratio > 0.0 { - data_tree.par_cull_insignificant_data(min_ratio); - } + data_tree.par_cull_insignificant_data(min_ratio); if !no_sort { data_tree.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse()); } diff --git a/src/data_tree/retain.rs b/src/data_tree/retain.rs index 2de05127..e8fba39a 100644 --- a/src/data_tree/retain.rs +++ b/src/data_tree/retain.rs @@ -36,11 +36,16 @@ where } /// Recursively cull all descendants whose sizes are too small relative to root. + /// + /// A `min_ratio` that is not greater than zero, including `NaN`, leaves the tree unchanged. #[cfg(feature = "cli")] pub fn par_cull_insignificant_data(&mut self, min_ratio: f32) where Size: Into, { + if min_ratio <= 0.0 || min_ratio.is_nan() { + return; + } let minimal = self.size().into() as f32 * min_ratio; self.par_retain(|descendant, _| descendant.size().into() as f32 >= minimal); } diff --git a/tests/json.rs b/tests/json.rs index 54070244..4d35a61d 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -74,9 +74,7 @@ fn ascending_sample_tree() -> SampleTree { /// so that the expected visualization can be derived directly from a tree. fn apply_pipeline(tree: SampleTree, max_depth: u64, min_ratio: f32, no_sort: bool) -> SampleTree { let mut tree = tree.into_par_retained(|_, depth| depth + 1 < max_depth); - if min_ratio > 0.0 { - tree.par_cull_insignificant_data(min_ratio); - } + tree.par_cull_insignificant_data(min_ratio); if !no_sort { tree.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse()); } From b0697c5496560a6fd9a1a9e68324084164fbf83a Mon Sep 17 00:00:00 2001 From: khai96_ Date: Thu, 28 May 2026 15:48:22 +0700 Subject: [PATCH 04/11] docs: remove unnecessary documentation --- src/data_tree/retain.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/data_tree/retain.rs b/src/data_tree/retain.rs index e8fba39a..5b11b798 100644 --- a/src/data_tree/retain.rs +++ b/src/data_tree/retain.rs @@ -36,8 +36,6 @@ where } /// Recursively cull all descendants whose sizes are too small relative to root. - /// - /// A `min_ratio` that is not greater than zero, including `NaN`, leaves the tree unchanged. #[cfg(feature = "cli")] pub fn par_cull_insignificant_data(&mut self, min_ratio: f32) where From 392e394583df801eb4b496dde0e9369dafdf87ed Mon Sep 17 00:00:00 2001 From: khai96_ Date: Thu, 28 May 2026 20:32:23 +0700 Subject: [PATCH 05/11] refactor: use `.with_args` Why is the AI so stupid? --- tests/json.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/json.rs b/tests/json.rs index 4d35a61d..8c6a8e3e 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -106,14 +106,12 @@ fn run_json_input(tree: SampleTree, extra_args: &[&str]) -> String { }; let json = serde_json::to_string_pretty(&json_data).expect("convert sample tree to JSON"); let workspace = Temp::new_dir().expect("create temporary directory"); - let mut command = Command::new(PDU) + let command = Command::new(PDU) .with_current_dir(&workspace) .with_arg("--json-input") .with_arg("--bytes-format=metric") - .with_arg("--total-width=100"); - for arg in extra_args { - command = command.with_arg(*arg); - } + .with_arg("--total-width=100") + .with_args(extra_args); let mut child = command .with_stdin(Stdio::piped()) .with_stdout(Stdio::piped()) From 63fdee4796191f360dd8eb12df0780aad7ab4abb Mon Sep 17 00:00:00 2001 From: khai96_ Date: Thu, 28 May 2026 20:33:20 +0700 Subject: [PATCH 06/11] refactor: full chaining Why, again, is the AI so fucking stupid? --- tests/json.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/json.rs b/tests/json.rs index 8c6a8e3e..73d6bfd9 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -106,13 +106,12 @@ fn run_json_input(tree: SampleTree, extra_args: &[&str]) -> String { }; let json = serde_json::to_string_pretty(&json_data).expect("convert sample tree to JSON"); let workspace = Temp::new_dir().expect("create temporary directory"); - let command = Command::new(PDU) + let mut child = Command::new(PDU) .with_current_dir(&workspace) .with_arg("--json-input") .with_arg("--bytes-format=metric") .with_arg("--total-width=100") - .with_args(extra_args); - let mut child = command + .with_args(extra_args) .with_stdin(Stdio::piped()) .with_stdout(Stdio::piped()) .with_stderr(Stdio::piped()) From d306b488f33b847f44bdc7313235c488baa263e9 Mon Sep 17 00:00:00 2001 From: khai96_ Date: Thu, 28 May 2026 20:51:42 +0700 Subject: [PATCH 07/11] test: log the outputs before assertions --- tests/json.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/json.rs b/tests/json.rs index 73d6bfd9..a3fac1f3 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -245,22 +245,28 @@ fn json_input_max_depth() { #[test] fn json_input_min_ratio() { let actual = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0.1"]); + eprintln!("ACTUAL:\n{actual}\n"); let expected = visualize(&apply_pipeline(sample_tree(), 10, 0.1, false)); assert_eq!(actual, expected); + eprintln!("EXPECTED:\n{expected}\n"); // The culling must actually drop the insignificant entries. let unculled = visualize(&apply_pipeline(sample_tree(), 10, 0.0, false)); + eprintln!("UNCULLED:\n{unculled}\n"); assert_ne!(expected, unculled); } #[test] fn json_input_no_sort() { let actual = run_json_input(ascending_sample_tree(), &["--no-sort", "--min-ratio=0"]); + eprintln!("ACTUAL:\n{actual}\n"); let expected = visualize(&apply_pipeline(ascending_sample_tree(), 10, 0.0, true)); + eprintln!("EXPECTED:\n{expected}\n"); assert_eq!(actual, expected); // Without `--no-sort` the entries are reordered, proving the flag is honored. let sorted = run_json_input(ascending_sample_tree(), &["--min-ratio=0"]); + eprintln!("SORTED:\n{sorted}\n"); assert_ne!(actual, sorted); } From ad0478e9e6d479c255705ae379b66490b24dd6a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 14:07:50 +0000 Subject: [PATCH 08/11] test: add implementation-independent oracle for --json-input --min-ratio The min-ratio test previously verified `actual` only against a value produced by replaying the same culling pipeline as the code under test. Assert directly that an entry far above the threshold survives and one far below it is culled, so the test no longer depends solely on that pipeline. https://claude.ai/code/session_01Nj3xEp1eoDKRp1MUCxNQRQ --- tests/json.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/json.rs b/tests/json.rs index a3fac1f3..1dd017fe 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -254,6 +254,12 @@ fn json_input_min_ratio() { let unculled = visualize(&apply_pipeline(sample_tree(), 10, 0.0, false)); eprintln!("UNCULLED:\n{unculled}\n"); assert_ne!(expected, unculled); + + // Implementation-independent oracle: `foo` is far above the 10% threshold and + // must remain, while `bar` is far below it and must be culled. This pins the + // culling without reusing the pipeline that produces `expected`. + assert!(actual.contains("foo")); + assert!(!actual.contains("bar")); } #[test] From f5c94bae886fcccc48a08dbcf81f1e4eb706d671 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 14:26:54 +0000 Subject: [PATCH 09/11] test: verify --json-input shaping flags without replaying the pipeline Drop the `apply_pipeline` and `visualize` helpers, which computed each expected rendering by re-running the same depth/cull/sort pipeline as the code under test. Verify each flag instead with implementation-independent oracles and real-run differentials: compare against a run with the flag neutralized, and assert directly which entries appear, disappear, or change order. This matches how the other tests in this file derive their oracles. https://claude.ai/code/session_01Nj3xEp1eoDKRp1MUCxNQRQ --- tests/json.rs | 71 ++++++++++++++++++--------------------------------- 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/tests/json.rs b/tests/json.rs index 1dd017fe..889405d7 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -70,29 +70,6 @@ fn ascending_sample_tree() -> SampleTree { ) } -/// Apply the same post-deserialization pipeline that `--json-input` performs, -/// so that the expected visualization can be derived directly from a tree. -fn apply_pipeline(tree: SampleTree, max_depth: u64, min_ratio: f32, no_sort: bool) -> SampleTree { - let mut tree = tree.into_par_retained(|_, depth| depth + 1 < max_depth); - tree.par_cull_insignificant_data(min_ratio); - if !no_sort { - tree.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse()); - } - tree -} - -/// Render a tree the same way the `--json-input` code path does. -fn visualize(tree: &SampleTree) -> String { - let visualizer = Visualizer { - data_tree: tree, - bytes_format: BytesFormat::MetricUnits, - direction: Direction::BottomUp, - bar_alignment: BarAlignment::Left, - column_width_distribution: ColumnWidthDistribution::total(100), - }; - format!("{visualizer}").trim_end().to_string() -} - /// Feed a tree to `pdu --json-input` and return its trimmed stdout. fn run_json_input(tree: SampleTree, extra_args: &[&str]) -> String { let json_tree = JsonTree { @@ -228,52 +205,54 @@ fn json_input() { #[test] fn json_input_max_depth() { let actual = run_json_input(sample_tree(), &["--max-depth=2", "--min-ratio=0"]); - let expected = visualize(&apply_pipeline(sample_tree(), 2, 0.0, false)); - assert_eq!(actual, expected); + eprintln!("ACTUAL:\n{actual}\n"); + let unlimited = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0"]); + eprintln!("UNLIMITED:\n{unlimited}\n"); - // The truncation must actually drop the deeper levels of the tree. - let untruncated = visualize(&apply_pipeline(sample_tree(), u64::MAX, 0.0, false)); - assert_ne!(expected, untruncated); + // Limiting the depth must change the output. + assert_ne!(actual, unlimited); - // Implementation-independent oracle: with two levels, the root's direct - // children appear while their descendants do not. This pins the depth - // boundary without reusing the pipeline that produces `expected`. + // With two levels, the root's direct children appear while their deeper + // descendants do not. `subdirectory with a really long name` lives at depth 2 + // and renders in the unlimited run, so its absence here is caused by the limit. assert!(actual.contains("foo")); assert!(!actual.contains("subdirectory with a really long name")); + assert!(unlimited.contains("subdirectory with a really long name")); } #[test] fn json_input_min_ratio() { let actual = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0.1"]); eprintln!("ACTUAL:\n{actual}\n"); - let expected = visualize(&apply_pipeline(sample_tree(), 10, 0.1, false)); - assert_eq!(actual, expected); - eprintln!("EXPECTED:\n{expected}\n"); - - // The culling must actually drop the insignificant entries. - let unculled = visualize(&apply_pipeline(sample_tree(), 10, 0.0, false)); + let unculled = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0"]); eprintln!("UNCULLED:\n{unculled}\n"); - assert_ne!(expected, unculled); - // Implementation-independent oracle: `foo` is far above the 10% threshold and - // must remain, while `bar` is far below it and must be culled. This pins the - // culling without reusing the pipeline that produces `expected`. + // Culling must change the output. + assert_ne!(actual, unculled); + + // `foo` is far above the 10% threshold and survives, while `bar` is far below + // it and is culled. `bar` renders in the unculled run, so its absence here is + // caused by the culling. assert!(actual.contains("foo")); assert!(!actual.contains("bar")); + assert!(unculled.contains("bar")); } #[test] fn json_input_no_sort() { let actual = run_json_input(ascending_sample_tree(), &["--no-sort", "--min-ratio=0"]); eprintln!("ACTUAL:\n{actual}\n"); - let expected = visualize(&apply_pipeline(ascending_sample_tree(), 10, 0.0, true)); - eprintln!("EXPECTED:\n{expected}\n"); - assert_eq!(actual, expected); - - // Without `--no-sort` the entries are reordered, proving the flag is honored. let sorted = run_json_input(ascending_sample_tree(), &["--min-ratio=0"]); eprintln!("SORTED:\n{sorted}\n"); + + // Sorting must change the output. assert_ne!(actual, sorted); + + // `--no-sort` preserves the ascending input order `a, b, c`, whereas the + // default sorts by descending size, so their relative positions flip. + let position = |text: &str, name: &str| text.find(name).expect("entry must be present"); + assert!(position(&actual, "a") > position(&actual, "c")); + assert!(position(&sorted, "a") < position(&sorted, "c")); } #[test] From d1e6b8baa6038f22b86808377f7feb003f01d3d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 14:52:53 +0000 Subject: [PATCH 10/11] test: use assert-cmp for the order comparisons in json_input_no_sort Replace the bare boolean `assert!` comparisons with `assert_op!` from the already-declared `assert-cmp` crate so a failure reports both operand values. Bind the positions to locals first, as the simple macro form and the `perfectionist::macro-argument-binding` lint both require. https://claude.ai/code/session_01Nj3xEp1eoDKRp1MUCxNQRQ --- tests/json.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/json.rs b/tests/json.rs index 889405d7..f3b48eb3 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -4,6 +4,7 @@ pub mod _utils; pub use _utils::*; +use assert_cmp::assert_op; use command_extra::CommandExtra; use parallel_disk_usage::{ bytes_format::BytesFormat, @@ -251,8 +252,12 @@ fn json_input_no_sort() { // `--no-sort` preserves the ascending input order `a, b, c`, whereas the // default sorts by descending size, so their relative positions flip. let position = |text: &str, name: &str| text.find(name).expect("entry must be present"); - assert!(position(&actual, "a") > position(&actual, "c")); - assert!(position(&sorted, "a") < position(&sorted, "c")); + let actual_a = position(&actual, "a"); + let actual_c = position(&actual, "c"); + assert_op!(actual_a > actual_c); + let sorted_a = position(&sorted, "a"); + let sorted_c = position(&sorted, "c"); + assert_op!(sorted_a < sorted_c); } #[test] From 9495776928abc9a4b5e0d9603c8f6fd833648be1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 28 May 2026 15:04:07 +0000 Subject: [PATCH 11/11] test: allow assert_op_expr in macro_argument_binding and use it directly Add `assert_op_expr` to the rule's `allow_extra` list: the macro evaluates each operand exactly once, so it carries no drop-or-duplicate hazard, unlike the `debug_assert_op*` pair already in `deny_extra`. With the exemption in place, pass the position expressions to `assert_op_expr!` directly instead of binding them to locals first. https://claude.ai/code/session_01Nj3xEp1eoDKRp1MUCxNQRQ --- dylint.toml | 1 + tests/json.rs | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/dylint.toml b/dylint.toml index 7aae5151..0bcf008e 100644 --- a/dylint.toml +++ b/dylint.toml @@ -26,6 +26,7 @@ prefix = [ ["perfectionist::macro_argument_binding"] deny_extra = ["debug_assert_op", "debug_assert_op_expr"] +allow_extra = ["assert_op_expr"] ["perfectionist::single_letter_closure_param"] extra_trivial_callback_methods = ["sort_reflection_by"] diff --git a/tests/json.rs b/tests/json.rs index f3b48eb3..7bc40622 100644 --- a/tests/json.rs +++ b/tests/json.rs @@ -4,7 +4,7 @@ pub mod _utils; pub use _utils::*; -use assert_cmp::assert_op; +use assert_cmp::assert_op_expr; use command_extra::CommandExtra; use parallel_disk_usage::{ bytes_format::BytesFormat, @@ -252,12 +252,8 @@ fn json_input_no_sort() { // `--no-sort` preserves the ascending input order `a, b, c`, whereas the // default sorts by descending size, so their relative positions flip. let position = |text: &str, name: &str| text.find(name).expect("entry must be present"); - let actual_a = position(&actual, "a"); - let actual_c = position(&actual, "c"); - assert_op!(actual_a > actual_c); - let sorted_a = position(&sorted, "a"); - let sorted_c = position(&sorted, "c"); - assert_op!(sorted_a < sorted_c); + assert_op_expr!(position(&actual, "a"), >, position(&actual, "c")); + assert_op_expr!(position(&sorted, "a"), <, position(&sorted, "c")); } #[test]