Skip to content
Merged
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
56 changes: 43 additions & 13 deletions crates/wright-cli/src/present.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,17 @@ impl Drop for Activity {
let _ = handle.join();
}
if self.visible.load(Ordering::Acquire) {
eprint!("\r \r");
let _ = std::io::stderr().flush();
clear_activity_line(&mut std::io::stderr());
}
}
}

fn clear_activity_line(writer: &mut impl Write) {
let _ = write!(writer, "\r\x1b[2K\r");
let _ = writeln!(writer);
let _ = writer.flush();
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Renderer {
Terminal,
Expand Down Expand Up @@ -245,29 +250,35 @@ fn render_verdict<T: serde::Serialize>(
} else {
status.to_string()
};
let summary = match envelope.command.as_str() {
"check" => format!(
"{label} check — {} diagnostic(s)",
envelope.diagnostics.len()
),
println!("{label} {}", envelope.command);
let metadata = match envelope.command.as_str() {
"check" => format!("{} diagnostic(s)", envelope.diagnostics.len()),
"lint" => format!(
"{label} lint — {} finding(s) across {} rule(s)",
"{} finding(s) across {} rule(s)",
array_len(value, "/result/findings"),
array_len(value, "/result/rules"),
),
"analyze" => format!(
"{label} analyze — {} symbol(s), {} rule measurement(s)",
"{} symbol(s), {} rule measurement(s)",
array_len(value, "/result/facts/symbols"),
array_len(value, "/result/facts/rules"),
),
"inspect" => format!(
"{label} inspect — {} rule(s), {} symbol(s)",
"{} rule(s), {} symbol(s)",
array_len(value, "/result/rules"),
array_len(value, "/result/symbols"),
),
other => format!("{label} {other}"),
_ => return,
};
println!("{summary}");
println!(" {}", dim(&metadata, color));
}

fn dim(value: &str, color: bool) -> String {
if color {
format!("\x1b[2m{value}\x1b[0m")
} else {
value.to_string()
}
}

fn array_len(value: &serde_json::Value, pointer: &str) -> usize {
Expand Down Expand Up @@ -527,6 +538,7 @@ fn render_analyze<T: serde::Serialize>(envelope: &Envelope<T>) {
.and_then(serde_json::Value::as_array)
.cloned()
.unwrap_or_default();
println!("\nAnalysis details");
for symbol in &symbols {
let kind = symbol
.get("kind")
Expand Down Expand Up @@ -588,6 +600,10 @@ fn render_lint<T: serde::Serialize>(envelope: &Envelope<T>) {
.and_then(serde_json::Value::as_array)
.cloned()
.unwrap_or_default();
println!("\nLint findings");
if findings.is_empty() {
println!(" none");
}
for finding in &findings {
let code = finding
.get("code")
Expand Down Expand Up @@ -640,7 +656,8 @@ fn render_inspect<T: serde::Serialize>(envelope: &Envelope<T>) {
.get("rules")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
println!("inspect: {count} rule(s), {} symbol(s)", symbols.len());
println!("\nProgram structure");
println!(" {count} rule(s), {} symbol(s)", symbols.len());
for rule in &rules {
let id = rule
.get("id")
Expand Down Expand Up @@ -924,6 +941,19 @@ mod tests {
assert!(activity.visible.load(Ordering::Acquire));
}

#[test]
fn activity_cleanup_clears_the_line_and_terminates_it() {
let mut output = Vec::new();
clear_activity_line(&mut output);
assert_eq!(output, b"\r\x1b[2K\r\n");
}

#[test]
fn metadata_is_dimmed_only_for_terminal_color_output() {
assert_eq!(dim("2 symbols", false), "2 symbols");
assert_eq!(dim("2 symbols", true), "\x1b[2m2 symbols\x1b[0m");
}

#[test]
fn workflow_command_escaping_is_split_by_context() {
assert_eq!(escape_workflow_property("a,b:c%\n"), "a%2Cb%3Ac%25%0A");
Expand Down
25 changes: 25 additions & 0 deletions crates/wright-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,31 @@ fn check_over_clean_input_exits_zero() {
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}

#[test]
fn terminal_renderer_uses_command_specific_hierarchy() {
let path = temp_file("flow.txt", &corpus_workshop("synthetic/control-flow"));
for (command, heading, detail) in [
("check", "PASS check", "diagnostic(s)"),
("lint", "WARN lint", "Lint findings"),
("analyze", "PASS analyze", "Analysis details"),
("inspect", "PASS inspect", "Program structure"),
] {
let output = run(&[
command,
path.to_str().unwrap(),
"--renderer",
"terminal",
"--color",
"never",
]);
assert!(output.status.code().is_some(), "{command} exited by signal");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains(heading), "{command}: {stdout}");
assert!(stdout.contains(detail), "{command}: {stdout}");
}
let _ = std::fs::remove_dir_all(path.parent().unwrap());
}

#[test]
fn check_over_malformed_input_exits_one_with_structured_diagnostics() {
// Enough locale evidence to pass detection, then a syntax error.
Expand Down