use std::fs;
use std::path::PathBuf;
use crate::api_surface::build_api_surface_report;
use tempfile::tempdir;
use tokmd_analysis_types::AnalysisLimits;
use tokmd_analysis_types::ApiSurfaceReport;
use tokmd_types::{ChildIncludeMode, ExportData, FileKind, FileRow};
fn file_row(path: &str, module: &str, lang: &str) -> FileRow {
FileRow {
path: path.to_string(),
module: module.to_string(),
lang: lang.to_string(),
kind: FileKind::Parent,
code: 10,
comments: 0,
blanks: 0,
lines: 10,
bytes: 200,
tokens: 50,
}
}
fn export(rows: Vec<FileRow>) -> ExportData {
ExportData {
rows,
module_roots: vec![],
module_depth: 1,
children: ChildIncludeMode::Separate,
}
}
#[test]
fn empty_files_zeroed_report() {
let dir = tempdir().unwrap();
let exp = export(vec![]);
let report =
build_api_surface_report(dir.path(), &[], &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.total_items, 0);
assert_eq!(report.public_items, 0);
assert_eq!(report.internal_items, 0);
assert_eq!(report.public_ratio, 0.0);
assert_eq!(report.documented_ratio, 0.0);
assert!(report.by_language.is_empty());
assert!(report.by_module.is_empty());
assert!(report.top_exporters.is_empty());
}
#[test]
fn rust_pub_fn_detection() {
let dir = tempdir().unwrap();
let code = "/// Documented\npub fn hello() {}\nfn private() {}\n";
fs::write(dir.path().join("lib.rs"), code).unwrap();
let exp = export(vec![file_row("lib.rs", "root", "Rust")]);
let files = vec![PathBuf::from("lib.rs")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.total_items, 2);
assert_eq!(report.public_items, 1);
assert_eq!(report.internal_items, 1);
}
#[test]
fn rust_pub_struct_enum_trait() {
let dir = tempdir().unwrap();
let code = "pub struct Foo {}\npub enum Bar {}\npub trait Baz {}\nenum Private {}\n";
fs::write(dir.path().join("types.rs"), code).unwrap();
let exp = export(vec![file_row("types.rs", "root", "Rust")]);
let files = vec![PathBuf::from("types.rs")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.public_items, 3);
assert_eq!(report.internal_items, 1);
assert_eq!(report.total_items, 4);
}
#[test]
fn js_export_detection() {
let dir = tempdir().unwrap();
let code = "export function greet() {}\nexport class App {}\nfunction helper() {}\n";
fs::write(dir.path().join("app.js"), code).unwrap();
let exp = export(vec![file_row("app.js", "root", "JavaScript")]);
let files = vec![PathBuf::from("app.js")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.public_items, 2);
assert_eq!(report.internal_items, 1);
}
#[test]
fn python_public_private_convention() {
let dir = tempdir().unwrap();
let code = "def public_func():\n pass\n\ndef _private_func():\n pass\n\nclass MyClass:\n pass\n";
fs::write(dir.path().join("mod.py"), code).unwrap();
let exp = export(vec![file_row("mod.py", "root", "Python")]);
let files = vec![PathBuf::from("mod.py")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.public_items, 2);
assert_eq!(report.internal_items, 1);
}
#[test]
fn go_uppercase_convention() {
let dir = tempdir().unwrap();
let code = "func PublicFunc() {}\nfunc privateFunc() {}\ntype Handler struct {}\ntype config struct {}\n";
fs::write(dir.path().join("main.go"), code).unwrap();
let exp = export(vec![file_row("main.go", "root", "Go")]);
let files = vec![PathBuf::from("main.go")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.public_items, 2);
assert_eq!(report.internal_items, 2);
}
#[test]
fn public_plus_internal_equals_total() {
let dir = tempdir().unwrap();
let code = "pub fn a() {}\nfn b() {}\npub struct C {}\nenum D {}\npub const E: i32 = 1;\n";
fs::write(dir.path().join("lib.rs"), code).unwrap();
let exp = export(vec![file_row("lib.rs", "root", "Rust")]);
let files = vec![PathBuf::from("lib.rs")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(
report.public_items + report.internal_items,
report.total_items,
"public + internal must equal total"
);
}
#[test]
fn documented_ratio_calculation() {
let dir = tempdir().unwrap();
let code = "/// Documented\npub fn documented() {}\npub fn undocumented() {}\n";
fs::write(dir.path().join("lib.rs"), code).unwrap();
let exp = export(vec![file_row("lib.rs", "root", "Rust")]);
let files = vec![PathBuf::from("lib.rs")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.public_items, 2);
assert!((report.documented_ratio - 0.5).abs() < 0.001);
}
#[test]
fn unsupported_language_ignored() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("data.csv"), "a,b,c\n1,2,3\n").unwrap();
let exp = export(vec![file_row("data.csv", "root", "CSV")]);
let files = vec![PathBuf::from("data.csv")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.total_items, 0);
}
#[test]
fn multi_language_breakdown() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("lib.rs"),
"pub fn rust_fn() {}\nfn internal() {}\n",
)
.unwrap();
fs::write(dir.path().join("app.js"), "export function js_fn() {}\n").unwrap();
let exp = export(vec![
file_row("lib.rs", "root", "Rust"),
file_row("app.js", "root", "JavaScript"),
]);
let files = vec![PathBuf::from("lib.rs"), PathBuf::from("app.js")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert!(report.by_language.contains_key("Rust"));
assert!(report.by_language.contains_key("JavaScript"));
let rust = &report.by_language["Rust"];
let js = &report.by_language["JavaScript"];
assert_eq!(rust.total_items, 2);
assert_eq!(js.total_items, 1);
}
#[test]
fn serde_roundtrip_preserves_all_fields() {
let dir = tempdir().unwrap();
fs::write(
dir.path().join("lib.rs"),
"/// Doc\npub fn foo() {}\nfn bar() {}\n",
)
.unwrap();
let exp = export(vec![file_row("lib.rs", "root", "Rust")]);
let files = vec![PathBuf::from("lib.rs")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
let json = serde_json::to_string(&report).unwrap();
let deser: ApiSurfaceReport = serde_json::from_str(&json).unwrap();
assert_eq!(deser.total_items, report.total_items);
assert_eq!(deser.public_items, report.public_items);
assert_eq!(deser.internal_items, report.internal_items);
assert!((deser.public_ratio - report.public_ratio).abs() < f64::EPSILON);
assert!((deser.documented_ratio - report.documented_ratio).abs() < f64::EPSILON);
assert_eq!(deser.by_language.len(), report.by_language.len());
assert_eq!(deser.by_module.len(), report.by_module.len());
assert_eq!(deser.top_exporters.len(), report.top_exporters.len());
}
#[test]
fn child_rows_excluded() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("lib.rs"), "pub fn parent_fn() {}\n").unwrap();
fs::write(
dir.path().join("embedded.js"),
"export function child_fn() {}\n",
)
.unwrap();
let mut child = file_row("embedded.js", "root", "JavaScript");
child.kind = FileKind::Child;
let exp = export(vec![file_row("lib.rs", "root", "Rust"), child]);
let files = vec![PathBuf::from("lib.rs"), PathBuf::from("embedded.js")];
let report =
build_api_surface_report(dir.path(), &files, &exp, &AnalysisLimits::default()).unwrap();
assert_eq!(report.total_items, 1);
assert_eq!(report.public_items, 1);
}
mod properties {
use proptest::prelude::*;
proptest! {
#[test]
fn ratios_always_in_range(
public in 0usize..100,
internal in 0usize..100,
) {
let total = public + internal;
let public_ratio = if total == 0 { 0.0 } else { public as f64 / total as f64 };
let documented_ratio = if public == 0 { 0.0 } else { (public / 2) as f64 / public as f64 };
prop_assert!((0.0..=1.0).contains(&public_ratio));
prop_assert!((0.0..=1.0).contains(&documented_ratio));
prop_assert!(public + internal == total);
}
}
}