327 lines
9.2 KiB
Rust
327 lines
9.2 KiB
Rust
|
|
use serde_json::Value;
|
|||
|
|
use std::fs;
|
|||
|
|
use std::path::{Path, PathBuf};
|
|||
|
|
use std::process::{Command, Output};
|
|||
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|||
|
|
|
|||
|
|
fn temp_project(name: &str) -> PathBuf {
|
|||
|
|
let unique = SystemTime::now()
|
|||
|
|
.duration_since(UNIX_EPOCH)
|
|||
|
|
.unwrap()
|
|||
|
|
.as_nanos();
|
|||
|
|
let root = std::env::temp_dir().join(format!("cstat-file-summary-{name}-{unique}"));
|
|||
|
|
fs::create_dir_all(root.join("src")).unwrap();
|
|||
|
|
fs::write(
|
|||
|
|
root.join("Cargo.toml"),
|
|||
|
|
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
|
|||
|
|
)
|
|||
|
|
.unwrap();
|
|||
|
|
fs::write(
|
|||
|
|
root.join("src/lib.rs"),
|
|||
|
|
r#"
|
|||
|
|
mod other;
|
|||
|
|
|
|||
|
|
use crate::other::external_helper;
|
|||
|
|
use std::fmt::Debug;
|
|||
|
|
|
|||
|
|
const LIMIT: usize = 10;
|
|||
|
|
static NAME: &str = "fixture";
|
|||
|
|
|
|||
|
|
struct Parser;
|
|||
|
|
enum Mode {
|
|||
|
|
Fast,
|
|||
|
|
Slow,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
trait Parse {
|
|||
|
|
fn parse(&self);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn live() {
|
|||
|
|
helper();
|
|||
|
|
external_helper();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn helper() {}
|
|||
|
|
|
|||
|
|
fn orphan() {}
|
|||
|
|
|
|||
|
|
fn dead_parent() {
|
|||
|
|
dead_child();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn dead_child() {}
|
|||
|
|
|
|||
|
|
#[cfg(test)]
|
|||
|
|
mod tests {
|
|||
|
|
use super::*;
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn covers_live() {
|
|||
|
|
live();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
"#,
|
|||
|
|
)
|
|||
|
|
.unwrap();
|
|||
|
|
fs::write(
|
|||
|
|
root.join("src/other.rs"),
|
|||
|
|
r#"
|
|||
|
|
pub fn external_helper() {}
|
|||
|
|
pub fn other_dead() {}
|
|||
|
|
"#,
|
|||
|
|
)
|
|||
|
|
.unwrap();
|
|||
|
|
root
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn run_cstat(path: &Path, args: &[&str]) -> Output {
|
|||
|
|
let bin = env!("CARGO_BIN_EXE_cstat");
|
|||
|
|
let mut command = Command::new(bin);
|
|||
|
|
command.args(["--no-color", "--path"]);
|
|||
|
|
command.arg(path);
|
|||
|
|
command.args(args);
|
|||
|
|
command.output().expect("invoke cstat binary")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn assert_success(output: &Output) {
|
|||
|
|
assert!(
|
|||
|
|
output.status.success(),
|
|||
|
|
"cstat failed: status={:?}\nstderr={}\nstdout={}",
|
|||
|
|
output.status,
|
|||
|
|
String::from_utf8_lossy(&output.stderr),
|
|||
|
|
String::from_utf8_lossy(&output.stdout),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn assert_json_array_contains(array: &Value, expected: &str) {
|
|||
|
|
let values = array.as_array().expect("json array");
|
|||
|
|
assert!(
|
|||
|
|
values.iter().any(|value| value == expected),
|
|||
|
|
"expected {expected:?} in {values:?}",
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn file_summary_human_accepts_file_path() {
|
|||
|
|
let root = temp_project("human");
|
|||
|
|
let file = root.join("src/lib.rs");
|
|||
|
|
let output = run_cstat(&file, &["-v", "summary"]);
|
|||
|
|
assert_success(&output);
|
|||
|
|
|
|||
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|||
|
|
assert!(
|
|||
|
|
stdout.contains("cstat file summary (src/lib.rs)"),
|
|||
|
|
"stdout={stdout}"
|
|||
|
|
);
|
|||
|
|
for section in [
|
|||
|
|
"Projected line reachability",
|
|||
|
|
"Static test/benchmark reachability",
|
|||
|
|
"Symbols",
|
|||
|
|
"Dependencies",
|
|||
|
|
"Dead code candidates",
|
|||
|
|
"Call trace size",
|
|||
|
|
] {
|
|||
|
|
assert!(
|
|||
|
|
stdout.contains(section),
|
|||
|
|
"missing section {section}: {stdout}"
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
for full_section in [
|
|||
|
|
"ℹ Full section: cstat loc --path src/lib.rs -v",
|
|||
|
|
"ℹ Full section: cstat test-reachability --path src/lib.rs -v",
|
|||
|
|
"ℹ Full section: cstat symbols --path src/lib.rs -v",
|
|||
|
|
"ℹ Full section: cstat deps --path src/lib.rs -v",
|
|||
|
|
"ℹ Full section: cstat dead-code --path src/lib.rs -v",
|
|||
|
|
"ℹ Full section: cstat call-trace --path src/lib.rs -v",
|
|||
|
|
] {
|
|||
|
|
assert!(
|
|||
|
|
stdout.contains(full_section),
|
|||
|
|
"missing verbose line {full_section}: {stdout}",
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
for expected in [
|
|||
|
|
"fn 6", "struct 1", "enum 1", "trait 1", "const 1", "static 1",
|
|||
|
|
] {
|
|||
|
|
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
|
|||
|
|
}
|
|||
|
|
assert!(stdout.contains("test/bench roots 1"), "stdout={stdout}");
|
|||
|
|
assert!(
|
|||
|
|
stdout.contains("not statically reachable:"),
|
|||
|
|
"stdout={stdout}"
|
|||
|
|
);
|
|||
|
|
assert!(stdout.contains("orphan"), "stdout={stdout}");
|
|||
|
|
assert!(stdout.contains("dead_child"), "stdout={stdout}");
|
|||
|
|
assert!(stdout.contains("in-file fan-in"), "stdout={stdout}");
|
|||
|
|
for expected in ["Call trace size", "live", "dead_parent", "orphan"] {
|
|||
|
|
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fs::remove_dir_all(root).unwrap();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn file_summary_json_is_untruncated_and_file_scoped() {
|
|||
|
|
let root = temp_project("json");
|
|||
|
|
let file = root.join("src/lib.rs");
|
|||
|
|
let output = run_cstat(&file, &["--json", "summary"]);
|
|||
|
|
assert_success(&output);
|
|||
|
|
|
|||
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
|||
|
|
let value: Value = serde_json::from_str(&stdout).expect("parse file summary JSON");
|
|||
|
|
assert_eq!(value["file"], "src/lib.rs");
|
|||
|
|
assert_eq!(value["project"], "fixture");
|
|||
|
|
for field in [
|
|||
|
|
"line_counts",
|
|||
|
|
"static_reachability",
|
|||
|
|
"symbols",
|
|||
|
|
"dependencies",
|
|||
|
|
"dead_code",
|
|||
|
|
"call_trace",
|
|||
|
|
] {
|
|||
|
|
assert!(value.get(field).is_some(), "missing {field}: {stdout}");
|
|||
|
|
}
|
|||
|
|
assert_eq!(value["symbols"]["functions"], 6);
|
|||
|
|
assert_eq!(value["symbols"]["structs"], 1);
|
|||
|
|
assert_eq!(value["symbols"]["enums"], 1);
|
|||
|
|
assert_eq!(value["symbols"]["traits"], 1);
|
|||
|
|
assert_eq!(value["symbols"]["consts"], 1);
|
|||
|
|
assert_eq!(value["symbols"]["statics"], 1);
|
|||
|
|
assert_eq!(
|
|||
|
|
value["static_reachability"]["test_benchmark_entry_count"],
|
|||
|
|
1
|
|||
|
|
);
|
|||
|
|
assert_json_array_contains(&value["static_reachability"]["reachable_functions"], "live");
|
|||
|
|
assert_json_array_contains(
|
|||
|
|
&value["static_reachability"]["reachable_functions"],
|
|||
|
|
"helper",
|
|||
|
|
);
|
|||
|
|
for function in ["orphan", "dead_parent", "dead_child"] {
|
|||
|
|
assert_json_array_contains(
|
|||
|
|
&value["static_reachability"]["unreachable_functions"],
|
|||
|
|
function,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let dead_code = value["dead_code"]["functions"]
|
|||
|
|
.as_array()
|
|||
|
|
.expect("dead code functions");
|
|||
|
|
let dead_child = dead_code
|
|||
|
|
.iter()
|
|||
|
|
.find(|entry| entry["function"] == "dead_child")
|
|||
|
|
.expect("dead_child dead-code row");
|
|||
|
|
assert_eq!(dead_child["candidate"], true);
|
|||
|
|
assert_eq!(dead_child["in_file_fan_in"], 1);
|
|||
|
|
assert!(
|
|||
|
|
dead_child["reason"]
|
|||
|
|
.as_str()
|
|||
|
|
.unwrap()
|
|||
|
|
.contains("no test/benchmark root reaches that caller"),
|
|||
|
|
"dead_child reason={:?}",
|
|||
|
|
dead_child["reason"]
|
|||
|
|
);
|
|||
|
|
let orphan = dead_code
|
|||
|
|
.iter()
|
|||
|
|
.find(|entry| entry["function"] == "orphan")
|
|||
|
|
.expect("orphan dead-code row");
|
|||
|
|
assert!(
|
|||
|
|
orphan["reason"]
|
|||
|
|
.as_str()
|
|||
|
|
.unwrap()
|
|||
|
|
.contains("no direct, trait, drop, function-item, or closure edge"),
|
|||
|
|
"orphan reason={:?}",
|
|||
|
|
orphan["reason"]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let traces = value["call_trace"]["functions"]
|
|||
|
|
.as_array()
|
|||
|
|
.expect("call trace functions");
|
|||
|
|
let live = traces
|
|||
|
|
.iter()
|
|||
|
|
.find(|entry| entry["function"] == "live")
|
|||
|
|
.expect("live trace row");
|
|||
|
|
assert!(
|
|||
|
|
live["functions_reached"].as_u64().unwrap() > 1,
|
|||
|
|
"live trace={live:?}",
|
|||
|
|
);
|
|||
|
|
let orphan = traces
|
|||
|
|
.iter()
|
|||
|
|
.find(|entry| entry["function"] == "orphan")
|
|||
|
|
.expect("orphan trace row");
|
|||
|
|
assert_eq!(orphan["functions_reached"], 1);
|
|||
|
|
|
|||
|
|
fs::remove_dir_all(root).unwrap();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn file_section_commands_accept_file_path() {
|
|||
|
|
let root = temp_project("sections");
|
|||
|
|
let file = root.join("src/lib.rs");
|
|||
|
|
for command in [
|
|||
|
|
"loc",
|
|||
|
|
"symbols",
|
|||
|
|
"deps",
|
|||
|
|
"dead-code",
|
|||
|
|
"test-reachability",
|
|||
|
|
"call-trace",
|
|||
|
|
] {
|
|||
|
|
let output = run_cstat(&file, &["-v", command]);
|
|||
|
|
assert_success(&output);
|
|||
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|||
|
|
assert!(!stdout.is_empty(), "empty output for {command}");
|
|||
|
|
if command == "symbols" {
|
|||
|
|
for expected in [
|
|||
|
|
"fn 6", "struct 1", "enum 1", "trait 1", "const 1", "static 1",
|
|||
|
|
] {
|
|||
|
|
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if command == "call-trace" {
|
|||
|
|
for expected in ["live", "helper", "other::external_helper"] {
|
|||
|
|
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fs::remove_dir_all(root).unwrap();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn file_path_errors_are_clear() {
|
|||
|
|
let root = temp_project("errors");
|
|||
|
|
let notes = root.join("notes.rs");
|
|||
|
|
fs::write(¬es, "fn scratch() {}\n").unwrap();
|
|||
|
|
|
|||
|
|
let output = run_cstat(¬es, &["summary"]);
|
|||
|
|
assert!(
|
|||
|
|
!output.status.success(),
|
|||
|
|
"unexpected success: stdout={} stderr={}",
|
|||
|
|
String::from_utf8_lossy(&output.stdout),
|
|||
|
|
String::from_utf8_lossy(&output.stderr),
|
|||
|
|
);
|
|||
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|||
|
|
assert!(
|
|||
|
|
stderr.contains("file is not under this crate's src, tests, or benches"),
|
|||
|
|
"stderr={stderr}",
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let output = run_cstat(¬es, &["--json", "summary"]);
|
|||
|
|
assert!(
|
|||
|
|
!output.status.success(),
|
|||
|
|
"unexpected json success: stdout={} stderr={}",
|
|||
|
|
String::from_utf8_lossy(&output.stdout),
|
|||
|
|
String::from_utf8_lossy(&output.stderr),
|
|||
|
|
);
|
|||
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
|||
|
|
let value: Value = serde_json::from_str(&stdout).expect("parse error JSON");
|
|||
|
|
assert!(
|
|||
|
|
value["error"]
|
|||
|
|
.as_str()
|
|||
|
|
.unwrap()
|
|||
|
|
.contains("file is not under this crate's src, tests, or benches"),
|
|||
|
|
"json={value}",
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
fs::remove_dir_all(root).unwrap();
|
|||
|
|
}
|