Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
37 changes: 35 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,25 @@ 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);
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 +146,7 @@ impl App {
column_width_distribution,
direction,
bar_alignment,
shaping,
)
};
}
Expand Down
104 changes: 104 additions & 0 deletions tests/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,56 @@ 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)],
)
}

/// 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 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)
.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 +169,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 +202,59 @@ 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"]);
eprintln!("ACTUAL:\n{actual}\n");
let unlimited = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0"]);
eprintln!("UNLIMITED:\n{unlimited}\n");

// Limiting the depth must change the output.
assert_ne!(actual, unlimited);

// 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 unculled = run_json_input(sample_tree(), &["--max-depth=10", "--min-ratio=0"]);
eprintln!("UNCULLED:\n{unculled}\n");

// 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 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"));
Comment thread
KSXGitHub marked this conversation as resolved.
Outdated
}

#[test]
fn json_output_json_input() {
let workspace = SampleWorkspace::default();
Expand Down
Loading