Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>)
Expand All @@ -75,12 +94,27 @@ impl App {
column_width_distribution: ColumnWidthDistribution,
direction: Direction,
bar_alignment: BarAlignment,
shaping: JsonInputShaping,
) -> Result<String, RuntimeError> {
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,
Expand Down Expand Up @@ -114,6 +148,7 @@ impl App {
column_width_distribution,
direction,
bar_alignment,
shaping,
)
};
}
Expand Down
118 changes: 118 additions & 0 deletions tests/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -151,6 +230,45 @@ 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);

// 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]
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();
Expand Down
Loading