use std::fs;
use std::path::PathBuf;
use crate::api_surface::build_api_surface_report;
use proptest::prelude::*;
use tokmd_analysis_types::AnalysisLimits;
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 rust_item_line() -> impl Strategy<Value = String> {
prop_oneof![
Just("pub fn generated() {}".to_string()),
Just("fn private_gen() {}".to_string()),
Just("pub struct GenStruct;".to_string()),
Just("struct PrivStruct;".to_string()),
Just("pub enum GenEnum {}".to_string()),
Just("enum PrivEnum {}".to_string()),
Just("pub trait GenTrait {}".to_string()),
Just("trait PrivTrait {}".to_string()),
Just("pub const GEN_CONST: u32 = 0;".to_string()),
Just("pub type GenType = u32;".to_string()),
Just("/// Doc comment".to_string()),
Just("// regular comment".to_string()),
Just(String::new()),
]
}
proptest! {
#[test]
fn total_equals_public_plus_internal(
lines in prop::collection::vec(rust_item_line(), 0..50)
) {
let code = lines.join("\n") + "\n";
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lib.rs");
fs::write(&path, &code).unwrap();
let export = make_export(vec![make_row("lib.rs", ".", "Rust")]);
let paths = vec![PathBuf::from("lib.rs")];
let report = build_api_surface_report(
dir.path(), &paths, &export, &default_limits(),
).unwrap();
prop_assert_eq!(
report.total_items,
report.public_items + report.internal_items,
"total must equal public + internal"
);
}
}
proptest! {
#[test]
fn public_ratio_in_unit_range(
lines in prop::collection::vec(rust_item_line(), 0..50)
) {
let code = lines.join("\n") + "\n";
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("lib.rs"), &code).unwrap();
let export = make_export(vec![make_row("lib.rs", ".", "Rust")]);
let paths = vec![PathBuf::from("lib.rs")];
let report = build_api_surface_report(
dir.path(), &paths, &export, &default_limits(),
).unwrap();
prop_assert!(report.public_ratio >= 0.0, "ratio must be >= 0");
prop_assert!(report.public_ratio <= 1.0, "ratio must be <= 1");
}
}
proptest! {
#[test]
fn documented_ratio_in_unit_range(
lines in prop::collection::vec(rust_item_line(), 0..50)
) {
let code = lines.join("\n") + "\n";
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("lib.rs"), &code).unwrap();
let export = make_export(vec![make_row("lib.rs", ".", "Rust")]);
let paths = vec![PathBuf::from("lib.rs")];
let report = build_api_surface_report(
dir.path(), &paths, &export, &default_limits(),
).unwrap();
prop_assert!(report.documented_ratio >= 0.0, "doc ratio must be >= 0");
prop_assert!(report.documented_ratio <= 1.0, "doc ratio must be <= 1");
}
}
proptest! {
#[test]
fn lang_breakdown_sums_to_totals(
lines in prop::collection::vec(rust_item_line(), 0..30)
) {
let code = lines.join("\n") + "\n";
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("lib.rs"), &code).unwrap();
let export = make_export(vec![make_row("lib.rs", ".", "Rust")]);
let paths = vec![PathBuf::from("lib.rs")];
let report = build_api_surface_report(
dir.path(), &paths, &export, &default_limits(),
).unwrap();
let lang_total: usize = report.by_language.values().map(|l| l.total_items).sum();
let lang_pub: usize = report.by_language.values().map(|l| l.public_items).sum();
let lang_int: usize = report.by_language.values().map(|l| l.internal_items).sum();
prop_assert_eq!(report.total_items, lang_total);
prop_assert_eq!(report.public_items, lang_pub);
prop_assert_eq!(report.internal_items, lang_int);
}
}
proptest! {
#[test]
fn modules_sorted_descending(
lines_a in prop::collection::vec(rust_item_line(), 0..20),
lines_b in prop::collection::vec(rust_item_line(), 0..20),
) {
let code_a = lines_a.join("\n") + "\n";
let code_b = lines_b.join("\n") + "\n";
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("a")).unwrap();
fs::create_dir_all(dir.path().join("b")).unwrap();
fs::write(dir.path().join("a/lib.rs"), &code_a).unwrap();
fs::write(dir.path().join("b/lib.rs"), &code_b).unwrap();
let export = make_export(vec![
make_row("a/lib.rs", "mod_a", "Rust"),
make_row("b/lib.rs", "mod_b", "Rust"),
]);
let paths = vec![
PathBuf::from("a/lib.rs"),
PathBuf::from("b/lib.rs"),
];
let report = build_api_surface_report(
dir.path(), &paths, &export, &default_limits(),
).unwrap();
for w in report.by_module.windows(2) {
prop_assert!(
w[0].total_items >= w[1].total_items,
"modules must be sorted descending by total_items"
);
}
}
}
proptest! {
#[test]
fn top_exporters_have_public_items(
lines in prop::collection::vec(rust_item_line(), 0..30)
) {
let code = lines.join("\n") + "\n";
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("lib.rs"), &code).unwrap();
let export = make_export(vec![make_row("lib.rs", ".", "Rust")]);
let paths = vec![PathBuf::from("lib.rs")];
let report = build_api_surface_report(
dir.path(), &paths, &export, &default_limits(),
).unwrap();
for item in &report.top_exporters {
prop_assert!(item.public_items > 0, "top exporter must have public items");
}
}
}
proptest! {
#[test]
fn top_exporters_sorted_descending(
lines_a in prop::collection::vec(rust_item_line(), 0..20),
lines_b in prop::collection::vec(rust_item_line(), 0..20),
) {
let code_a = lines_a.join("\n") + "\n";
let code_b = lines_b.join("\n") + "\n";
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("a.rs"), &code_a).unwrap();
fs::write(dir.path().join("b.rs"), &code_b).unwrap();
let export = make_export(vec![
make_row("a.rs", ".", "Rust"),
make_row("b.rs", ".", "Rust"),
]);
let paths = vec![
PathBuf::from("a.rs"),
PathBuf::from("b.rs"),
];
let report = build_api_surface_report(
dir.path(), &paths, &export, &default_limits(),
).unwrap();
for w in report.top_exporters.windows(2) {
prop_assert!(
w[0].public_items >= w[1].public_items,
"top exporters must be sorted descending by public_items"
);
}
}
}
proptest! {
#[test]
fn nested_struct_invariants_hold(
lines_a in prop::collection::vec(rust_item_line(), 0..20),
lines_b in prop::collection::vec(rust_item_line(), 0..20),
) {
let code_a = lines_a.join("\n") + "\n";
let code_b = lines_b.join("\n") + "\n";
let dir = tempfile::tempdir().unwrap();
fs::create_dir_all(dir.path().join("a")).unwrap();
fs::create_dir_all(dir.path().join("b")).unwrap();
fs::write(dir.path().join("a/lib.rs"), &code_a).unwrap();
fs::write(dir.path().join("b/lib.rs"), &code_b).unwrap();
let export = make_export(vec![
make_row("a/lib.rs", "mod_a", "Rust"),
make_row("b/lib.rs", "mod_b", "Rust"),
]);
let paths = vec![PathBuf::from("a/lib.rs"), PathBuf::from("b/lib.rs")];
let report =
build_api_surface_report(dir.path(), &paths, &export, &default_limits()).unwrap();
for lang_stats in report.by_language.values() {
prop_assert_eq!(
lang_stats.total_items,
lang_stats.public_items + lang_stats.internal_items,
"language totals must sum correctly"
);
prop_assert!(
lang_stats.public_ratio >= 0.0 && lang_stats.public_ratio <= 1.0,
"language public_ratio must be in [0.0, 1.0]"
);
}
for mod_row in &report.by_module {
prop_assert!(
mod_row.public_items <= mod_row.total_items,
"module public items cannot exceed total items"
);
prop_assert!(
mod_row.public_ratio >= 0.0 && mod_row.public_ratio <= 1.0,
"module public_ratio must be in [0.0, 1.0]"
);
}
for exp_item in &report.top_exporters {
prop_assert!(
exp_item.public_items <= exp_item.total_items,
"exporter public items cannot exceed total items"
);
}
}
}
#[test]
fn empty_input_always_yields_zeros() {
let dir = tempfile::tempdir().unwrap();
let export = make_export(vec![]);
let report = build_api_surface_report(dir.path(), &[], &export, &default_limits()).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);
}