use std::fs;
use std::path::PathBuf;
use crate::api_surface::build_api_surface_report;
use tokmd_analysis_types::AnalysisLimits;
use tokmd_analysis_types::ApiSurfaceReport;
use tokmd_types::{ChildIncludeMode, ExportData, FileKind, FileRow};
fn make_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: 2,
blanks: 1,
lines: 13,
bytes: 100,
tokens: 30,
}
}
fn make_export(rows: Vec<FileRow>) -> ExportData {
ExportData {
rows,
module_roots: vec![],
module_depth: 1,
children: ChildIncludeMode::Separate,
}
}
fn default_limits() -> AnalysisLimits {
AnalysisLimits::default()
}
fn write_temp_files(files: &[(&str, &str)]) -> (tempfile::TempDir, Vec<PathBuf>) {
let dir = tempfile::tempdir().expect("create tempdir");
let mut paths = Vec::new();
for (rel, content) in files {
let full = dir.path().join(rel);
if let Some(parent) = full.parent() {
fs::create_dir_all(parent).unwrap();
}
fs::write(&full, content).unwrap();
paths.push(PathBuf::from(rel));
}
(dir, paths)
}
fn build_single_file(code: &str, filename: &str, lang: &str) -> ApiSurfaceReport {
let (dir, paths) = write_temp_files(&[(filename, code)]);
let export = make_export(vec![make_row(filename, "src", lang)]);
build_api_surface_report(dir.path(), &paths, &export, &default_limits()).unwrap()
}
#[test]
fn rust_pub_fn_counted() {
let report = build_single_file("pub fn hello() {}\n", "lib.rs", "Rust");
assert_eq!(report.public_items, 1);
assert_eq!(report.total_items, 1);
}
#[test]
fn rust_pub_struct_counted() {
let report = build_single_file("pub struct Foo;\n", "lib.rs", "Rust");
assert_eq!(report.public_items, 1);
}
#[test]
fn rust_pub_enum_counted() {
let report = build_single_file("pub enum Bar { A, B }\n", "lib.rs", "Rust");
assert_eq!(report.public_items, 1);
}
#[test]
fn rust_pub_trait_counted() {
let report = build_single_file("pub trait MyTrait {}\n", "lib.rs", "Rust");
assert_eq!(report.public_items, 1);
}
#[test]
fn rust_pub_const_counted() {
let report = build_single_file("pub const VALUE: i32 = 42;\n", "lib.rs", "Rust");
assert_eq!(report.public_items, 1);
}
#[test]
fn rust_pub_async_fn_counted() {
let report = build_single_file("pub async fn run() {}\n", "lib.rs", "Rust");
assert_eq!(report.public_items, 1);
}
#[test]
fn rust_pub_crate_counted_as_public() {
let report = build_single_file("pub(crate) fn internal() {}\n", "lib.rs", "Rust");
assert_eq!(report.public_items, 1);
}
#[test]
fn rust_private_fn_not_public() {
let report = build_single_file("fn private_fn() {}\n", "lib.rs", "Rust");
assert_eq!(report.public_items, 0);
assert_eq!(report.internal_items, 1);
}
#[test]
fn js_export_function_counted() {
let report = build_single_file("export function greet() {}\n", "app.js", "JavaScript");
assert_eq!(report.public_items, 1);
}
#[test]
fn ts_export_class_counted() {
let report = build_single_file("export class Widget {}\n", "widget.ts", "TypeScript");
assert_eq!(report.public_items, 1);
}
#[test]
fn ts_export_interface_counted() {
let report = build_single_file("export interface Config {}\n", "config.ts", "TypeScript");
assert_eq!(report.public_items, 1);
}
#[test]
fn js_non_export_function_is_internal() {
let report = build_single_file("function helper() {}\n", "util.js", "JavaScript");
assert_eq!(report.public_items, 0);
assert_eq!(report.internal_items, 1);
}
#[test]
fn python_public_def_counted() {
let report = build_single_file("def greet():\n pass\n", "app.py", "Python");
assert_eq!(report.public_items, 1);
}
#[test]
fn python_class_counted() {
let report = build_single_file("class Foo:\n pass\n", "foo.py", "Python");
assert_eq!(report.public_items, 1);
}
#[test]
fn python_private_def_is_internal() {
let report = build_single_file("def _helper():\n pass\n", "util.py", "Python");
assert_eq!(report.public_items, 0);
assert_eq!(report.internal_items, 1);
}
#[test]
fn go_uppercase_func_is_public() {
let report = build_single_file("func Hello() {}\n", "main.go", "Go");
assert_eq!(report.public_items, 1);
}
#[test]
fn go_lowercase_func_is_internal() {
let report = build_single_file("func hello() {}\n", "main.go", "Go");
assert_eq!(report.public_items, 0);
assert_eq!(report.internal_items, 1);
}
#[test]
fn go_uppercase_type_is_public() {
let report = build_single_file("type Config struct {}\n", "config.go", "Go");
assert_eq!(report.public_items, 1);
}
#[test]
fn java_public_class_counted() {
let report = build_single_file("public class App {}\n", "App.java", "Java");
assert_eq!(report.public_items, 1);
}
#[test]
fn java_private_class_is_internal() {
let report = build_single_file("private class Helper {}\n", "Helper.java", "Java");
assert_eq!(report.public_items, 0);
assert_eq!(report.internal_items, 1);
}
#[test]
fn rust_multiple_symbols_counted() {
let code = "pub fn a() {}\npub fn b() {}\nfn c() {}\npub struct D;\n";
let report = build_single_file(code, "lib.rs", "Rust");
assert_eq!(report.public_items, 3);
assert_eq!(report.internal_items, 1);
assert_eq!(report.total_items, 4);
}
#[test]
fn empty_file_produces_zero_items() {
let report = build_single_file("", "empty.rs", "Rust");
assert_eq!(report.total_items, 0);
assert_eq!(report.public_items, 0);
assert_eq!(report.internal_items, 0);
}
#[test]
fn all_private_rust_file() {
let code = "fn private_a() {}\nfn private_b() {}\nstruct Internal;\n";
let report = build_single_file(code, "lib.rs", "Rust");
assert_eq!(report.public_items, 0);
assert_eq!(report.internal_items, 3);
assert!(report.top_exporters.is_empty());
}
#[test]
fn comments_only_file_produces_zero_items() {
let code = "// This is a comment\n// Another comment\n/// Doc comment\n";
let report = build_single_file(code, "lib.rs", "Rust");
assert_eq!(report.total_items, 0);
}
#[test]
fn unsupported_language_produces_empty_report() {
let (dir, paths) = write_temp_files(&[("style.css", "body { color: red; }")]);
let export = make_export(vec![make_row("style.css", "src", "CSS")]);
let report = build_api_surface_report(dir.path(), &paths, &export, &default_limits()).unwrap();
assert_eq!(report.total_items, 0);
}
#[test]
fn report_deterministic_across_calls() {
let code = "pub fn alpha() {}\npub fn beta() {}\nfn gamma() {}\n";
let r1 = build_single_file(code, "lib.rs", "Rust");
let r2 = build_single_file(code, "lib.rs", "Rust");
let j1 = serde_json::to_string(&r1).unwrap();
let j2 = serde_json::to_string(&r2).unwrap();
assert_eq!(j1, j2);
}
#[test]
fn by_language_map_sorted_by_key() {
let files: &[(&str, &str)] = &[
("app.py", "def greet():\n pass\n"),
("lib.rs", "pub fn hello() {}\n"),
("main.go", "func Hello() {}\n"),
];
let (dir, paths) = write_temp_files(files);
let export = make_export(vec![
make_row("app.py", "src", "Python"),
make_row("lib.rs", "src", "Rust"),
make_row("main.go", "src", "Go"),
]);
let report = build_api_surface_report(dir.path(), &paths, &export, &default_limits()).unwrap();
let keys: Vec<&String> = report.by_language.keys().collect();
let mut sorted = keys.clone();
sorted.sort();
assert_eq!(keys, sorted);
}
#[test]
fn top_exporters_sorted_by_public_items_desc() {
let files: &[(&str, &str)] = &[
("a.rs", "pub fn x() {}\n"),
("b.rs", "pub fn y() {}\npub fn z() {}\npub fn w() {}\n"),
];
let (dir, paths) = write_temp_files(files);
let export = make_export(vec![
make_row("a.rs", "src", "Rust"),
make_row("b.rs", "src", "Rust"),
]);
let report = build_api_surface_report(dir.path(), &paths, &export, &default_limits()).unwrap();
if report.top_exporters.len() >= 2 {
assert!(report.top_exporters[0].public_items >= report.top_exporters[1].public_items);
}
}
#[test]
fn public_ratio_is_zero_when_no_items() {
let report = build_single_file("", "empty.rs", "Rust");
assert_eq!(report.public_ratio, 0.0);
}
#[test]
fn public_ratio_is_one_when_all_public() {
let report = build_single_file("pub fn a() {}\npub fn b() {}\n", "lib.rs", "Rust");
assert!((report.public_ratio - 1.0).abs() < 0.001);
}
#[test]
fn documented_ratio_calculated() {
let code = "/// Documented\npub fn documented() {}\npub fn undocumented() {}\n";
let report = build_single_file(code, "lib.rs", "Rust");
assert!((report.documented_ratio - 0.5).abs() < 0.001);
}
#[test]
fn report_serializes_and_deserializes() {
let report = build_single_file("pub fn hello() {}\nfn world() {}\n", "lib.rs", "Rust");
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);
}