cstat/tests/symbols_cli.rs

296 lines
8 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};
const LIB_RS: &str = r#"const LIMIT: usize = 10;
static NAME: &str = "fixture";
struct Parser;
enum Mode {
Fast,
}
trait Parse {
fn parse(&self);
}
impl Parser {
fn new() -> Self {
Self
}
}
impl Parse for Parser {
fn parse(&self) {}
}
fn free() {
let _ = LIMIT;
}
mod inline {
pub struct Inner;
pub const FLAG: bool = true;
pub fn nested() {}
}
"#;
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-symbols-{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"), LIB_RS).unwrap();
fs::write(root.join("src/broken.rs"), "fn broken(\n").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 parse_json(output: &Output) -> Value {
serde_json::from_slice(&output.stdout).expect("parse symbols JSON")
}
fn find_file<'a>(value: &'a Value, path: &str) -> &'a Value {
value["files"]
.as_array()
.expect("files array")
.iter()
.find(|entry| entry["path"] == path)
.unwrap_or_else(|| panic!("missing file row {path}: {value}"))
}
fn assert_zero_symbol_counts(value: &Value) {
for field in [
"total",
"functions",
"structs",
"enums",
"traits",
"trait_impls",
"consts",
"statics",
] {
assert_eq!(value[field], 0, "expected zero {field}: {value}");
}
}
fn find_symbol<'a>(rows: &'a [Value], kind: &str, symbol: &str) -> &'a Value {
rows.iter()
.find(|row| row["kind"] == kind && row["symbol"] == symbol)
.unwrap_or_else(|| panic!("missing {kind} symbol {symbol}: {rows:?}"))
}
#[test]
fn project_json_reports_trait_impl_contract_and_parse_errors() {
let root = temp_project("project-json");
let output = run_cstat(&root, &["--json", "symbols"]);
assert_success(&output);
let value = parse_json(&output);
assert!(value.get("totals").is_some(), "json={value}");
assert!(value.get("files").is_some(), "json={value}");
let totals = &value["totals"];
assert_eq!(totals["total"], 12);
assert_eq!(totals["functions"], 4);
assert_eq!(totals["structs"], 2);
assert_eq!(totals["enums"], 1);
assert_eq!(totals["traits"], 1);
assert_eq!(totals["trait_impls"], 1);
assert_eq!(totals["consts"], 2);
assert_eq!(totals["statics"], 1);
assert_eq!(totals["parse_error_files"], 1);
assert!(
totals.get("impls").is_none(),
"old impls key present: {totals}"
);
let lib = find_file(&value, "src/lib.rs");
assert_eq!(lib["total"], 12);
assert_eq!(lib["functions"], 4);
assert_eq!(lib["structs"], 2);
assert_eq!(lib["enums"], 1);
assert_eq!(lib["traits"], 1);
assert_eq!(lib["trait_impls"], 1);
assert_eq!(lib["consts"], 2);
assert_eq!(lib["statics"], 1);
assert_eq!(lib["parse_error"], false);
assert!(lib.get("impls").is_none(), "old impls key present: {lib}");
let broken = find_file(&value, "src/broken.rs");
assert_eq!(broken["parse_error"], true);
assert_zero_symbol_counts(broken);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn selected_file_json_reports_rows_spans_and_schema() {
let root = temp_project("selected-json");
let file = root.join("src/lib.rs");
let output = run_cstat(&file, &["--json", "symbols"]);
assert_success(&output);
let value = parse_json(&output);
for field in [
"file",
"total",
"functions",
"structs",
"enums",
"traits",
"trait_impls",
"consts",
"statics",
"parse_error",
"symbols",
] {
assert!(value.get(field).is_some(), "missing {field}: {value}");
}
assert_eq!(value["file"], "src/lib.rs");
assert_eq!(value["parse_error"], false);
assert_eq!(value["total"], 12);
assert_eq!(value["functions"], 4);
assert_eq!(value["structs"], 2);
assert_eq!(value["enums"], 1);
assert_eq!(value["traits"], 1);
assert_eq!(value["trait_impls"], 1);
assert_eq!(value["consts"], 2);
assert_eq!(value["statics"], 1);
assert!(
value.get("impls").is_none(),
"old impls key present: {value}"
);
let rows = value["symbols"].as_array().expect("symbols array");
let fn_rows = rows.iter().filter(|row| row["kind"] == "fn").count();
assert_eq!(fn_rows, 4, "rows={rows:?}");
let free = find_symbol(rows, "fn", "free");
assert_eq!(free["line_start"], 24);
assert_eq!(free["line_end"], 26);
let inherent_method = find_symbol(rows, "fn", "Parser::new");
assert_eq!(inherent_method["line_start"], 15);
assert_eq!(inherent_method["line_end"], 17);
find_symbol(rows, "fn", "inline::nested");
let trait_impl_rows: Vec<&Value> = rows
.iter()
.filter(|row| row["kind"] == "trait_impl")
.collect();
assert_eq!(trait_impl_rows.len(), 1, "rows={rows:?}");
let trait_impl = trait_impl_rows[0];
assert_eq!(trait_impl["symbol"], "<Parser as Parse>");
assert_eq!(trait_impl["line_start"], 20);
assert_eq!(trait_impl["line_end"], 22);
find_symbol(rows, "const", "inline::FLAG");
find_symbol(rows, "static", "NAME");
fs::remove_dir_all(root).unwrap();
}
#[test]
fn selected_file_json_surfaces_parse_error() {
let root = temp_project("parse-error");
let file = root.join("src/broken.rs");
let output = run_cstat(&file, &["--json", "symbols"]);
assert_success(&output);
let value = parse_json(&output);
assert_eq!(value["file"], "src/broken.rs");
assert_eq!(value["parse_error"], true);
assert_zero_symbol_counts(&value);
assert!(
value["symbols"]
.as_array()
.expect("symbols array")
.is_empty(),
"json={value}"
);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn symbols_human_verbose_explains_contract() {
let root = temp_project("human-verbose");
let output = run_cstat(&root, &["-v", "symbols"]);
assert_success(&output);
let stdout = String::from_utf8_lossy(&output.stdout);
for expected in [
"Rust AST",
"syn",
"semantic name resolution",
"public API usage",
"trait_impl",
"inherent",
"Parse-error",
"const",
"static",
] {
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
}
fs::remove_dir_all(root).unwrap();
}
#[test]
fn symbols_help_mentions_file_path_and_trait_impls() {
let root = temp_project("help");
let root_help = run_cstat(&root, &["--help"]);
assert_success(&root_help);
let root_stdout = String::from_utf8_lossy(&root_help.stdout);
assert!(
root_stdout.contains("Rust project directory or Rust source file"),
"stdout={root_stdout}"
);
let symbols_help = run_cstat(&root, &["symbols", "--help"]);
assert_success(&symbols_help);
let symbols_stdout = String::from_utf8_lossy(&symbols_help.stdout);
assert!(
symbols_stdout.contains("Rust AST symbol counts"),
"stdout={symbols_stdout}"
);
assert!(
symbols_stdout.contains("trait impl"),
"stdout={symbols_stdout}"
);
fs::remove_dir_all(root).unwrap();
}