use std::collections::BTreeMap;
use tokmd_substrate::{DiffRange, LangSummary, RepoSubstrate, SubstrateFile};
use proptest::prelude::*;
fn arb_lang_summary() -> impl Strategy<Value = LangSummary> {
(
0..100usize,
0..10_000usize,
0..10_000usize,
0..1_000_000usize,
0..100_000usize,
)
.prop_map(|(files, code, lines, bytes, tokens)| LangSummary {
files,
code,
lines,
bytes,
tokens,
})
}
fn arb_substrate_file() -> impl Strategy<Value = SubstrateFile> {
(
"[a-z/]{1,50}", "[A-Za-z]{1,20}", 0..10_000usize, 0..10_000usize, 0..1_000_000usize, 0..100_000usize, "[a-z/]{0,30}", any::<bool>(), )
.prop_map(
|(path, lang, code, lines, bytes, tokens, module, in_diff)| SubstrateFile {
path,
lang,
code,
lines,
bytes,
tokens,
module,
in_diff,
},
)
}
fn arb_diff_range() -> impl Strategy<Value = DiffRange> {
(
"[a-z0-9./-]{1,30}", "[a-z0-9./-]{1,30}", prop::collection::vec("[a-z/.]{1,40}", 0..10), 0..500usize, 0..10_000usize, 0..10_000usize, )
.prop_map(
|(base, head, changed_files, commit_count, insertions, deletions)| DiffRange {
base,
head,
changed_files,
commit_count,
insertions,
deletions,
},
)
}
fn arb_lang_summary_map() -> impl Strategy<Value = BTreeMap<String, LangSummary>> {
prop::collection::btree_map("[A-Za-z]{1,15}", arb_lang_summary(), 0..5)
}
fn arb_repo_substrate() -> impl Strategy<Value = RepoSubstrate> {
(
"[a-z/]{1,50}", prop::collection::vec(arb_substrate_file(), 0..10), arb_lang_summary_map(), prop::option::of(arb_diff_range()), 0..1_000_000usize, 0..10_000_000usize, 0..1_000_000usize, )
.prop_map(
|(
repo_root,
files,
lang_summary,
diff_range,
total_tokens,
total_bytes,
total_code_lines,
)| {
RepoSubstrate {
repo_root,
files,
lang_summary,
diff_range,
total_tokens,
total_bytes,
total_code_lines,
}
},
)
}
proptest! {
#[test]
fn substrate_file_roundtrip(file in arb_substrate_file()) {
let json = serde_json::to_string(&file).unwrap();
let back: SubstrateFile = serde_json::from_str(&json).unwrap();
prop_assert_eq!(&back.path, &file.path);
prop_assert_eq!(&back.lang, &file.lang);
prop_assert_eq!(back.code, file.code);
prop_assert_eq!(back.lines, file.lines);
prop_assert_eq!(back.bytes, file.bytes);
prop_assert_eq!(back.tokens, file.tokens);
prop_assert_eq!(&back.module, &file.module);
prop_assert_eq!(back.in_diff, file.in_diff);
}
#[test]
fn lang_summary_roundtrip(ls in arb_lang_summary()) {
let json = serde_json::to_string(&ls).unwrap();
let back: LangSummary = serde_json::from_str(&json).unwrap();
prop_assert_eq!(back.files, ls.files);
prop_assert_eq!(back.code, ls.code);
prop_assert_eq!(back.lines, ls.lines);
prop_assert_eq!(back.bytes, ls.bytes);
prop_assert_eq!(back.tokens, ls.tokens);
}
#[test]
fn diff_range_roundtrip(dr in arb_diff_range()) {
let json = serde_json::to_string(&dr).unwrap();
let back: DiffRange = serde_json::from_str(&json).unwrap();
prop_assert_eq!(&back.base, &dr.base);
prop_assert_eq!(&back.head, &dr.head);
prop_assert_eq!(&back.changed_files, &dr.changed_files);
prop_assert_eq!(back.commit_count, dr.commit_count);
prop_assert_eq!(back.insertions, dr.insertions);
prop_assert_eq!(back.deletions, dr.deletions);
}
#[test]
fn repo_substrate_roundtrip(sub in arb_repo_substrate()) {
let json = serde_json::to_string(&sub).unwrap();
let back: RepoSubstrate = serde_json::from_str(&json).unwrap();
prop_assert_eq!(&back.repo_root, &sub.repo_root);
prop_assert_eq!(back.files.len(), sub.files.len());
prop_assert_eq!(back.lang_summary.len(), sub.lang_summary.len());
prop_assert_eq!(back.total_tokens, sub.total_tokens);
prop_assert_eq!(back.total_bytes, sub.total_bytes);
prop_assert_eq!(back.total_code_lines, sub.total_code_lines);
prop_assert_eq!(back.diff_range.is_some(), sub.diff_range.is_some());
}
#[test]
fn diff_files_count_le_total(sub in arb_repo_substrate()) {
prop_assert!(sub.diff_files().count() <= sub.files.len());
}
#[test]
fn diff_files_all_in_diff(sub in arb_repo_substrate()) {
for f in sub.diff_files() {
prop_assert!(f.in_diff);
}
}
#[test]
fn files_for_lang_correct(sub in arb_repo_substrate(), lang in "[A-Za-z]{1,10}") {
for f in sub.files_for_lang(&lang) {
prop_assert_eq!(&f.lang, &lang);
}
}
#[test]
fn lang_summary_keys_sorted(sub in arb_repo_substrate()) {
let keys: Vec<_> = sub.lang_summary.keys().collect();
for w in keys.windows(2) {
prop_assert!(w[0] <= w[1], "BTreeMap keys should be sorted");
}
}
#[test]
fn no_diff_range_skipped_in_json(sub in arb_repo_substrate()) {
if sub.diff_range.is_none() {
let json = serde_json::to_string(&sub).unwrap();
prop_assert!(!json.contains("diff_range"));
}
}
#[test]
fn clone_identical(sub in arb_repo_substrate()) {
let cloned = sub.clone();
let j1 = serde_json::to_string(&sub).unwrap();
let j2 = serde_json::to_string(&cloned).unwrap();
prop_assert_eq!(j1, j2);
}
#[test]
fn files_for_lang_partition(sub in arb_repo_substrate()) {
let langs: Vec<String> = sub.lang_summary.keys().cloned().collect();
let counted: usize = langs.iter()
.map(|l| sub.files_for_lang(l).count())
.sum();
prop_assert!(counted <= sub.files.len(),
"per-lang file counts must not exceed total files");
}
#[test]
fn diff_nondiff_partition(sub in arb_repo_substrate()) {
let diff_count = sub.files.iter().filter(|f| f.in_diff).count();
let non_diff_count = sub.files.iter().filter(|f| !f.in_diff).count();
prop_assert_eq!(diff_count + non_diff_count, sub.files.len());
}
#[test]
fn double_roundtrip_stable(sub in arb_repo_substrate()) {
let json1 = serde_json::to_string(&sub).unwrap();
let mid: RepoSubstrate = serde_json::from_str(&json1).unwrap();
let json2 = serde_json::to_string(&mid).unwrap();
prop_assert_eq!(json1, json2, "double roundtrip must be stable");
}
#[test]
fn substrate_file_all_fields_preserved(file in arb_substrate_file()) {
let json = serde_json::to_string(&file).unwrap();
let back: SubstrateFile = serde_json::from_str(&json).unwrap();
prop_assert_eq!(&back.path, &file.path);
prop_assert_eq!(&back.lang, &file.lang);
prop_assert_eq!(back.code, file.code);
prop_assert_eq!(back.lines, file.lines);
prop_assert_eq!(back.bytes, file.bytes);
prop_assert_eq!(back.tokens, file.tokens);
prop_assert_eq!(&back.module, &file.module);
prop_assert_eq!(back.in_diff, file.in_diff);
}
#[test]
fn diff_range_file_order_preserved(dr in arb_diff_range()) {
let json = serde_json::to_string(&dr).unwrap();
let back: DiffRange = serde_json::from_str(&json).unwrap();
prop_assert_eq!(&back.changed_files, &dr.changed_files);
}
#[test]
fn files_order_preserved(sub in arb_repo_substrate()) {
let json = serde_json::to_string(&sub).unwrap();
let back: RepoSubstrate = serde_json::from_str(&json).unwrap();
for (orig, roundtripped) in sub.files.iter().zip(back.files.iter()) {
prop_assert_eq!(&orig.path, &roundtripped.path);
prop_assert_eq!(&orig.lang, &roundtripped.lang);
}
}
#[test]
fn lang_summary_values_preserved(sub in arb_repo_substrate()) {
let json = serde_json::to_string(&sub).unwrap();
let back: RepoSubstrate = serde_json::from_str(&json).unwrap();
for (lang, orig) in &sub.lang_summary {
let roundtripped = back.lang_summary.get(lang)
.expect("lang must survive roundtrip");
prop_assert_eq!(roundtripped.files, orig.files);
prop_assert_eq!(roundtripped.code, orig.code);
prop_assert_eq!(roundtripped.lines, orig.lines);
prop_assert_eq!(roundtripped.bytes, orig.bytes);
prop_assert_eq!(roundtripped.tokens, orig.tokens);
}
}
}
#[test]
fn empty_substrate_roundtrips() {
let sub = RepoSubstrate {
repo_root: String::new(),
files: vec![],
lang_summary: BTreeMap::new(),
diff_range: None,
total_tokens: 0,
total_bytes: 0,
total_code_lines: 0,
};
let json = serde_json::to_string(&sub).unwrap();
let back: RepoSubstrate = serde_json::from_str(&json).unwrap();
assert!(back.files.is_empty());
assert!(back.lang_summary.is_empty());
assert!(back.diff_range.is_none());
assert_eq!(back.diff_files().count(), 0);
}
#[test]
fn empty_substrate_no_diff_range_in_json() {
let sub = RepoSubstrate {
repo_root: "/repo".to_string(),
files: vec![],
lang_summary: BTreeMap::new(),
diff_range: None,
total_tokens: 0,
total_bytes: 0,
total_code_lines: 0,
};
let json = serde_json::to_string(&sub).unwrap();
assert!(
!json.contains("diff_range"),
"diff_range: None must be omitted from JSON via skip_serializing_if"
);
}
#[test]
fn files_for_nonexistent_lang_returns_empty() {
let sub = RepoSubstrate {
repo_root: "/repo".to_string(),
files: vec![SubstrateFile {
path: "src/lib.rs".to_string(),
lang: "Rust".to_string(),
code: 10,
lines: 20,
bytes: 300,
tokens: 50,
module: "src".to_string(),
in_diff: false,
}],
lang_summary: BTreeMap::new(),
diff_range: None,
total_tokens: 50,
total_bytes: 300,
total_code_lines: 10,
};
assert_eq!(sub.files_for_lang("Python").count(), 0);
assert_eq!(sub.files_for_lang("Rust").count(), 1);
}