use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use tempfile::TempDir;
fn create_test_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
let path = dir.path().join(name);
let mut file = File::create(&path).unwrap();
writeln!(file, "{}", content).unwrap();
path
}
const RUST_FUNCTION_WITH_CONTEXT: &str = r#"
// Leading comment before function
fn example_function() -> i32 {
let x = 42;
let y = 10;
x + y
}
// Trailing comment after function
fn another_function() -> i32 {
0
}
"#;
const PYTHON_CLASS_WITH_CONTEXT: &str = r#"
# Module docstring
class ExampleClass:
"""Example class docstring."""
def method_one(self):
"""First method."""
pass
def method_two(self):
"""Second method."""
pass
# After class
def standalone_function():
pass
"#;
const RUST_STRUCT_WITH_CONTEXT: &str = r#"
// Before struct
struct DataPoint {
x: i32,
y: i32,
}
// After struct
fn process() -> DataPoint {
DataPoint { x: 0, y: 0 }
}
"#;
const RUST_FUNCTION_AFTER_CONTEXT: &str = r#"
fn calculate() -> i32 {
let result = 10 + 20;
result
}
// This comment should appear in -A context after expanded function
fn next_function() -> i32 {
0
}
"#;
const RUST_MODULE_NESTED: &str = r#"
mod outer_module {
// Before inner function
fn inner_function() -> i32 {
42
}
// After inner function
}
"#;
const TYPESCRIPT_INTERFACE_WITH_CONTEXT: &str = r#"
/**
* Example interface documentation
*/
interface ExampleInterface {
name: string;
value: number;
}
// After interface
class Implementation implements ExampleInterface {
name = "test";
value = 42;
}
"#;
const JAVASCRIPT_CLASS_WITH_CONTEXT: &str = r#"
// Leading comment
class JSClass {
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
// Trailing comment
"#;
const CPP_CLASS_WITH_CONTEXT: &str = r#"
// Before class
classCppClass {
public:
int getX() const { return x; }
void setX(int value) { x = value; }
private:
int x = 0;
};
// After class
"#;
const JAVA_CLASS_WITH_CONTEXT: &str = r#"
/**
* Java class documentation
*/
public class JavaClass {
private int value;
public JavaClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
// After class
"#;
#[test]
fn test_context_with_expand_rust() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.rs", RUST_FUNCTION_WITH_CONTEXT);
let source = std::fs::read_to_string(&file).unwrap();
let func_offset = source.find("example_function").unwrap();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, func_offset, Language::Rust).unwrap();
let ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 2, 0).unwrap();
assert!(
!ctx.before.is_empty(),
"Should have context before expanded function"
);
assert!(
ctx.before
.iter()
.any(|line| line.contains("Leading comment")),
"Context should include comment before expanded function"
);
assert!(
ctx.after.is_empty(),
"Should not have context after when only -B requested"
);
}
#[test]
fn test_context_with_expand_python() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.py", PYTHON_CLASS_WITH_CONTEXT);
let source = std::fs::read_to_string(&file).unwrap();
let class_offset = source.find("ExampleClass").unwrap();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, class_offset, Language::Python).unwrap();
let ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 1, 1).unwrap();
assert!(
!ctx.before.is_empty(),
"Should have context before expanded class"
);
let expanded = std::fs::read_to_string(&file).unwrap();
let expanded_content = &expanded[expanded_start..expanded_end];
assert!(
expanded_content.contains("method_one"),
"Should include all class methods"
);
assert!(
expanded_content.contains("method_two"),
"Should include all class methods"
);
}
#[test]
fn test_context_before_respects_expansion() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let content = r#"
fn small_function() -> i32 {
42
}
"#;
let file = create_test_file(&dir, "test.rs", content);
let source = std::fs::read_to_string(&file).unwrap();
let name_offset = source.find("small_function").unwrap();
let name_end = name_offset + "small_function".len();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, name_offset, Language::Rust).unwrap();
let ctx_expanded =
extract_context_asymmetric(&file, expanded_start, expanded_end, 2, 0).unwrap();
let ctx_name = extract_context_asymmetric(&file, name_offset, name_end, 2, 0).unwrap();
assert!(
ctx_expanded.before.len() <= ctx_name.before.len() || ctx_expanded.before.is_empty(),
"Expanded context should differ from non-expanded context"
);
}
#[test]
fn test_context_after_respects_expansion() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.rs", RUST_FUNCTION_AFTER_CONTEXT);
let source = std::fs::read_to_string(&file).unwrap();
let name_offset = source.find("calculate").unwrap();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, name_offset, Language::Rust).unwrap();
let ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 0, 3).unwrap();
assert!(
!ctx.after.is_empty(),
"Should have context after expanded function"
);
assert!(
ctx.after
.iter()
.any(|line| line.contains("This comment") || line.contains("next_function")),
"Context after should be relative to expanded symbol end"
);
}
#[test]
fn test_context_both_respects_expansion() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.rs", RUST_STRUCT_WITH_CONTEXT);
let source = std::fs::read_to_string(&file).unwrap();
let struct_offset = source.find("DataPoint").unwrap();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, struct_offset, Language::Rust).unwrap();
let ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 1, 1).unwrap();
assert!(!ctx.before.is_empty(), "Should have context before");
assert!(
ctx.before.iter().any(|line| line.contains("Before struct")),
"Context -C should include lines before expanded struct"
);
assert!(!ctx.after.is_empty(), "Should have context after");
let expanded = std::fs::read_to_string(&file).unwrap();
let expanded_content = &expanded[expanded_start..expanded_end];
assert!(
expanded_content.contains("x: i32"),
"Should include struct fields"
);
assert!(
expanded_content.contains("y: i32"),
"Should include struct fields"
);
}
#[test]
fn test_context_no_expand_baseline() {
use splice::context::extract_context_asymmetric;
let dir = TempDir::new().unwrap();
let content = r#"
fn test_function() -> i32 {
100
}
"#;
let file = create_test_file(&dir, "test.rs", content);
let source = std::fs::read_to_string(&file).unwrap();
let name_offset = source.find("test_function").unwrap();
let name_end = name_offset + "test_function".len();
let ctx = extract_context_asymmetric(&file, name_offset, name_end, 1, 1).unwrap();
assert!(
ctx.before.len() > 0 || ctx.after.len() > 0 || !ctx.selected.is_empty(),
"Should have some context content"
);
}
#[test]
fn test_context_expand_level_2() {
use splice::context::extract_context_asymmetric;
use splice::expand::{expand_symbol, ExpansionLevel};
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.rs", RUST_MODULE_NESTED);
let source = std::fs::read_to_string(&file).unwrap();
let func_offset = source.find("inner_function").unwrap();
let (expanded_start, expanded_end) = expand_symbol(
&file,
func_offset,
Language::Rust,
ExpansionLevel::ContainingBlock,
)
.unwrap();
let _ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 1, 1).unwrap();
let expanded = std::fs::read_to_string(&file).unwrap();
let expanded_content = &expanded[expanded_start..expanded_end];
assert!(
expanded_content.contains("outer_module") || expanded_content.contains("mod"),
"Level 2 expansion should include containing block"
);
}
#[test]
fn test_context_with_expand_typescript() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.ts", TYPESCRIPT_INTERFACE_WITH_CONTEXT);
let source = std::fs::read_to_string(&file).unwrap();
let interface_offset = source.find("ExampleInterface").unwrap();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, interface_offset, Language::TypeScript).unwrap();
let ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 1, 1).unwrap();
assert!(
!ctx.before.is_empty(),
"Should have context before expanded interface"
);
let expanded = std::fs::read_to_string(&file).unwrap();
let expanded_content = &expanded[expanded_start..expanded_end];
assert!(
expanded_content.contains("name: string"),
"Should include interface properties"
);
assert!(
expanded_content.contains("value: number"),
"Should include interface properties"
);
}
#[test]
fn test_context_with_expand_javascript() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.js", JAVASCRIPT_CLASS_WITH_CONTEXT);
let source = std::fs::read_to_string(&file).unwrap();
let class_offset = source.find("JSClass").unwrap();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, class_offset, Language::JavaScript).unwrap();
let ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 1, 1).unwrap();
assert!(
ctx.before
.iter()
.any(|line| line.contains("Leading comment")),
"Context should include lines before expanded class"
);
let expanded = std::fs::read_to_string(&file).unwrap();
let expanded_content = &expanded[expanded_start..expanded_end];
assert!(
expanded_content.contains("constructor"),
"Should include constructor"
);
assert!(
expanded_content.contains("getValue"),
"Should include methods"
);
}
#[test]
fn test_context_with_expand_cpp() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.cpp", CPP_CLASS_WITH_CONTEXT);
let source = std::fs::read_to_string(&file).unwrap();
let class_offset = source.find("CppClass").unwrap();
let (expanded_start, expanded_end) =
match expand_to_body_with_docs(&file, class_offset, Language::Cpp) {
Ok(span) => span,
Err(_) => {
(class_offset, class_offset + "CppClass".len())
}
};
let ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 1, 1).unwrap();
assert!(
ctx.before.len() > 0 || ctx.after.len() > 0,
"Should have some context"
);
}
#[test]
fn test_context_with_expand_java() {
use splice::context::extract_context_asymmetric;
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let file = create_test_file(&dir, "test.java", JAVA_CLASS_WITH_CONTEXT);
let source = std::fs::read_to_string(&file).unwrap();
let class_offset = source.find("JavaClass").unwrap();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, class_offset, Language::Java).unwrap();
let ctx = extract_context_asymmetric(&file, expanded_start, expanded_end, 1, 1).unwrap();
assert!(
!ctx.before.is_empty(),
"Should have context before expanded class"
);
let expanded = std::fs::read_to_string(&file).unwrap();
let expanded_content = &expanded[expanded_start..expanded_end];
assert!(
expanded_content.contains("getValue"),
"Should include all methods"
);
}
#[test]
fn test_expansion_larger_than_replaced_span() {
use splice::expand::expand_to_body_with_docs;
use splice::symbol::Language;
let dir = TempDir::new().unwrap();
let content = r#"
fn test_name() -> i32 {
42
}
"#;
let file = create_test_file(&dir, "test.rs", content);
let source = std::fs::read_to_string(&file).unwrap();
let name_offset = source.find("test_name").unwrap();
let name_end = name_offset + "test_name".len();
let (expanded_start, expanded_end) =
expand_to_body_with_docs(&file, name_offset, Language::Rust).unwrap();
assert!(
expanded_start < name_offset,
"Expanded start should be before name start"
);
assert!(
expanded_end > name_end,
"Expanded end should be after name end"
);
let replaced_size = name_end - name_offset;
let expanded_size = expanded_end - expanded_start;
assert!(
expanded_size > replaced_size,
"Expanded span should be larger than replaced name span"
);
}