Skip to content

Commit 4b0f192

Browse files
KSXGitHubclaude
andauthored
fix: stop ignoring --max-depth, --min-ratio, --no-sort on --json-input (#424)
https://claude.ai/code/session_01Nj3xEp1eoDKRp1MUCxNQRQ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5b06136 commit 4b0f192

3 files changed

Lines changed: 141 additions & 2 deletions

File tree

dylint.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ prefix = [
2626

2727
["perfectionist::macro_argument_binding"]
2828
deny_extra = ["debug_assert_op", "debug_assert_op_expr"]
29+
allow_extra = ["assert_op_expr"]
2930

3031
["perfectionist::single_letter_closure_param"]
3132
extra_trivial_callback_methods = ["sort_reflection_by"]

src/app.rs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ pub struct App {
3030
args: Args,
3131
}
3232

33+
/// Tree-shaping options applied to a deserialized `--json-input` tree before visualization.
34+
#[derive(Clone, Copy)]
35+
struct JsonInputShaping {
36+
/// Maximum number of levels to display.
37+
max_depth: u64,
38+
/// Minimal size proportion required to appear.
39+
min_ratio: f32,
40+
/// Whether to preserve the input order of the entries.
41+
no_sort: bool,
42+
}
43+
3344
impl App {
3445
/// Initialize the application from the environment.
3546
pub fn from_env() -> Self {
@@ -58,10 +69,18 @@ impl App {
5869
bytes_format,
5970
top_down,
6071
align_right,
72+
max_depth,
73+
min_ratio,
74+
no_sort,
6175
..
6276
} = self.args;
6377
let direction = Direction::from_top_down(top_down);
6478
let bar_alignment = BarAlignment::from_align_right(align_right);
79+
let shaping = JsonInputShaping {
80+
max_depth: max_depth.get(),
81+
min_ratio: min_ratio.into(),
82+
no_sort,
83+
};
6584

6685
let body = stdin()
6786
.pipe(serde_json::from_reader::<_, JsonData>)
@@ -75,12 +94,25 @@ impl App {
7594
column_width_distribution: ColumnWidthDistribution,
7695
direction: Direction,
7796
bar_alignment: BarAlignment,
97+
shaping: JsonInputShaping,
7898
) -> Result<String, RuntimeError> {
7999
let JsonTree { tree, shared } = tree;
100+
let JsonInputShaping {
101+
max_depth,
102+
min_ratio,
103+
no_sort,
104+
} = shaping;
80105

81-
let data_tree = tree
106+
let mut data_tree = tree
82107
.par_try_into_tree()
83-
.map_err(|error| RuntimeError::InvalidInputReflection(error.to_string()))?;
108+
.map_err(|error| RuntimeError::InvalidInputReflection(error.to_string()))?
109+
.into_par_retained(|_, depth| depth + 1 < max_depth);
110+
data_tree.par_cull_insignificant_data(min_ratio);
111+
if !no_sort {
112+
data_tree
113+
.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse());
114+
}
115+
84116
let visualizer = Visualizer {
85117
data_tree: &data_tree,
86118
bytes_format,
@@ -114,6 +146,7 @@ impl App {
114146
column_width_distribution,
115147
direction,
116148
bar_alignment,
149+
shaping,
117150
)
118151
};
119152
}

tests/json.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
pub mod _utils;
55
pub use _utils::*;
66

7+
use assert_cmp::assert_op_expr;
78
use command_extra::CommandExtra;
89
use parallel_disk_usage::{
910
bytes_format::BytesFormat,
@@ -58,6 +59,56 @@ fn sample_tree() -> SampleTree {
5859
.into_par_sorted(|left, right| left.size().cmp(&right.size()).reverse())
5960
}
6061

62+
/// Sample tree whose entries are deliberately stored in ascending order of size,
63+
/// which is the opposite of the descending order produced by the default sorting.
64+
fn ascending_sample_tree() -> SampleTree {
65+
let file =
66+
|name: &'static str, size: u64| SampleTree::file(name.to_string(), Bytes::from(size));
67+
SampleTree::dir(
68+
"root".to_string(),
69+
1024.into(),
70+
vec![file("a", 50), file("b", 500), file("c", 5000)],
71+
)
72+
}
73+
74+
/// Feed a tree to `pdu --json-input` and return its trimmed stdout.
75+
fn run_json_input(tree: SampleTree, extra_args: &[&str]) -> String {
76+
let json_tree = JsonTree {
77+
tree: tree.into_reflection(),
78+
shared: Default::default(),
79+
};
80+
let json_data = JsonData {
81+
schema_version: SchemaVersion,
82+
binary_version: None,
83+
body: json_tree.into(),
84+
};
85+
let json = serde_json::to_string_pretty(&json_data).expect("convert sample tree to JSON");
86+
let workspace = Temp::new_dir().expect("create temporary directory");
87+
let mut child = Command::new(PDU)
88+
.with_current_dir(&workspace)
89+
.with_arg("--json-input")
90+
.with_arg("--bytes-format=metric")
91+
.with_arg("--total-width=100")
92+
.with_args(extra_args)
93+
.with_stdin(Stdio::piped())
94+
.with_stdout(Stdio::piped())
95+
.with_stderr(Stdio::piped())
96+
.spawn()
97+
.expect("spawn command");
98+
child
99+
.stdin
100+
.as_mut()
101+
.expect("get stdin of child process")
102+
.write_all(json.as_bytes())
103+
.expect("write JSON string to child process's stdin");
104+
child
105+
.wait_with_output()
106+
.expect("wait for output of child process")
107+
.pipe(stdout_text)
108+
.trim_end()
109+
.to_string()
110+
}
111+
61112
#[test]
62113
fn json_output() {
63114
let workspace = SampleWorkspace::default();
@@ -119,6 +170,7 @@ fn json_input() {
119170
.with_arg("--bytes-format=metric")
120171
.with_arg("--total-width=100")
121172
.with_arg("--max-depth=10")
173+
.with_arg("--min-ratio=0")
122174
.with_stdin(Stdio::piped())
123175
.with_stdout(Stdio::piped())
124176
.with_stderr(Stdio::piped())
@@ -151,6 +203,59 @@ fn json_input() {
151203
assert_eq!(actual, expected);
152204
}
153205

206+
#[test]
207+
fn json_input_max_depth() {
208+
let actual = run_json_input(sample_tree(), &["--max-depth=2", "--min-ratio=0"]);
209+
eprintln!("ACTUAL:\n{actual}\n");
210+
let unlimited = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0"]);
211+
eprintln!("UNLIMITED:\n{unlimited}\n");
212+
213+
// Limiting the depth must change the output.
214+
assert_ne!(actual, unlimited);
215+
216+
// With two levels, the root's direct children appear while their deeper
217+
// descendants do not. `subdirectory with a really long name` lives at depth 2
218+
// and renders in the unlimited run, so its absence here is caused by the limit.
219+
assert!(actual.contains("foo"));
220+
assert!(!actual.contains("subdirectory with a really long name"));
221+
assert!(unlimited.contains("subdirectory with a really long name"));
222+
}
223+
224+
#[test]
225+
fn json_input_min_ratio() {
226+
let actual = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0.1"]);
227+
eprintln!("ACTUAL:\n{actual}\n");
228+
let unculled = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0"]);
229+
eprintln!("UNCULLED:\n{unculled}\n");
230+
231+
// Culling must change the output.
232+
assert_ne!(actual, unculled);
233+
234+
// `foo` is far above the 10% threshold and survives, while `bar` is far below
235+
// it and is culled. `bar` renders in the unculled run, so its absence here is
236+
// caused by the culling.
237+
assert!(actual.contains("foo"));
238+
assert!(!actual.contains("bar"));
239+
assert!(unculled.contains("bar"));
240+
}
241+
242+
#[test]
243+
fn json_input_no_sort() {
244+
let actual = run_json_input(ascending_sample_tree(), &["--no-sort", "--min-ratio=0"]);
245+
eprintln!("ACTUAL:\n{actual}\n");
246+
let sorted = run_json_input(ascending_sample_tree(), &["--min-ratio=0"]);
247+
eprintln!("SORTED:\n{sorted}\n");
248+
249+
// Sorting must change the output.
250+
assert_ne!(actual, sorted);
251+
252+
// `--no-sort` preserves the ascending input order `a, b, c`, whereas the
253+
// default sorts by descending size, so their relative positions flip.
254+
let position = |text: &str, name: &str| text.find(name).expect("entry must be present");
255+
assert_op_expr!(position(&actual, "a"), >, position(&actual, "c"));
256+
assert_op_expr!(position(&sorted, "a"), <, position(&sorted, "c"));
257+
}
258+
154259
#[test]
155260
fn json_output_json_input() {
156261
let workspace = SampleWorkspace::default();

0 commit comments

Comments
 (0)