203 lines
6 KiB
Rust
203 lines
6 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-deps-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"), "pub mod alpha;\npub mod beta;\n").unwrap();
|
||
|
|
fs::write(
|
||
|
|
root.join("src/alpha.rs"),
|
||
|
|
r#"pub struct Shared;
|
||
|
|
|
||
|
|
pub fn shared(input: Shared) -> Shared {
|
||
|
|
input
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn closure_owner(input: Shared) -> Shared {
|
||
|
|
let first = || shared(input);
|
||
|
|
let second = || shared(Shared);
|
||
|
|
first();
|
||
|
|
second()
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn sibling(input: Shared) -> Shared {
|
||
|
|
shared(input)
|
||
|
|
}
|
||
|
|
"#,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
fs::write(
|
||
|
|
root.join("src/beta.rs"),
|
||
|
|
r#"use crate::alpha::shared;
|
||
|
|
|
||
|
|
pub fn beta() {
|
||
|
|
let _ = shared(crate::alpha::Shared);
|
||
|
|
}
|
||
|
|
"#,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
root
|
||
|
|
}
|
||
|
|
|
||
|
|
fn run_cstat(project: &Path, args: &[&str]) -> Output {
|
||
|
|
let bin = env!("CARGO_BIN_EXE_cstat");
|
||
|
|
let mut command = Command::new(bin);
|
||
|
|
command.args(["--no-color", "--path"]);
|
||
|
|
command.arg(project);
|
||
|
|
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),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn deps_human_verbose_shows_actual_one_way_direction() {
|
||
|
|
let root = temp_project("human-direction");
|
||
|
|
let output = run_cstat(&root, &["deps", "--top", "10", "-v"]);
|
||
|
|
assert_success(&output);
|
||
|
|
|
||
|
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
|
|
assert!(
|
||
|
|
stdout.contains("Cells are pair strength, not edge direction"),
|
||
|
|
"stdout={stdout}"
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
stdout.contains("One-way rows show actual source module -> target module direction."),
|
||
|
|
"stdout={stdout}"
|
||
|
|
);
|
||
|
|
assert!(stdout.contains("signature type tokens"), "stdout={stdout}");
|
||
|
|
let directed_line = stdout
|
||
|
|
.lines()
|
||
|
|
.find(|line| line.contains("beta") && line.contains("alpha") && line.contains("one-way"))
|
||
|
|
.unwrap_or_else(|| panic!("missing beta -> alpha coupling line: {stdout}"));
|
||
|
|
assert!(
|
||
|
|
directed_line.find("beta") < directed_line.find("alpha"),
|
||
|
|
"line should show source before target: {directed_line}"
|
||
|
|
);
|
||
|
|
assert!(
|
||
|
|
!stdout.contains("alpha::closure_owner"),
|
||
|
|
"function-scope pseudo-module leaked into cohesion output: {stdout}"
|
||
|
|
);
|
||
|
|
|
||
|
|
fs::remove_dir_all(root).unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn deps_json_coupling_edges_and_cohesion_use_source_modules() {
|
||
|
|
let root = temp_project("json-direction");
|
||
|
|
let output = run_cstat(&root, &["--json", "deps"]);
|
||
|
|
assert_success(&output);
|
||
|
|
|
||
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
||
|
|
let value: Value = serde_json::from_str(&stdout).expect("parse deps JSON");
|
||
|
|
let pairs = value["coupling_pairs"]
|
||
|
|
.as_array()
|
||
|
|
.expect("coupling_pairs array");
|
||
|
|
let alpha_beta = pairs
|
||
|
|
.iter()
|
||
|
|
.find(|pair| pair["module_a"] == "alpha" && pair["module_b"] == "beta")
|
||
|
|
.unwrap_or_else(|| panic!("missing alpha/beta pair: {value}"));
|
||
|
|
assert_eq!(alpha_beta["direction"], "unidirectional");
|
||
|
|
assert_eq!(
|
||
|
|
alpha_beta["edges"],
|
||
|
|
serde_json::json!([{ "from": "beta", "to": "alpha" }]),
|
||
|
|
"pair={alpha_beta}"
|
||
|
|
);
|
||
|
|
|
||
|
|
let cohesion = value["cohesion"].as_array().expect("cohesion array");
|
||
|
|
let modules = cohesion
|
||
|
|
.iter()
|
||
|
|
.filter_map(|row| row["module"].as_str())
|
||
|
|
.collect::<Vec<_>>();
|
||
|
|
assert!(modules.contains(&"alpha"), "modules={modules:?}");
|
||
|
|
assert!(modules.contains(&"beta"), "modules={modules:?}");
|
||
|
|
assert!(
|
||
|
|
!modules
|
||
|
|
.iter()
|
||
|
|
.any(|module| module.contains("closure_owner")),
|
||
|
|
"cohesion should use source modules, not function scopes: {modules:?}"
|
||
|
|
);
|
||
|
|
let alpha = cohesion
|
||
|
|
.iter()
|
||
|
|
.find(|row| row["module"] == "alpha")
|
||
|
|
.unwrap_or_else(|| panic!("missing alpha cohesion row: {value}"));
|
||
|
|
assert_eq!(alpha["function_count"], 3);
|
||
|
|
|
||
|
|
fs::remove_dir_all(root).unwrap();
|
||
|
|
}
|
||
|
|
|
||
|
|
fn nested_project(name: &str) -> PathBuf {
|
||
|
|
let root = temp_project(name);
|
||
|
|
fs::remove_file(root.join("src/alpha.rs")).unwrap();
|
||
|
|
fs::remove_file(root.join("src/beta.rs")).unwrap();
|
||
|
|
fs::write(root.join("src/lib.rs"), "pub mod parent;\n").unwrap();
|
||
|
|
fs::create_dir_all(root.join("src/parent")).unwrap();
|
||
|
|
fs::write(
|
||
|
|
root.join("src/parent/mod.rs"),
|
||
|
|
r#"mod child;
|
||
|
|
|
||
|
|
pub struct Shared;
|
||
|
|
|
||
|
|
pub use child::child;
|
||
|
|
"#,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
fs::write(
|
||
|
|
root.join("src/parent/child.rs"),
|
||
|
|
r#"use super::Shared;
|
||
|
|
|
||
|
|
pub fn child(_: Shared) {}
|
||
|
|
"#,
|
||
|
|
)
|
||
|
|
.unwrap();
|
||
|
|
root
|
||
|
|
}
|
||
|
|
|
||
|
|
#[test]
|
||
|
|
fn deps_json_resolves_nested_mod_and_super_as_bidirectional_pair() {
|
||
|
|
let root = nested_project("nested-bidir");
|
||
|
|
let output = run_cstat(&root, &["--json", "deps"]);
|
||
|
|
assert_success(&output);
|
||
|
|
|
||
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
||
|
|
let value: Value = serde_json::from_str(&stdout).expect("parse deps JSON");
|
||
|
|
let pairs = value["coupling_pairs"]
|
||
|
|
.as_array()
|
||
|
|
.expect("coupling_pairs array");
|
||
|
|
let parent_child = pairs
|
||
|
|
.iter()
|
||
|
|
.find(|pair| pair["module_a"] == "parent" && pair["module_b"] == "parent::child")
|
||
|
|
.unwrap_or_else(|| panic!("missing parent/child pair: {value}"));
|
||
|
|
assert_eq!(parent_child["direction"], "bidirectional");
|
||
|
|
assert_eq!(
|
||
|
|
parent_child["edges"],
|
||
|
|
serde_json::json!([
|
||
|
|
{ "from": "parent", "to": "parent::child" },
|
||
|
|
{ "from": "parent::child", "to": "parent" }
|
||
|
|
]),
|
||
|
|
"pair={parent_child}"
|
||
|
|
);
|
||
|
|
|
||
|
|
fs::remove_dir_all(root).unwrap();
|
||
|
|
}
|