308 lines
8.9 KiB
Rust
308 lines
8.9 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-loc-cli-{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#"
|
|
pub fn live() {
|
|
helper();
|
|
}
|
|
|
|
/*
|
|
block comment only
|
|
*/
|
|
fn helper() {}
|
|
|
|
/* single-line block comment only */
|
|
fn orphan() {
|
|
// comment-only
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn covers_live() {
|
|
live();
|
|
}
|
|
}
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
root
|
|
}
|
|
|
|
fn run_cstat_raw(args: &[&str]) -> Output {
|
|
let bin = env!("CARGO_BIN_EXE_cstat");
|
|
let mut command = Command::new(bin);
|
|
command.args(args);
|
|
command.output().expect("invoke cstat binary")
|
|
}
|
|
|
|
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 span_array_contains_function(array: &Value, expected: &str) -> bool {
|
|
array
|
|
.as_array()
|
|
.expect("span array")
|
|
.iter()
|
|
.any(|item| item.get("function").and_then(Value::as_str) == Some(expected))
|
|
}
|
|
|
|
#[test]
|
|
fn loc_help_exposes_project_and_selected_file_modes() {
|
|
let output = run_cstat_raw(&["loc", "--help"]);
|
|
assert_success(&output);
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
for expected in [
|
|
"Rust project directory or Rust source file",
|
|
"Project mode:",
|
|
"Selected-file mode:",
|
|
"cstat loc --path .",
|
|
"cstat loc --path src/lib.rs",
|
|
"cstat loc --explain",
|
|
"cstat loc --explain --json",
|
|
"code_lines is a static textual classification",
|
|
"blank lines are excluded",
|
|
"// comment-only lines are excluded",
|
|
"block-comment-only regions are excluded",
|
|
"Project JSON fields:",
|
|
"Selected-file JSON fields:",
|
|
"--top only limits the project-mode",
|
|
"ignored for selected-file and --explain modes",
|
|
] {
|
|
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn loc_explain_human_is_canonical_reference() {
|
|
let output = run_cstat_raw(&[
|
|
"--path",
|
|
"/definitely/missing/cstat/path",
|
|
"loc",
|
|
"--explain",
|
|
]);
|
|
assert_success(&output);
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
for expected in [
|
|
"loc reference",
|
|
"Project mode: cstat loc --path .",
|
|
"Machine contract: cstat loc --explain --json",
|
|
"code_lines: static textual classification",
|
|
"not semantic Rust parsing",
|
|
"Project JSON fields:",
|
|
"Selected-file JSON fields:",
|
|
] {
|
|
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
|
|
}
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
assert!(
|
|
!stderr.contains("Error discovering files"),
|
|
"unexpected discovery error: {stderr}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn loc_explain_json_exposes_machine_contract() {
|
|
let output = run_cstat_raw(&["--json", "loc", "--explain"]);
|
|
assert_success(&output);
|
|
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
|
let value: Value = serde_json::from_str(&stdout).expect("parse loc explain JSON");
|
|
assert_eq!(value["probe"], "loc");
|
|
assert_eq!(
|
|
value["commands"]["explain_json"],
|
|
"cstat loc --explain --json",
|
|
);
|
|
assert_eq!(value["code_lines"]["kind"], "static textual classification",);
|
|
assert!(
|
|
value["project_json_fields"]["files"]
|
|
.as_array()
|
|
.expect("project fields")
|
|
.iter()
|
|
.any(|field| field.as_str() == Some("code_lines")),
|
|
"json={stdout}",
|
|
);
|
|
assert!(
|
|
value["selected_file_json_fields"]["reachable_spans"]
|
|
.as_array()
|
|
.expect("selected fields")
|
|
.iter()
|
|
.any(|field| field.as_str() == Some("function")),
|
|
"json={stdout}",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn loc_verbose_points_to_loc_explain_not_guide() {
|
|
let root = temp_project("verbose-loc-reference");
|
|
let output = run_cstat(&root, &["-v", "loc"]);
|
|
assert_success(&output);
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
assert!(
|
|
stdout.contains("Full loc reference: cstat loc --explain"),
|
|
"stdout={stdout}",
|
|
);
|
|
assert!(
|
|
!stdout.contains("cstat guide size"),
|
|
"stdout unexpectedly referenced guide: {stdout}",
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loc_project_json_exposes_shape_contract() {
|
|
let root = temp_project("project-json");
|
|
let output = run_cstat(&root, &["--json", "loc"]);
|
|
assert_success(&output);
|
|
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
|
let value: Value = serde_json::from_str(&stdout).expect("parse loc JSON");
|
|
assert_eq!(value["cstat_version"], env!("CARGO_PKG_VERSION"));
|
|
|
|
let files = value["files"].as_array().expect("files array");
|
|
assert!(!files.is_empty(), "json={stdout}");
|
|
let lib = files
|
|
.iter()
|
|
.find(|file| file["path"] == "src/lib.rs")
|
|
.expect("src/lib.rs row");
|
|
let total_lines = lib["total_lines"].as_u64().expect("total_lines");
|
|
let code_lines = lib["code_lines"].as_u64().expect("code_lines");
|
|
assert!(
|
|
total_lines >= code_lines && code_lines > 0,
|
|
"lib row={lib:?}",
|
|
);
|
|
|
|
let aggregate = &value["aggregate"];
|
|
for field in [
|
|
"total_files",
|
|
"total_loc",
|
|
"mean",
|
|
"std_dev",
|
|
"median",
|
|
"min",
|
|
"max",
|
|
] {
|
|
assert!(aggregate.get(field).is_some(), "missing {field}: {stdout}");
|
|
}
|
|
|
|
let directories = value["directory_breakdown"]
|
|
.as_array()
|
|
.expect("directory_breakdown array");
|
|
assert!(
|
|
directories.iter().any(|directory| {
|
|
directory["directory"] == "src"
|
|
&& directory["code_lines"]
|
|
.as_u64()
|
|
.is_some_and(|code_lines| code_lines > 0)
|
|
}),
|
|
"directory_breakdown={directories:?}",
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loc_selected_file_human_is_usable() {
|
|
let root = temp_project("selected-human");
|
|
let file = root.join("src/lib.rs");
|
|
let output = run_cstat(&file, &["loc"]);
|
|
assert_success(&output);
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
for expected in [
|
|
"Projected line reachability",
|
|
"file src/lib.rs",
|
|
"physical lines",
|
|
"production code lines",
|
|
"statically reachable lines",
|
|
"not statically reachable lines",
|
|
"reachable function spans:",
|
|
"unreachable function spans:",
|
|
"live",
|
|
"orphan",
|
|
] {
|
|
assert!(stdout.contains(expected), "missing {expected}: {stdout}");
|
|
}
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn loc_selected_file_json_exposes_projected_reachability_contract() {
|
|
let root = temp_project("selected-json");
|
|
let file = root.join("src/lib.rs");
|
|
let output = run_cstat(&file, &["--json", "loc"]);
|
|
assert_success(&output);
|
|
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
|
let value: Value = serde_json::from_str(&stdout).expect("parse selected-file loc JSON");
|
|
assert_eq!(value["file"], "src/lib.rs");
|
|
let total_lines = value["total_lines"].as_u64().expect("total_lines");
|
|
let code_lines = value["code_lines"].as_u64().expect("code_lines");
|
|
let projected_reachable_lines = value["projected_reachable_lines"]
|
|
.as_u64()
|
|
.expect("projected_reachable_lines");
|
|
let projected_unreachable_lines = value["projected_unreachable_lines"]
|
|
.as_u64()
|
|
.expect("projected_unreachable_lines");
|
|
assert!(total_lines >= code_lines, "json={stdout}");
|
|
assert_eq!(
|
|
projected_reachable_lines + projected_unreachable_lines,
|
|
code_lines,
|
|
"json={stdout}",
|
|
);
|
|
assert!(
|
|
span_array_contains_function(&value["reachable_spans"], "live"),
|
|
"json={stdout}",
|
|
);
|
|
assert!(
|
|
span_array_contains_function(&value["reachable_spans"], "helper"),
|
|
"json={stdout}",
|
|
);
|
|
assert!(
|
|
span_array_contains_function(&value["unreachable_spans"], "orphan"),
|
|
"json={stdout}",
|
|
);
|
|
|
|
fs::remove_dir_all(root).unwrap();
|
|
}
|