use splice::context::{extract_context, extract_context_asymmetric};
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
use std::io::Write;
use std::path::Path;
use std::time::Instant;
use tempfile::TempDir;
fn create_large_rust_file(dir: &Path, name: &str, num_functions: usize) -> std::path::PathBuf {
let file_path = dir.join(name);
let mut file = std::fs::File::create(&file_path).unwrap();
for i in 0..num_functions {
writeln!(
file,
r#"/// Documentation for function {}
/// This function adds two numbers together.
pub fn add_{}(x: i32, y: i32) -> i32 {{
// Line 1 of function body
// Line 2 of function body
// Line 3 of function body
x + y
}}
/// Another function for multiplication
pub fn multiply_{}(x: i32, y: i32) -> i32 {{
x * y
}}
"#,
i, i, i
)
.unwrap();
}
file_path
}
#[test]
fn test_context_extraction_32kb_file() {
let dir = TempDir::new().unwrap();
let file_path = create_large_rust_file(dir.path(), "large_32kb.rs", 350);
let file_size = std::fs::metadata(&file_path).unwrap().len();
assert!(
file_size > 32_000,
"Test file should exceed 32KB, got {} bytes",
file_size
);
let source = std::fs::read_to_string(&file_path).unwrap();
let offset = source.find("add_175").expect("Symbol not found");
let start = Instant::now();
let ctx = extract_context(&file_path, offset, offset + 50, 3).unwrap();
let duration = start.elapsed();
assert!(!ctx.before.is_empty(), "Should have context before");
assert!(!ctx.selected.is_empty(), "Should have selected context");
assert!(!ctx.after.is_empty(), "Should have context after");
assert!(
duration.as_millis() < 100,
"Context extraction on 32KB file took {}ms, expected < 100ms",
duration.as_millis()
);
println!(
"Context extraction on {}KB file took {}ms",
file_size / 1024,
duration.as_millis()
);
}
#[test]
fn test_context_extraction_64kb_file() {
let dir = TempDir::new().unwrap();
let file_path = create_large_rust_file(dir.path(), "large_64kb.rs", 700);
let file_size = std::fs::metadata(&file_path).unwrap().len();
assert!(
file_size > 64_000,
"Test file should exceed 64KB, got {} bytes",
file_size
);
let source = std::fs::read_to_string(&file_path).unwrap();
let offset = source.find("add_350").expect("Symbol not found");
let start = Instant::now();
let ctx = extract_context(&file_path, offset, offset + 50, 3).unwrap();
let duration = start.elapsed();
assert!(!ctx.before.is_empty(), "Should have context before");
assert!(!ctx.selected.is_empty(), "Should have selected context");
assert!(!ctx.after.is_empty(), "Should have context after");
assert!(
duration.as_millis() < 200,
"Context extraction on 64KB file took {}ms, expected < 200ms",
duration.as_millis()
);
println!(
"Context extraction on {}KB file took {}ms",
file_size / 1024,
duration.as_millis()
);
}
#[test]
fn test_context_extraction_128kb_file() {
let dir = TempDir::new().unwrap();
let file_path = create_large_rust_file(dir.path(), "large_128kb.rs", 1400);
let file_size = std::fs::metadata(&file_path).unwrap().len();
assert!(
file_size > 128_000,
"Test file should exceed 128KB, got {} bytes",
file_size
);
let source = std::fs::read_to_string(&file_path).unwrap();
let offset = source.find("add_700").expect("Symbol not found");
let start = Instant::now();
let ctx = extract_context(&file_path, offset, offset + 50, 3).unwrap();
let duration = start.elapsed();
assert!(!ctx.before.is_empty(), "Should have context before");
assert!(!ctx.selected.is_empty(), "Should have selected context");
assert!(!ctx.after.is_empty(), "Should have context after");
assert!(
duration.as_millis() < 400,
"Context extraction on 128KB file took {}ms, expected < 400ms",
duration.as_millis()
);
println!(
"Context extraction on {}KB file took {}ms",
file_size / 1024,
duration.as_millis()
);
}
#[test]
fn test_context_extraction_with_expansion_64kb() {
let dir = TempDir::new().unwrap();
let file_path = create_large_rust_file(dir.path(), "large_expand.rs", 700);
let file_size = std::fs::metadata(&file_path).unwrap().len();
assert!(
file_size > 64_000,
"Test file should exceed 64KB, got {} bytes",
file_size
);
let source = std::fs::read_to_string(&file_path).unwrap();
let offset = source.find("add_350").expect("Symbol not found");
let start = Instant::now();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file_path, offset, Language::Rust).unwrap();
let ctx = extract_context(&file_path, expanded_start, expanded_end, 3).unwrap();
let duration = start.elapsed();
assert!(
expanded_end > expanded_start,
"Should expand to larger span"
);
assert!(!ctx.before.is_empty(), "Should have context before");
assert!(
!ctx.selected.is_empty(),
"Should have selected content from expanded span"
);
assert!(
duration.as_millis() < 300,
"Expansion + context extraction on 64KB file took {}ms, expected < 300ms",
duration.as_millis()
);
println!(
"Expansion + context extraction on {}KB file took {}ms",
file_size / 1024,
duration.as_millis()
);
}
#[test]
fn test_asymmetric_context_extraction_64kb() {
let dir = TempDir::new().unwrap();
let file_path = create_large_rust_file(dir.path(), "large_asymmetric.rs", 700);
let file_size = std::fs::metadata(&file_path).unwrap().len();
assert!(
file_size > 64_000,
"Test file should exceed 64KB, got {} bytes",
file_size
);
let source = std::fs::read_to_string(&file_path).unwrap();
let offset = source.find("add_350").expect("Symbol not found");
let start = Instant::now();
let ctx = extract_context_asymmetric(&file_path, offset, offset + 50, 5, 2).unwrap();
let duration = start.elapsed();
assert!(
ctx.before.len() <= 5,
"Should have at most 5 lines before, got {}",
ctx.before.len()
);
assert!(
ctx.after.len() <= 2,
"Should have at most 2 lines after, got {}",
ctx.after.len()
);
assert!(!ctx.selected.is_empty(), "Should have selected content");
assert!(
duration.as_millis() < 150,
"Asymmetric context extraction on 64KB file took {}ms, expected < 150ms",
duration.as_millis()
);
println!(
"Asymmetric context extraction ({} before, {} after) on {}KB file took {}ms",
ctx.before.len(),
ctx.after.len(),
file_size / 1024,
duration.as_millis()
);
}
#[test]
fn test_context_extraction_at_file_boundaries() {
let dir = TempDir::new().unwrap();
let file_path = create_large_rust_file(dir.path(), "large_boundary.rs", 350);
let source = std::fs::read_to_string(&file_path).unwrap();
let first_offset = source.find("add_0").expect("Symbol not found");
let start = Instant::now();
let ctx_start = extract_context(&file_path, first_offset, first_offset + 50, 3).unwrap();
let duration_start = start.elapsed();
assert!(
!ctx_start.selected.is_empty(),
"Should extract at file start"
);
assert!(
ctx_start.before.len() <= 3,
"At file start, before context should be minimal"
);
let last_offset = source.rfind("multiply_").expect("Symbol not found");
let start = Instant::now();
let ctx_end = extract_context(&file_path, last_offset, last_offset + 50, 3).unwrap();
let duration_end = start.elapsed();
assert!(!ctx_end.selected.is_empty(), "Should extract at file end");
assert!(
ctx_end.after.len() <= 1,
"At file end, after context should be minimal"
);
assert!(
duration_start.as_millis() < 50,
"Context extraction at file start took {}ms, expected < 50ms",
duration_start.as_millis()
);
assert!(
duration_end.as_millis() < 50,
"Context extraction at file end took {}ms, expected < 50ms",
duration_end.as_millis()
);
println!(
"Boundary extraction: start {}ms, end {}ms",
duration_start.as_millis(),
duration_end.as_millis()
);
}
#[test]
fn test_context_extraction_linear_scaling() {
let dir = TempDir::new().unwrap();
let test_cases = vec![
(100, 50), (200, 100), (400, 200), ];
let mut timings = Vec::new();
for (num_functions, expected_max_ms) in test_cases {
let file_path = create_large_rust_file(
dir.path(),
&format!("scale_{}.rs", num_functions),
num_functions,
);
let source = std::fs::read_to_string(&file_path).unwrap();
let offset = source.find("add_").expect("Symbol not found");
let start = Instant::now();
let ctx = extract_context(&file_path, offset, offset + 50, 3).unwrap();
let duration = start.elapsed();
assert!(
!ctx.before.is_empty(),
"Should extract context for {} functions",
num_functions
);
let duration_ms = duration.as_millis();
timings.push((num_functions, duration_ms));
assert!(
duration_ms < expected_max_ms,
"Context extraction on {}-function file took {}ms, expected < {}ms",
num_functions,
duration_ms,
expected_max_ms
);
println!("{} functions: {}ms", num_functions, duration_ms);
}
if timings.len() >= 2 {
let (_, time1) = timings[0];
let (count2, time2) = timings[1];
let ratio = time2 as f64 / time1.max(1) as f64;
assert!(
ratio < 3.0,
"Performance scaling seems non-linear: {} functions took {:.2}x the time of {} functions",
count2,
ratio,
timings[0].0
);
}
}
#[test]
fn test_context_extraction_zero_context_large_file() {
let dir = TempDir::new().unwrap();
let file_path = create_large_rust_file(dir.path(), "large_zero.rs", 350);
let source = std::fs::read_to_string(&file_path).unwrap();
let offset = source.find("add_175").expect("Symbol not found");
let start = Instant::now();
let ctx = extract_context(&file_path, offset, offset + 50, 0).unwrap();
let duration = start.elapsed();
assert_eq!(ctx.before.len(), 0, "Should have no context before");
assert_eq!(ctx.after.len(), 0, "Should have no context after");
assert!(!ctx.selected.is_empty(), "Should have selected content");
assert!(
duration.as_millis() < 50,
"Zero-context extraction took {}ms, expected < 50ms",
duration.as_millis()
);
}
#[test]
fn test_context_extraction_large_context_window() {
let dir = TempDir::new().unwrap();
let file_path = create_large_rust_file(dir.path(), "large_window.rs", 700);
let source = std::fs::read_to_string(&file_path).unwrap();
let offset = source.find("add_350").expect("Symbol not found");
let start = Instant::now();
let ctx = extract_context(&file_path, offset, offset + 50, 20).unwrap();
let duration = start.elapsed();
assert!(
ctx.before.len() <= 20,
"Should have at most 20 lines before, got {}",
ctx.before.len()
);
assert!(
ctx.after.len() <= 20,
"Should have at most 20 lines after, got {}",
ctx.after.len()
);
assert!(
duration.as_millis() < 200,
"Large context window extraction took {}ms, expected < 200ms",
duration.as_millis()
);
println!(
"Large context window (up to 20 lines): {}ms",
duration.as_millis()
);
}