use splitrs::file_analyzer::FileAnalyzer;
use splitrs::module_generator::generate_tests_rs_with_uses;
use std::collections::HashMap;
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),
);
(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
);
}