refactor: cstat loc behavioral tune
This commit is contained in:
parent
d699420eb1
commit
98b7f6a5d9
5 changed files with 447 additions and 29 deletions
|
|
@ -35,7 +35,11 @@ remain available under `cstat advanced ...`.
|
||||||
|
|
||||||
Each accepts `--json` for structured output.
|
Each accepts `--json` for structured output.
|
||||||
|
|
||||||
- `cstat loc --json --path .` — line counts and directory breakdown.
|
- `cstat loc --explain --json` — machine-readable `loc` contract: modes,
|
||||||
|
`code_lines` rules, project JSON fields, and selected-file JSON fields.
|
||||||
|
- `cstat loc --json --path .` — project size-shape data.
|
||||||
|
- `cstat loc --json --path src/lib.rs` — selected-file projected static line
|
||||||
|
reachability.
|
||||||
- `cstat symbols --json --path .` — symbol totals by kind and per file.
|
- `cstat symbols --json --path .` — symbol totals by kind and per file.
|
||||||
- `cstat deps --json --path .` — dependency edges, coupling, fan-in/fan-out,
|
- `cstat deps --json --path .` — dependency edges, coupling, fan-in/fan-out,
|
||||||
and cohesion.
|
and cohesion.
|
||||||
|
|
@ -82,5 +86,8 @@ Use these only when the focused report points to a question they answer:
|
||||||
test/benchmark roots can reach a function or edge. They are not runtime hit-count profiling.
|
test/benchmark roots can reach a function or edge. They are not runtime hit-count profiling.
|
||||||
- Dependency edges come from source-level `use`/`mod` relationships. Generated
|
- Dependency edges come from source-level `use`/`mod` relationships. Generated
|
||||||
code and macro expansion can hide edges.
|
code and macro expansion can hide edges.
|
||||||
|
- For exact `loc` `code_lines` rules and JSON fields, run
|
||||||
|
`cstat loc --explain` or `cstat loc --explain --json`; that command is the
|
||||||
|
canonical contract.
|
||||||
- Prefer targeted reductions: remove dead code, split large files, move symbols
|
- Prefer targeted reductions: remove dead code, split large files, move symbols
|
||||||
across modules, then reduce per-function complexity.
|
across modules, then reduce per-function complexity.
|
||||||
|
|
|
||||||
37
src/guide.rs
37
src/guide.rs
|
|
@ -64,30 +64,31 @@ when learning what the metrics mean.",
|
||||||
fn topic_size() -> TopicContent {
|
fn topic_size() -> TopicContent {
|
||||||
TopicContent {
|
TopicContent {
|
||||||
description: "\
|
description: "\
|
||||||
Size metrics measure the volume of code at file and function granularity. \
|
Size metrics measure the volume and distribution of Rust source. `cstat loc` owns the line-count contract; run `cstat loc --explain` for modes, code_lines rules, and JSON fields.",
|
||||||
Large files and functions are not inherently problematic, but they correlate \
|
|
||||||
with higher defect density, harder navigation, and merge conflicts. The \
|
|
||||||
distribution shape matters more than any single value.",
|
|
||||||
metrics: &[
|
metrics: &[
|
||||||
("file LoC", "Total lines of code per file. High values may indicate a module doing too much."),
|
(
|
||||||
("function LoC", "Lines per function body. Long functions tend to have higher cognitive load."),
|
"file size shape",
|
||||||
("mean / median", "Central tendency of the distribution. A large gap between mean and median indicates skew from outliers."),
|
"Project-level file-size distribution from `cstat loc --path .`.",
|
||||||
("std_dev", "Spread of the distribution. High values mean uneven file sizes."),
|
),
|
||||||
("max", "The largest single file or function. Often the first place to investigate."),
|
(
|
||||||
|
"selected-file line reachability",
|
||||||
|
"Projected static reachable/unreachable production spans from `cstat loc --path <file.rs>`.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"distribution statistics",
|
||||||
|
"Mean, median, std_dev, min, and max summarize project file sizes.",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
commands: &[
|
commands: &[
|
||||||
"cstat loc — per-file LoC with bar charts and directory breakdown",
|
"cstat loc --explain — loc modes, code_lines rules, and JSON field contract",
|
||||||
"cstat loc --json — structured file-level size data",
|
"cstat loc --path . — project size-shape report",
|
||||||
|
"cstat loc --path src/lib.rs — selected-file projected line reachability",
|
||||||
"cstat dist --metric loc — histogram and outlier analysis of LoC distribution",
|
"cstat dist --metric loc — histogram and outlier analysis of LoC distribution",
|
||||||
],
|
],
|
||||||
patterns: &[
|
patterns: &[
|
||||||
"A right-skewed LoC distribution (long tail) usually means a few files have \
|
"Use `cstat loc --explain` as the canonical reference before consuming loc output.",
|
||||||
grown disproportionately. Check whether those files contain multiple concerns.",
|
"A right-skewed project-mode code_lines distribution means a few files dominate size.",
|
||||||
"Files above 500 lines often contain function clusters that could be separate \
|
"Selected-file loc is a static projection from function spans and test/benchmark reachability; do not read it as runtime coverage.",
|
||||||
modules. Cross-reference with `cstat deps` to see if the file's functions form \
|
|
||||||
distinct groups with few cross-calls.",
|
|
||||||
"Uniform file sizes are not a goal — some modules are naturally larger. The \
|
|
||||||
signal is when a file is large AND has low cohesion or high internal complexity.",
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
src/loc.rs
77
src/loc.rs
|
|
@ -377,11 +377,13 @@ pub fn render_loc(files: &[PathBuf], project_path: &Path, top_n: Option<usize>,
|
||||||
|
|
||||||
if verbose {
|
if verbose {
|
||||||
render::verbose_block(&[
|
render::verbose_block(&[
|
||||||
"Bar chart: files ranked by code lines (excluding blank lines and comments).",
|
"Bar chart: files ranked by code_lines.",
|
||||||
"Bar length is proportional to LoC relative to the largest file.",
|
"code_lines excludes blank lines, // comment-only lines, and block-comment-only regions.",
|
||||||
|
"Line classification is static text scanning, not semantic Rust parsing.",
|
||||||
|
"Bar length is proportional to code_lines relative to the largest file.",
|
||||||
"Color gradient: red = top of the ranking (most lines), green = bottom.",
|
"Color gradient: red = top of the ranking (most lines), green = bottom.",
|
||||||
|
"Full loc reference: cstat loc --explain.",
|
||||||
]);
|
]);
|
||||||
render::guide_ref("size");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
|
|
@ -477,6 +479,71 @@ pub fn render_loc_file_json(rs_files: &[PathBuf], project_path: &Path, file: &Pa
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_loc_explain(json: bool) {
|
||||||
|
if json {
|
||||||
|
render_loc_explain_json();
|
||||||
|
} else {
|
||||||
|
render_loc_explain_human();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_loc_explain_human() {
|
||||||
|
render::section_header("loc reference");
|
||||||
|
println!(" Project mode: cstat loc --path .");
|
||||||
|
println!(" Project JSON: cstat loc --path . --json");
|
||||||
|
println!(" Selected-file mode: cstat loc --path src/lib.rs");
|
||||||
|
println!(" Selected-file JSON: cstat loc --path src/lib.rs --json");
|
||||||
|
println!(" Machine contract: cstat loc --explain --json");
|
||||||
|
println!();
|
||||||
|
println!(" code_lines: static textual classification");
|
||||||
|
println!(" excludes blank lines, // comment-only lines, and block-comment-only regions");
|
||||||
|
println!(" not semantic Rust parsing");
|
||||||
|
println!();
|
||||||
|
println!(" Project JSON fields: cstat_version, files, aggregate, directory_breakdown");
|
||||||
|
println!(" Selected-file JSON fields: file, total_lines, code_lines, projected_reachable_lines, projected_unreachable_lines, reachable_spans, unreachable_spans");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_loc_explain_json() {
|
||||||
|
let output = serde_json::json!({
|
||||||
|
"cstat_version": env!("CARGO_PKG_VERSION"),
|
||||||
|
"probe": "loc",
|
||||||
|
"commands": {
|
||||||
|
"project_human": "cstat loc --path .",
|
||||||
|
"project_json": "cstat loc --path . --json",
|
||||||
|
"selected_file_human": "cstat loc --path src/lib.rs",
|
||||||
|
"selected_file_json": "cstat loc --path src/lib.rs --json",
|
||||||
|
"explain_human": "cstat loc --explain",
|
||||||
|
"explain_json": "cstat loc --explain --json",
|
||||||
|
},
|
||||||
|
"code_lines": {
|
||||||
|
"kind": "static textual classification",
|
||||||
|
"excludes": [
|
||||||
|
"blank lines",
|
||||||
|
"// comment-only lines",
|
||||||
|
"block-comment-only regions via simple /* ... */ state tracking",
|
||||||
|
],
|
||||||
|
"not": "semantic Rust parsing",
|
||||||
|
},
|
||||||
|
"project_json_fields": {
|
||||||
|
"cstat_version": "cstat version string",
|
||||||
|
"files": ["path", "total_lines", "code_lines"],
|
||||||
|
"aggregate": ["total_files", "total_loc", "mean", "std_dev", "median", "min", "max"],
|
||||||
|
"directory_breakdown": ["directory", "code_lines"],
|
||||||
|
},
|
||||||
|
"selected_file_json_fields": {
|
||||||
|
"file": "selected file path relative to the crate root",
|
||||||
|
"total_lines": "physical lines in the selected file",
|
||||||
|
"code_lines": "production function-span code lines after exclusions",
|
||||||
|
"projected_reachable_lines": "production code lines in statically reachable function spans",
|
||||||
|
"projected_unreachable_lines": "production code lines not in statically reachable function spans",
|
||||||
|
"reachable_spans": ["function", "line_start", "line_end"],
|
||||||
|
"unreachable_spans": ["function", "line_start", "line_end"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("{}", serde_json::to_string(&output).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
pub fn analyze_file_projected_line_reachability_from_files(
|
pub fn analyze_file_projected_line_reachability_from_files(
|
||||||
rs_files: &[PathBuf],
|
rs_files: &[PathBuf],
|
||||||
project_path: &Path,
|
project_path: &Path,
|
||||||
|
|
@ -511,8 +578,10 @@ pub fn render_file_projected_line_reachability_report(
|
||||||
if verbose {
|
if verbose {
|
||||||
render::verbose_block(&[
|
render::verbose_block(&[
|
||||||
"Physical lines count every source line in the selected file.",
|
"Physical lines count every source line in the selected file.",
|
||||||
"Code lines exclude blank/comment-only lines and test/support/wrapper spans for selected-file production accounting.",
|
"code_lines is production function-span code after excluding tests, benches, test-support helpers, and selected binary wrapper main().",
|
||||||
|
"Line classification excludes blank lines, // comment-only lines, and block-comment-only regions with simple text scanning.",
|
||||||
"Projected reachable lines are production code lines inside functions statically reached from project tests/benches.",
|
"Projected reachable lines are production code lines inside functions statically reached from project tests/benches.",
|
||||||
|
"Full loc reference: cstat loc --explain.",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
45
src/main.rs
45
src/main.rs
|
|
@ -33,7 +33,7 @@ struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Option<Commands>,
|
command: Option<Commands>,
|
||||||
|
|
||||||
/// Path to the Rust project directory (defaults to current directory)
|
/// Rust project directory or Rust source file to analyze (defaults to current directory)
|
||||||
#[arg(long, default_value = ".", global = true)]
|
#[arg(long, default_value = ".", global = true)]
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
||||||
|
|
@ -56,11 +56,40 @@ enum Commands {
|
||||||
Summary,
|
Summary,
|
||||||
/// Alias for the focused module metrics report
|
/// Alias for the focused module metrics report
|
||||||
Report,
|
Report,
|
||||||
/// Lines-of-code analysis with bar charts
|
#[command(
|
||||||
|
about = "Lines-of-code size-shape analysis",
|
||||||
|
long_about = r#"Lines-of-code size-shape analysis for Rust source.
|
||||||
|
|
||||||
|
Project mode:
|
||||||
|
cstat loc --path .
|
||||||
|
cstat loc --path . --json
|
||||||
|
|
||||||
|
Selected-file mode:
|
||||||
|
cstat loc --path src/lib.rs
|
||||||
|
cstat loc --path src/lib.rs --json
|
||||||
|
|
||||||
|
Explain mode:
|
||||||
|
cstat loc --explain
|
||||||
|
cstat loc --explain --json
|
||||||
|
|
||||||
|
Project mode reports total physical lines, code_lines, aggregate stats, per-file ranking, and directory breakdown for discovered Rust files. Selected-file mode reports projected static reachable/unreachable production line spans for one Rust source file.
|
||||||
|
|
||||||
|
code_lines is a static textual classification: blank lines are excluded, // comment-only lines are excluded, and block-comment-only regions are excluded using simple /* ... */ state tracking. It is not semantic Rust parsing.
|
||||||
|
|
||||||
|
Project JSON fields: cstat_version, files[{path,total_lines,code_lines}], aggregate{total_files,total_loc,mean,std_dev,median,min,max}, directory_breakdown[{directory,code_lines}].
|
||||||
|
Selected-file JSON fields: file, total_lines, code_lines, projected_reachable_lines, projected_unreachable_lines, reachable_spans[{function,line_start,line_end}], unreachable_spans[{function,line_start,line_end}].
|
||||||
|
|
||||||
|
Use --explain to print this usage and JSON field contract without running analysis. --path, --top, and -v are ignored by --explain.
|
||||||
|
|
||||||
|
--top only limits the project-mode per-file ranking (ignored for selected-file and --explain modes)."#
|
||||||
|
)]
|
||||||
Loc {
|
Loc {
|
||||||
/// Show only the top N files
|
/// Show only the top N files in project mode (ignored for selected-file and --explain modes)
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
top: Option<usize>,
|
top: Option<usize>,
|
||||||
|
/// Print the loc usage and JSON field contract without running analysis
|
||||||
|
#[arg(long)]
|
||||||
|
explain: bool,
|
||||||
},
|
},
|
||||||
/// Symbol counts by kind and file
|
/// Symbol counts by kind and file
|
||||||
Symbols,
|
Symbols,
|
||||||
|
|
@ -162,6 +191,12 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let verbose = cli.verbose;
|
let verbose = cli.verbose;
|
||||||
|
let command = cli.command.unwrap_or(Commands::Summary);
|
||||||
|
|
||||||
|
if let Commands::Loc { explain: true, .. } = &command {
|
||||||
|
loc::render_loc_explain(json);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let target = match discovery::resolve_target(&cli.path) {
|
let target = match discovery::resolve_target(&cli.path) {
|
||||||
Ok(target) => target,
|
Ok(target) => target,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -177,8 +212,6 @@ fn main() {
|
||||||
let project_path = &target.project_path;
|
let project_path = &target.project_path;
|
||||||
let rs_files = discovery::files_for_project(&target);
|
let rs_files = discovery::files_for_project(&target);
|
||||||
|
|
||||||
let command = cli.command.unwrap_or(Commands::Summary);
|
|
||||||
|
|
||||||
if let Some(file) = discovery::selected_file(&target) {
|
if let Some(file) = discovery::selected_file(&target) {
|
||||||
let project_rs_files = discovery::files_for_project(&target);
|
let project_rs_files = discovery::files_for_project(&target);
|
||||||
let target_rs_files = discovery::files_for_target(&target);
|
let target_rs_files = discovery::files_for_target(&target);
|
||||||
|
|
@ -307,7 +340,7 @@ fn main() {
|
||||||
report::render_report(&rs_files, &project_path, verbose);
|
report::render_report(&rs_files, &project_path, verbose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Loc { top } => {
|
Commands::Loc { top, .. } => {
|
||||||
if json {
|
if json {
|
||||||
loc::render_loc_json(&rs_files, &project_path);
|
loc::render_loc_json(&rs_files, &project_path);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
308
tests/loc_cli.rs
Normal file
308
tests/loc_cli.rs
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
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();
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue