Skip to content

Commit 12d6fed

Browse files
committed
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
1 parent 3b728c1 commit 12d6fed

2 files changed

Lines changed: 149 additions & 2 deletions

File tree

src/app.rs

Lines changed: 37 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,27 @@ 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+
if min_ratio > 0.0 {
111+
data_tree.par_cull_insignificant_data(min_ratio);
112+
}
113+
if !no_sort {
114+
data_tree
115+
.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse());
116+
}
117+
84118
let visualizer = Visualizer {
85119
data_tree: &data_tree,
86120
bytes_format,
@@ -114,6 +148,7 @@ impl App {
114148
column_width_distribution,
115149
direction,
116150
bar_alignment,
151+
shaping,
117152
)
118153
};
119154
}

tests/json.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,84 @@ fn sample_tree() -> SampleTree {
5858
.into_par_sorted(|left, right| left.size().cmp(&right.size()).reverse())
5959
}
6060

61+
/// Sample tree whose entries are deliberately stored in ascending order of size,
62+
/// which is the opposite of the descending order produced by the default sorting.
63+
fn ascending_sample_tree() -> SampleTree {
64+
let file =
65+
|name: &'static str, size: u64| SampleTree::file(name.to_string(), Bytes::from(size));
66+
SampleTree::dir(
67+
"root".to_string(),
68+
1024.into(),
69+
vec![file("a", 50), file("b", 500), file("c", 5000)],
70+
)
71+
}
72+
73+
/// Apply the same post-deserialization pipeline that `--json-input` performs,
74+
/// so that the expected visualization can be derived directly from a tree.
75+
fn apply_pipeline(tree: SampleTree, max_depth: u64, min_ratio: f32, no_sort: bool) -> SampleTree {
76+
let mut tree = tree.into_par_retained(|_, depth| depth + 1 < max_depth);
77+
if min_ratio > 0.0 {
78+
tree.par_cull_insignificant_data(min_ratio);
79+
}
80+
if !no_sort {
81+
tree.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse());
82+
}
83+
tree
84+
}
85+
86+
/// Render a tree the same way the `--json-input` code path does.
87+
fn visualize(tree: &SampleTree) -> String {
88+
let visualizer = Visualizer {
89+
data_tree: tree,
90+
bytes_format: BytesFormat::MetricUnits,
91+
direction: Direction::BottomUp,
92+
bar_alignment: BarAlignment::Left,
93+
column_width_distribution: ColumnWidthDistribution::total(100),
94+
};
95+
format!("{visualizer}").trim_end().to_string()
96+
}
97+
98+
/// Feed a tree to `pdu --json-input` and return its trimmed stdout.
99+
fn run_json_input(tree: SampleTree, extra_args: &[&str]) -> String {
100+
let json_tree = JsonTree {
101+
tree: tree.into_reflection(),
102+
shared: Default::default(),
103+
};
104+
let json_data = JsonData {
105+
schema_version: SchemaVersion,
106+
binary_version: None,
107+
body: json_tree.into(),
108+
};
109+
let json = serde_json::to_string_pretty(&json_data).expect("convert sample tree to JSON");
110+
let workspace = Temp::new_dir().expect("create temporary directory");
111+
let mut command = Command::new(PDU)
112+
.with_current_dir(&workspace)
113+
.with_arg("--json-input")
114+
.with_arg("--bytes-format=metric")
115+
.with_arg("--total-width=100");
116+
for arg in extra_args {
117+
command = command.with_arg(*arg);
118+
}
119+
let mut child = command
120+
.with_stdin(Stdio::piped())
121+
.with_stdout(Stdio::piped())
122+
.with_stderr(Stdio::piped())
123+
.spawn()
124+
.expect("spawn command");
125+
child
126+
.stdin
127+
.as_mut()
128+
.expect("get stdin of child process")
129+
.write_all(json.as_bytes())
130+
.expect("write JSON string to child process's stdin");
131+
child
132+
.wait_with_output()
133+
.expect("wait for output of child process")
134+
.pipe(stdout_text)
135+
.trim_end()
136+
.to_string()
137+
}
138+
61139
#[test]
62140
fn json_output() {
63141
let workspace = SampleWorkspace::default();
@@ -119,6 +197,7 @@ fn json_input() {
119197
.with_arg("--bytes-format=metric")
120198
.with_arg("--total-width=100")
121199
.with_arg("--max-depth=10")
200+
.with_arg("--min-ratio=0")
122201
.with_stdin(Stdio::piped())
123202
.with_stdout(Stdio::piped())
124203
.with_stderr(Stdio::piped())
@@ -151,6 +230,39 @@ fn json_input() {
151230
assert_eq!(actual, expected);
152231
}
153232

233+
#[test]
234+
fn json_input_max_depth() {
235+
let actual = run_json_input(sample_tree(), &["--max-depth=2", "--min-ratio=0"]);
236+
let expected = visualize(&apply_pipeline(sample_tree(), 2, 0.0, false));
237+
assert_eq!(actual, expected);
238+
239+
// The truncation must actually drop the deeper levels of the tree.
240+
let untruncated = visualize(&apply_pipeline(sample_tree(), u64::MAX, 0.0, false));
241+
assert_ne!(expected, untruncated);
242+
}
243+
244+
#[test]
245+
fn json_input_min_ratio() {
246+
let actual = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0.1"]);
247+
let expected = visualize(&apply_pipeline(sample_tree(), 10, 0.1, false));
248+
assert_eq!(actual, expected);
249+
250+
// The culling must actually drop the insignificant entries.
251+
let unculled = visualize(&apply_pipeline(sample_tree(), 10, 0.0, false));
252+
assert_ne!(expected, unculled);
253+
}
254+
255+
#[test]
256+
fn json_input_no_sort() {
257+
let actual = run_json_input(ascending_sample_tree(), &["--no-sort", "--min-ratio=0"]);
258+
let expected = visualize(&apply_pipeline(ascending_sample_tree(), 10, 0.0, true));
259+
assert_eq!(actual, expected);
260+
261+
// Without `--no-sort` the entries are reordered, proving the flag is honored.
262+
let sorted = run_json_input(ascending_sample_tree(), &["--min-ratio=0"]);
263+
assert_ne!(actual, sorted);
264+
}
265+
154266
#[test]
155267
fn json_output_json_input() {
156268
let workspace = SampleWorkspace::default();

0 commit comments

Comments
 (0)