use splitrs::file_analyzer::FileAnalyzer;
use splitrs::module_generator::generate_tests_rs_with_uses;
use std::collections::{HashMap, HashSet};
fn analyze(code: &str) -> (syn::File, FileAnalyzer) {
let file = syn::parse_file(code).expect("test fixture failed to parse as Rust");
let mut analyzer = FileAnalyzer::new(false, 500);
analyzer.analyze(&file);
(file, analyzer)
}
fn render_modules(file: &syn::File, analyzer: &FileAnalyzer) -> HashMap<String, String> {
let modules = analyzer.group_by_module(500);
let mut type_to_module: HashMap<String, String> = HashMap::new();
for m in &modules {
for t in m.get_exported_types() {
type_to_module.insert(t, m.name.clone());
}
}
let (needs_pub_super, cross_module_imports, fields_need_pub_super) =
analyzer.compute_cross_module_visibility(&modules);
modules
.iter()
.map(|m| {
let content = m.generate_content(
file,
&analyzer.use_statements,
&type_to_module,
&needs_pub_super,
cross_module_imports.get(&m.name),
&fields_need_pub_super,
Some(&analyzer.trait_tracker),
&HashSet::new(),
);
(m.name.clone(), content)
})
.collect()
}
#[test]
fn impl_method_to_sibling_helper_elevates_and_imports() {
let code = r#"
pub struct Foo {
pub value: i32,
}
impl Foo {
pub fn use_helper(&self) -> i32 {
helper(self.value)
}
}
fn helper(x: i32) -> i32 {
x * 2
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let functions_rs = rendered
.get("functions")
.expect("expected `functions` module");
let types_rs = rendered.get("types").expect("expected `types` module");
assert!(
functions_rs.contains("pub(super) fn helper"),
"helper must be elevated to pub(super); got:\n{}",
functions_rs
);
assert!(
types_rs.contains("use super::functions::helper"),
"types.rs must import `helper`; got:\n{}",
types_rs
);
}
#[test]
fn already_pub_helper_stays_pub() {
let code = r#"
pub struct Foo;
impl Foo {
pub fn use_helper(&self) -> i32 {
helper()
}
}
pub fn helper() -> i32 {
42
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let functions_rs = rendered
.get("functions")
.expect("expected `functions` module");
assert!(
functions_rs.contains("pub fn helper") && !functions_rs.contains("pub(super) fn helper"),
"already-pub helper must not be downgraded; got:\n{}",
functions_rs
);
}
#[test]
fn no_cross_refs_means_no_super_imports() {
let code = r#"
pub struct Alpha {
pub a: i32,
}
pub struct Beta {
pub b: i32,
}
impl Alpha {
pub fn ping(&self) -> i32 {
self.a
}
}
impl Beta {
pub fn pong(&self) -> i32 {
self.b
}
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
if let Some(types_rs) = rendered.get("types") {
assert!(
!types_rs.contains("use super::functions"),
"types.rs must not import from non-existent `functions` module; got:\n{}",
types_rs
);
}
}
#[test]
fn multiple_cross_refs_use_grouped_import() {
let code = r#"
pub struct Calc {
pub state: i32,
}
impl Calc {
pub fn run(&self) -> i32 {
helper_one(self.state) + helper_two(self.state) + helper_three(self.state)
}
}
fn helper_one(x: i32) -> i32 { x + 1 }
fn helper_two(x: i32) -> i32 { x + 2 }
fn helper_three(x: i32) -> i32 { x + 3 }
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let types_rs = rendered.get("types").expect("expected `types` module");
let functions_rs = rendered
.get("functions")
.expect("expected `functions` module");
for name in ["helper_one", "helper_two", "helper_three"] {
assert!(
functions_rs.contains(&format!("pub(super) fn {}", name)),
"{} must be elevated to pub(super); got:\n{}",
name,
functions_rs
);
}
let has_grouped = types_rs.contains("use super::functions::{")
&& types_rs.contains("helper_one")
&& types_rs.contains("helper_two")
&& types_rs.contains("helper_three");
assert!(
has_grouped,
"types.rs should use a single grouped import; got:\n{}",
types_rs
);
}
#[test]
fn function_referencing_sibling_type_gets_import() {
let code = r#"
pub struct Widget {
pub id: u32,
}
impl Widget {
pub fn new(id: u32) -> Self {
Self { id }
}
}
pub fn make_widget(id: u32) -> Widget {
Widget::new(id)
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let functions_rs = rendered
.get("functions")
.expect("expected `functions` module");
assert!(
functions_rs.contains("use super::types::Widget")
|| functions_rs.contains("use super::types::{") && functions_rs.contains("Widget"),
"functions.rs must import Widget; got:\n{}",
functions_rs
);
}
#[test]
fn external_fn_call_does_not_emit_super_import() {
let code = r#"
pub struct Foo;
impl Foo {
pub fn boom(&self) {
external_crate_fn();
}
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let types_rs = rendered.get("types").expect("expected `types` module");
let mut in_uses = false;
for line in types_rs.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with("use super::") && trimmed.contains("external_crate_fn") {
in_uses = true;
break;
}
}
assert!(
!in_uses,
"external_crate_fn must not appear in any use statement; got:\n{}",
types_rs
);
assert!(
!types_rs.contains("use super::functions"),
"types.rs must not import from a non-existent `functions` module; got:\n{}",
types_rs
);
}
#[test]
fn picking_rs_shape_end_to_end() {
let code = r#"
use std::vec::Vec;
pub struct Ray {
pub origin: [f64; 3],
pub direction: [f64; 3],
}
impl Ray {
pub fn new(origin: [f64; 3], direction: [f64; 3]) -> Self {
Self { origin, direction }
}
pub fn distance_to_point(&self, p: [f64; 3]) -> f64 {
let diff = sub(p, self.origin);
let crossed = cross(diff, self.direction);
norm(crossed) / norm(self.direction)
}
}
fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn norm(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let functions_rs = rendered
.get("functions")
.expect("expected `functions` module");
let types_rs = rendered.get("types").expect("expected `types` module");
for name in ["cross", "norm", "sub"] {
assert!(
functions_rs.contains(&format!("pub(super) fn {}", name)),
"{} must be elevated to pub(super); got:\n{}",
name,
functions_rs
);
}
let needs_all = types_rs.contains("cross")
&& types_rs.contains("norm")
&& types_rs.contains("sub")
&& types_rs.contains("use super::functions::");
assert!(
needs_all,
"types.rs must `use super::functions::{{...}}`; got:\n{}",
types_rs
);
syn::parse_file(types_rs).expect("types.rs must parse");
syn::parse_file(functions_rs).expect("functions.rs must parse");
}
#[test]
fn tests_rs_forwards_external_use_statements() {
let code = r#"
use external_crate::{Alpha, Beta};
pub struct Foo;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn t() {
let _a = Alpha::default();
let _b = Beta::default();
}
}
"#;
let file = syn::parse_file(code).expect("fixture must parse");
let mut analyzer = FileAnalyzer::new(false, 500);
analyzer.set_extract_tests(true);
analyzer.analyze(&file);
let extracted = analyzer.take_extracted_tests();
assert_eq!(extracted.len(), 1, "exactly one inline test mod expected");
let tests_rs = generate_tests_rs_with_uses(&extracted, &analyzer.use_statements);
assert!(
tests_rs.contains("use external_crate"),
"tests.rs must forward `use external_crate::...`; got:\n{}",
tests_rs
);
assert!(
tests_rs.contains("use super::*;"),
"tests.rs must still emit `use super::*;`; got:\n{}",
tests_rs
);
}
#[test]
fn trait_impl_method_to_sibling_helper_elevates() {
let code = r#"
pub trait Doer {
fn do_it(&self) -> i32;
}
pub struct Worker;
impl Doer for Worker {
fn do_it(&self) -> i32 {
compute(7)
}
}
fn compute(x: i32) -> i32 {
x * 3
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let functions_rs = rendered
.get("functions")
.expect("expected `functions` module");
let trait_impls_rs = rendered
.get("worker_traits")
.expect("expected `worker_traits` module (per-type trait grouping)");
assert!(
functions_rs.contains("pub(super) fn compute"),
"compute must be elevated to pub(super); got:\n{}",
functions_rs
);
let imports_compute_from_functions = trait_impls_rs.contains("use super::functions::compute")
|| trait_impls_rs
.split("use super::functions::{")
.nth(1)
.and_then(|after| after.split('}').next())
.is_some_and(|group| group.contains("compute"));
assert!(
imports_compute_from_functions,
"worker_traits.rs must import `compute` from `super::functions`; got:\n{}",
trait_impls_rs
);
}
#[test]
fn split_impl_block_chunk_calling_sibling_chunk_private_method_elevates_and_imports() {
let mut code = String::from(
r#"
pub struct Big {
pub value: i32,
}
fn helper_fn(x: i32) -> i32 {
x * 3
}
impl Big {
fn helper(&self) -> i32 {
self.value * 2
}
"#,
);
for i in 0..6 {
code.push_str(&format!(
" pub fn padding_{i}(&self) -> i32 {{\n \
let mut acc = self.value;\n \
acc += {i};\n \
acc += {i} * 2;\n \
acc += {i} * 3;\n \
acc\n \
}}\n"
));
}
code.push_str(
r#"
pub fn caller(&self) -> i32 {
self.helper() + helper_fn(self.value)
}
}
"#,
);
let mut analyzer = FileAnalyzer::new(true, 8);
let file = syn::parse_file(&code).expect("test fixture failed to parse as Rust");
analyzer.analyze(&file);
let modules = analyzer.group_by_module(8);
let method_group_modules: Vec<&splitrs::module_generator::Module> = modules
.iter()
.filter(|m| m.method_group.is_some())
.collect();
assert!(
method_group_modules.len() >= 2,
"fixture must force `impl Big` across >= 2 method_group chunks; got {} chunk module(s): {:?}",
method_group_modules.len(),
modules.iter().map(|m| &m.name).collect::<Vec<_>>()
);
let module_of = |method_name: &str| {
method_group_modules
.iter()
.find(|m| {
m.method_group
.as_ref()
.is_some_and(|g| g.methods.iter().any(|meth| meth.name == method_name))
})
.map(|m| m.name.clone())
};
let helper_module = module_of("helper").expect("fixture must define `helper`");
let caller_module = module_of("caller").expect("fixture must define `caller`");
assert_ne!(
helper_module, caller_module,
"fixture must place `helper` and `caller` in different chunks to reproduce the bug"
);
let (needs_pub_super, cross_module_imports, _fields_need_pub_super) =
analyzer.compute_cross_module_visibility(&modules);
assert!(
needs_pub_super.contains("helper"),
"`helper` (method) must be elevated to pub(super); got: {:?}",
needs_pub_super
);
assert!(
needs_pub_super.contains("helper_fn"),
"`helper_fn` (free fn) must be elevated to pub(super); got: {:?}",
needs_pub_super
);
let caller_imports = cross_module_imports.get(&caller_module);
let imports_helper_method = caller_imports.is_some_and(|by_module| {
by_module
.values()
.any(|names| names.iter().any(|n| n == "helper"))
});
assert!(
!imports_helper_method,
"{caller_module:?} must NOT import the method `helper` via `use` \
(E0432: methods aren't module-path items); got: {:?}",
caller_imports
);
let imports_helper_fn = caller_imports.is_some_and(|by_module| {
by_module
.values()
.any(|names| names.iter().any(|n| n == "helper_fn"))
});
assert!(
imports_helper_fn,
"{caller_module:?} must import the free function `helper_fn`; got: {:?}",
caller_imports
);
let mut type_to_module: HashMap<String, String> = HashMap::new();
for m in &modules {
for t in m.get_exported_types() {
type_to_module.insert(t, m.name.clone());
}
}
let rendered: HashMap<String, String> = modules
.iter()
.map(|m| {
let content = m.generate_content(
&file,
&analyzer.use_statements,
&type_to_module,
&needs_pub_super,
cross_module_imports.get(&m.name),
&_fields_need_pub_super,
Some(&analyzer.trait_tracker),
&HashSet::new(),
);
(m.name.clone(), content)
})
.collect();
for (name, content) in &rendered {
syn::parse_file(content)
.unwrap_or_else(|e| panic!("generated module {name:?} must parse: {e}\n{content}"));
}
let helper_rs = &rendered[&helper_module];
assert!(
helper_rs.contains("pub(super) fn helper("),
"{helper_module:?} must render `helper` as `pub(super)`; got:\n{helper_rs}"
);
let caller_rs = &rendered[&caller_module];
assert!(
!caller_rs.lines().any(|l| {
let t = l.trim_start();
t.starts_with("use ") && t.contains("::helper;") && !t.contains("helper_fn")
}),
"{caller_module:?} must not `use` the method `helper` by path; got:\n{caller_rs}"
);
assert!(
caller_rs.contains("helper_fn"),
"{caller_module:?} must import `helper_fn` from `super::functions`; got:\n{caller_rs}"
);
}
#[test]
fn extracted_test_referencing_sibling_const_gets_import() {
let code = r#"
const MAGIC: u32 = 42;
pub struct Foo {
pub value: u32,
}
impl Foo {
pub fn new(value: u32) -> Self {
Self { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uses_magic_constant() {
assert_eq!(MAGIC, 42);
}
}
"#;
let file = syn::parse_file(code).expect("fixture must parse");
let mut analyzer = FileAnalyzer::new(false, 500);
analyzer.set_extract_tests(true);
analyzer.analyze(&file);
let modules = analyzer.group_by_module(500);
let (needs_pub_super, cross_module_imports, _fields_need_pub_super) =
analyzer.compute_cross_module_visibility(&modules);
let extracted = analyzer.take_extracted_tests();
assert_eq!(extracted.len(), 1, "exactly one inline test mod expected");
let empty = HashMap::new();
let tests_sibling_imports = cross_module_imports.get("tests").unwrap_or(&empty);
let tests_rs = splitrs::module_generator::generate_tests_rs_with_imports(
&extracted,
&analyzer.use_statements,
tests_sibling_imports,
);
assert!(
tests_rs.contains("MAGIC"),
"tests.rs must import the sibling module's `MAGIC` const; got:\n{tests_rs}"
);
let imports_magic = tests_rs.lines().any(|l| {
let t = l.trim_start();
t.starts_with("use ") && t.contains("::MAGIC")
});
assert!(
imports_magic,
"tests.rs must contain a `use super::<module>::MAGIC;`-shaped import; got:\n{tests_rs}"
);
syn::parse_file(&tests_rs)
.unwrap_or_else(|e| panic!("generated tests.rs must parse: {e}\n{tests_rs}"));
let mut type_to_module: HashMap<String, String> = HashMap::new();
for m in &modules {
for t in m.get_exported_types() {
type_to_module.insert(t, m.name.clone());
}
}
for m in &modules {
let content = m.generate_content(
&file,
&analyzer.use_statements,
&type_to_module,
&needs_pub_super,
cross_module_imports.get(&m.name),
&_fields_need_pub_super,
Some(&analyzer.trait_tracker),
&HashSet::new(),
);
syn::parse_file(&content)
.unwrap_or_else(|e| panic!("module {:?} must parse: {e}\n{content}", m.name));
}
}
#[test]
fn file_backed_mod_declaration_is_diverted_not_bucketed() {
let code = r#"
pub mod existing;
pub struct Foo {
pub value: i32,
}
"#;
let (_file, mut analyzer) = analyze(code);
let file_backed = analyzer.take_file_backed_mods();
assert_eq!(
file_backed
.iter()
.map(|m| m.ident.to_string())
.collect::<Vec<_>>(),
vec!["existing".to_string()],
"file-backed `pub mod existing;` must be captured, not bucketed"
);
let modules = analyzer.group_by_module(500);
for m in &modules {
assert!(
!m.standalone_items
.iter()
.any(|item| matches!(item, syn::Item::Mod(mi) if mi.ident == "existing")),
"file-backed mod must never land in a generated bucket's \
standalone_items (module {:?})",
m.name
);
}
}
#[test]
fn use_of_pinned_sibling_mod_symbol_gets_super_prefix() {
let code = r#"
pub mod existing;
use existing::Marker;
pub struct Foo {
pub value: i32,
}
impl Foo {
pub fn make(&self) -> Marker {
Marker
}
}
"#;
let (file, mut analyzer) = analyze(code);
let file_backed = analyzer.take_file_backed_mods();
let pinned_root_mod_names: HashSet<String> =
file_backed.iter().map(|m| m.ident.to_string()).collect();
let modules = analyzer.group_by_module(500);
let types_mod = modules
.iter()
.find(|m| m.name == "types")
.expect("expected a `types` module containing `Foo`");
let (needs_pub_super, cross_module_imports, fields_need_pub_super) =
analyzer.compute_cross_module_visibility(&modules);
let type_to_module: HashMap<String, String> = HashMap::new();
let content = types_mod.generate_content(
&file,
&analyzer.use_statements,
&type_to_module,
&needs_pub_super,
cross_module_imports.get(&types_mod.name),
&fields_need_pub_super,
Some(&analyzer.trait_tracker),
&pinned_root_mod_names,
);
assert!(
content.contains("use super::existing::Marker"),
"reference to a pinned sibling mod's symbol must be rewritten with a \
`super::` prefix (a bare `use existing::Marker;`, valid only from \
the original root file, does not resolve from a sibling generated \
file); got:\n{}",
content
);
assert!(
!content.contains("\nuse existing::Marker"),
"must not ALSO keep the unprefixed bare form; got:\n{}",
content
);
syn::parse_file(&content)
.unwrap_or_else(|e| panic!("generated module must parse: {e}\n{content}"));
}
#[test]
fn bitflags_invocation_defined_type_is_importable_across_modules() {
let code = r#"
bitflags::bitflags! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Flags: u8 {
const A = 0x01;
const B = 0x02;
}
}
pub struct Holder {
pub flags: Flags,
}
"#;
let (file, analyzer) = analyze(code);
let modules = analyzer.group_by_module(500);
let macros_mod = modules
.iter()
.find(|m| m.name == "macros")
.expect("expected a `macros` module containing the bitflags! invocation");
assert!(
macros_mod
.get_exported_types()
.contains(&"Flags".to_string()),
"bitflags! invocation must register `Flags` as an exported/importable \
type name, not just an opaque macro item; exported: {:?}",
macros_mod.get_exported_types()
);
let mut type_to_module: HashMap<String, String> = HashMap::new();
for m in &modules {
for t in m.importable_exported_names() {
type_to_module.insert(t, m.name.clone());
}
}
let (needs_pub_super, cross_module_imports, fields_need_pub_super) =
analyzer.compute_cross_module_visibility(&modules);
let types_mod = modules
.iter()
.find(|m| m.name == "types")
.expect("expected a `types` module containing `Holder`");
let content = types_mod.generate_content(
&file,
&analyzer.use_statements,
&type_to_module,
&needs_pub_super,
cross_module_imports.get(&types_mod.name),
&fields_need_pub_super,
Some(&analyzer.trait_tracker),
&HashSet::new(),
);
assert!(
content.contains("Flags"),
"`Holder`'s `flags: Flags` field must still reference `Flags`; got:\n{}",
content
);
assert!(
content.contains("use super::macros::Flags")
|| content.contains("macros::{") && content.contains("Flags"),
"`Holder`'s `flags: Flags` field must import `Flags` from wherever the \
bitflags! invocation landed; got:\n{}",
content
);
syn::parse_file(&content)
.unwrap_or_else(|e| panic!("generated module must parse: {e}\n{content}"));
assert!(
macros_mod.has_public_reexport(),
"a module containing a `pub struct`-defining bitflags! invocation \
must be considered to have a public reexport"
);
assert!(
macros_mod
.public_export_names()
.contains(&"Flags".to_string()),
"`Flags` must appear in the module's public export names; got: {:?}",
macros_mod.public_export_names()
);
}
#[test]
fn associated_from_str_call_keeps_fromstr_import() {
let code = r#"
use std::str::FromStr;
pub struct Port(pub u16);
impl Port {
pub fn parse_from(text: &str) -> Port {
Port(u16::from_str(text).unwrap_or(0))
}
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let types_rs = rendered.get("types").expect("expected `types` module");
assert!(
types_rs.contains("FromStr"),
"an associated `u16::from_str(...)` call must keep the forwarded \
`use std::str::FromStr;` import alive; got:\n{}",
types_rs
);
syn::parse_file(types_rs)
.unwrap_or_else(|e| panic!("generated module must parse: {e}\n{types_rs}"));
}
#[test]
fn self_leaf_in_group_is_pruned_independently_of_sibling_glob() {
let code = r#"
use std::cmp::Ordering::{self, *};
pub struct Foo {
pub value: i32,
}
impl Foo {
pub fn cmp_value(&self, other: i32) -> bool {
self.value.cmp(&other) == Less
}
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let types_rs = rendered.get("types").expect("expected `types` module");
assert!(
types_rs.contains("Ordering::{self, *}") || types_rs.contains("Ordering::*"),
"the `*` glob (providing `Less`) must be kept even without `self`; \
got:\n{}",
types_rs
);
assert!(
!types_rs.contains("{self, *}") && !types_rs.contains("{*, self}"),
"`Ordering` (the `self` binding) is never referenced by name in this \
module, only `Less` (via the glob) is -- keeping `self` produces an \
`unused_imports` warning; got:\n{}",
types_rs
);
syn::parse_file(types_rs)
.unwrap_or_else(|e| panic!("generated module must parse: {e}\n{types_rs}"));
}
#[test]
fn self_leaf_in_group_is_kept_when_enclosing_name_is_used() {
let code = r#"
use std::cmp::Ordering::{self, *};
pub struct Foo {
pub ord: Ordering,
}
impl Foo {
pub fn cmp_value(&self, other: i32) -> bool {
self.ord == Less
}
}
"#;
let (file, analyzer) = analyze(code);
let rendered = render_modules(&file, &analyzer);
let types_rs = rendered.get("types").expect("expected `types` module");
assert!(
types_rs.contains("{self, *}") || types_rs.contains("{*, self}"),
"`Ordering` (the `self` binding) IS used as the `ord` field's type, \
so it must be kept alongside the glob providing `Less`; got:\n{}",
types_rs
);
syn::parse_file(types_rs)
.unwrap_or_else(|e| panic!("generated module must parse: {e}\n{types_rs}"));
}