use splice::checksum::{checksum_file, checksum_span};
use splice::context::extract_context;
use splice::error_codes::{ErrorCode, SpliceErrorCode};
use splice::ingest::{detect_language, detect_semantic_kind, Language};
use splice::output::SpanResult;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_rich_span_complete() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "/// Documentation").unwrap();
writeln!(file, "fn greet(name: &str) -> String {{").unwrap();
writeln!(file, " format!(\"Hello, {{}}\", name)").unwrap();
writeln!(file, "}}").unwrap();
let file_path = file.path();
let byte_start = 23; let byte_end = 65;
let mut span = SpanResult::from_byte_span(
file_path.to_string_lossy().to_string(),
byte_start,
byte_end,
);
let context = extract_context(file_path, byte_start, byte_end, 1).unwrap();
span = span.with_context(context);
span = span.with_semantic_info("function", "rust");
let span_checksum = checksum_span(file_path, byte_start, byte_end).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
span = span.with_both_checksums(span_checksum.as_hex(), file_checksum.as_hex());
let error_code = ErrorCode::new(
"SPL-E001",
"error",
format!("{}:2:1", file_path.display()),
"Example hint",
);
span = span.with_error_code(error_code);
assert!(span.context.is_some());
assert_eq!(
span.semantics.as_ref().map(|s| s.kind.clone()),
Some("function".to_string())
);
assert_eq!(
span.semantics.as_ref().map(|s| s.language.clone()),
Some("rust".to_string())
);
assert!(span
.checksums
.as_ref()
.and_then(|c| c.checksum_before.clone())
.is_some());
assert!(span
.checksums
.as_ref()
.and_then(|c| c.file_checksum_before.clone())
.is_some());
assert!(span.error_code.is_some());
}
#[test]
fn test_backward_compatibility_old_json() {
let old_json = r#"{
"file_path": "src/main.rs",
"symbol": null,
"kind": null,
"byte_start": 0,
"byte_end": 10,
"line_start": 0,
"line_end": 0,
"col_start": 0,
"col_end": 0,
"span_id": "test-id",
"match_id": null,
"before_hash": null,
"after_hash": null,
"span_checksum_before": null,
"span_checksum_after": null
}"#;
let parsed = serde_json::from_str::<SpanResult>(old_json);
assert!(parsed.is_err(), "old schema should not deserialize");
}
#[test]
fn test_new_json_includes_rich_fields() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "line 1").unwrap();
writeln!(file, "line 2").unwrap();
let file_path = file.path();
let mut span = SpanResult::from_byte_span(file_path.to_string_lossy().to_string(), 0, 14);
let context = extract_context(file_path, 0, 14, 1).unwrap();
span = span.with_context(context);
span = span.with_semantic_info("function", "rust");
span = span.with_checksum_before("abc123");
span = span.with_file_checksum_before("def456");
let error_code = ErrorCode::new("SPL-E001", "error", "test.rs:1:1", "hint");
span = span.with_error_code(error_code);
let json = serde_json::to_string_pretty(&span).unwrap();
assert!(json.contains("\"context\""));
assert!(json.contains("\"semantics\""));
assert!(json.contains("\"checksums\""));
assert!(json.contains("\"checksum_before\""));
assert!(json.contains("\"file_checksum_before\""));
assert!(json.contains("\"error_code\""));
}
#[test]
fn test_none_fields_omitted_from_json() {
let span = SpanResult::from_byte_span("test.rs".to_string(), 0, 10);
let json = serde_json::to_string(&span).unwrap();
assert!(!json.contains("\"context\""));
assert!(!json.contains("\"semantics\""));
assert!(!json.contains("\"checksums\""));
assert!(!json.contains("\"checksum_before\""));
assert!(!json.contains("\"file_checksum_before\""));
assert!(!json.contains("\"error_code\""));
}
#[test]
fn test_context_extraction_with_span() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "// before").unwrap();
writeln!(file, "fn foo() {{").unwrap();
writeln!(file, "}}").unwrap();
writeln!(file, "// after").unwrap();
let context = extract_context(file.path(), 12, 22, 1).unwrap();
assert_eq!(context.before.len(), 1);
assert!(context.before[0].contains("before"));
assert_eq!(context.selected.len(), 2);
assert_eq!(context.after.len(), 1);
assert!(context.after[0].contains("after"));
}
#[test]
fn test_semantic_kind_detection() {
let kind = detect_semantic_kind("function_item", Language::Rust);
assert_eq!(kind.as_str(), "function");
let kind = detect_semantic_kind("class_definition", Language::Python);
assert_eq!(kind.as_str(), "type");
let kind = detect_semantic_kind("interface_declaration", Language::TypeScript);
assert_eq!(kind.as_str(), "trait");
}
#[test]
fn test_language_detection_with_span() {
let file_paths = vec![
("main.rs", "rust"),
("script.py", "python"),
("code.c", "c"),
("class.cpp", "cpp"),
("Main.java", "java"),
("app.js", "javascript"),
("lib.ts", "typescript"),
];
for (file_name, expected_lang) in file_paths {
let mut span = SpanResult::from_byte_span(file_name.to_string(), 0, 10);
let path = std::path::Path::new(file_name);
if let Some(detected) = detect_language(path) {
span = span.with_language(detected.as_str());
assert_eq!(
span.semantics.as_ref().map(|s| s.language.clone()),
Some(expected_lang.to_string()),
"Failed for {}",
file_name
);
}
}
}
#[test]
fn test_checksum_with_span() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "content").unwrap();
let byte_start = 0;
let byte_end = 7;
let span_checksum = checksum_span(file.path(), byte_start, byte_end).unwrap();
let file_checksum = checksum_file(file.path()).unwrap();
let span = SpanResult::from_byte_span(
file.path().to_string_lossy().to_string(),
byte_start,
byte_end,
)
.with_both_checksums(span_checksum.as_hex(), file_checksum.as_hex());
assert_eq!(
span.checksums
.as_ref()
.and_then(|c| c.checksum_before.clone()),
Some(format!("sha256:{}", span_checksum.as_hex()))
);
assert_eq!(
span.checksums
.as_ref()
.and_then(|c| c.file_checksum_before.clone()),
Some(format!("sha256:{}", file_checksum.as_hex()))
);
}
#[test]
fn test_error_code_with_span() {
let error_code = ErrorCode::from_splice_code(
SpliceErrorCode::SymbolNotFound,
Some("src/main.rs"),
Some(42),
Some(10),
);
let span =
SpanResult::from_byte_span("src/main.rs".to_string(), 0, 10).with_error_code(error_code);
assert!(span.error_code.is_some());
let ec = span.error_code.as_ref().unwrap();
assert_eq!(ec.code, "SPL-E001");
assert_eq!(ec.severity, "error");
assert_eq!(ec.location, "src/main.rs:42:10");
assert!(!ec.hint.is_empty());
}
#[test]
fn test_full_span_serialization() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "fn test() {{}}").unwrap();
let file_path = file.path();
let mut span = SpanResult::from_byte_span(file_path.to_string_lossy().to_string(), 0, 13);
let context = extract_context(file_path, 0, 13, 0).unwrap();
span = span.with_context(context);
span = span.with_semantic_info("function", "rust");
span = span.with_checksum_before("span_hash");
span = span.with_file_checksum_before("file_hash");
let ec = ErrorCode::new("SPL-E001", "error", "test:1:1", "hint");
span = span.with_error_code(ec);
let json = serde_json::to_string(&span).unwrap();
let deserialized: SpanResult = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.file_path, span.file_path);
assert!(deserialized.context.is_some());
assert_eq!(
deserialized.semantics.as_ref().map(|s| s.kind.clone()),
span.semantics.as_ref().map(|s| s.kind.clone())
);
assert_eq!(
deserialized.semantics.as_ref().map(|s| s.language.clone()),
span.semantics.as_ref().map(|s| s.language.clone())
);
assert_eq!(
deserialized
.checksums
.as_ref()
.and_then(|c| c.checksum_before.clone()),
span.checksums
.as_ref()
.and_then(|c| c.checksum_before.clone())
);
assert_eq!(
deserialized
.checksums
.as_ref()
.and_then(|c| c.file_checksum_before.clone()),
span.checksums
.as_ref()
.and_then(|c| c.file_checksum_before.clone())
);
assert!(deserialized.error_code.is_some());
}
#[test]
fn test_context_utf8_multibyte() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "// 🦀 before").unwrap();
writeln!(file, "fn foo() {{}}").unwrap();
writeln!(file, "// 🚀 after").unwrap();
let context = extract_context(file.path(), 15, 25, 1).unwrap();
assert!(context.before[0].contains("🦀") || context.before[0].contains("before"));
assert!(context.after[0].contains("🚀") || context.after[0].contains("after"));
}
#[test]
fn test_context_at_file_boundaries() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "fn foo() {{}}").unwrap();
let context = extract_context(file.path(), 0, 11, 5).unwrap();
assert_eq!(context.before.len(), 0);
assert_eq!(context.selected.len(), 1);
let context = extract_context(file.path(), 10, 11, 5).unwrap();
assert_eq!(context.after.len(), 0);
}
#[test]
fn test_semantic_kind_all_languages() {
let tests = vec![
("function_item", Language::Rust, "function"),
("struct_item", Language::Rust, "type"),
("function_definition", Language::Python, "function"),
("class_definition", Language::Python, "type"),
("method_declaration", Language::Java, "function"),
("class_declaration", Language::Java, "type"),
("function_definition", Language::C, "function"),
("struct_specifier", Language::C, "type"),
("function_definition", Language::Cpp, "function"),
("class_specifier", Language::Cpp, "type"),
("function_declaration", Language::JavaScript, "function"),
("class_declaration", Language::JavaScript, "type"),
("function_declaration", Language::TypeScript, "function"),
("interface_declaration", Language::TypeScript, "trait"),
];
for (node_type, lang, expected) in tests {
let kind = detect_semantic_kind(node_type, lang);
assert_eq!(
kind.as_str(),
expected,
"Failed for {} {:?}",
node_type,
lang
);
}
}