use serde_json::json;
use splice::checksum::{checksum_file, checksum_span};
use splice::context::{extract_context, extract_context_asymmetric};
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;
fn span_kind(span: &SpanResult) -> Option<String> {
span.semantics.as_ref().map(|s| s.kind.clone())
}
fn span_language(span: &SpanResult) -> Option<String> {
span.semantics.as_ref().map(|s| s.language.clone())
}
fn span_checksum_before(span: &SpanResult) -> Option<String> {
span.checksums
.as_ref()
.and_then(|c| c.checksum_before.clone())
}
fn span_file_checksum_before(span: &SpanResult) -> Option<String> {
span.checksums
.as_ref()
.and_then(|c| c.file_checksum_before.clone())
}
const RUST_FUNCTION: &str = r#"
/// Adds two numbers together.
///
/// # Examples
/// ```
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Multiplies two numbers
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
"#;
const PYTHON_FUNCTION: &str = r#"
"""Adds two numbers.
This function performs addition of two integers.
Args:
a: First integer
b: Second integer
Returns:
The sum of a and b
"""
def add(a: int, b: int) -> int:
"""Add two integers together."""
return a + b
def multiply(a: int, b: int) -> int:
"""Multiply two integers."""
return a * b
"#;
const C_FUNCTION: &str = r#"
/* Adds two numbers.
*
* This function performs addition of two integers.
*
* @param a First integer
* @param b Second integer
* @return The sum of a and b
*/
int add(int a, int b) {
return a + b;
}
/* Multiplies two numbers */
int multiply(int a, int b) {
return a * b;
}
"#;
const CPP_FUNCTION: &str = r#"
// Adds two numbers.
// This function performs addition of two integers.
//
// @param a First integer
// @param b Second integer
// @return The sum of a and b
int add(int a, int b) {
return a + b;
}
// Multiplies two numbers
int multiply(int a, int b) {
return a * b;
}
"#;
const JAVA_CLASS: &str = r#"
/**
* A simple calculator class.
*
* This class provides basic arithmetic operations.
*/
public class Calculator {
/**
* Adds two numbers.
*
* @param a First number
* @param b Second number
* @return The sum
*/
public int add(int a, int b) {
return a + b;
}
/**
* Multiplies two numbers.
*/
public int multiply(int a, int b) {
return a * b;
}
}
"#;
const JAVASCRIPT_FUNCTION: &str = r#"
/**
* Adds two numbers.
*
* @param {number} a - First number
* @param {number} b - Second number
* @returns {number} The sum
*/
function add(a, b) {
return a + b;
}
/**
* Multiplies two numbers.
*
* @param {number} a - First number
* @param {number} b - Second number
* @returns {number} The product
*/
const multiply = (a, b) => a * b;
// Simple function
function subtract(a, b) {
return a - b;
}
"#;
const TYPESCRIPT_FUNCTION: &str = r#"
/**
* Adds two numbers.
*
* @param a - First number
* @param b - Second number
* @returns The sum
*/
function add(a: number, b: number): number {
return a + b;
}
/**
* Multiplies two numbers.
*/
const multiply = (a: number, b: number): number => {
return a * b;
};
// TypeScript interface
interface Calculator {
add(a: number, b: number): number;
multiply(a: number, b: number): number;
}
// TypeScript class implementing interface
class BasicCalculator implements Calculator {
add(a: number, b: number): number {
return a + b;
}
multiply(a: number, b: number): number {
return a * b;
}
}
"#;
#[test]
fn test_language_detection_all_languages() {
let test_cases = vec![
("test.rs", Language::Rust),
("lib.rs", Language::Rust),
("main.rs", Language::Rust),
("test.py", Language::Python),
("script.py", Language::Python),
("app.py", Language::Python),
("test.c", Language::C),
("header.h", Language::C),
("main.c", Language::C),
("test.cpp", Language::Cpp),
("header.hpp", Language::Cpp),
("lib.cc", Language::Cpp),
("test.java", Language::Java),
("Main.java", Language::Java),
("test.js", Language::JavaScript),
("app.mjs", Language::JavaScript),
("module.cjs", Language::JavaScript),
("test.ts", Language::TypeScript),
("component.tsx", Language::TypeScript),
];
for (filename, expected_lang) in test_cases {
let detected = detect_language(std::path::Path::new(filename));
assert_eq!(
detected,
Some(expected_lang),
"Language detection failed for {}: got {:?}, expected {:?}",
filename,
detected,
expected_lang
);
}
}
#[test]
fn test_semantic_kind_detection_rust() {
let test_cases = vec![
("function_item", "function"),
("struct_item", "type"),
("enum_item", "type"),
("trait_item", "trait"),
("impl_item", "trait"),
("static_item", "constant"),
("const_item", "constant"),
("type_item", "type_alias"),
("mod_item", "module"),
];
for (node_kind, expected_semantic_kind) in test_cases {
let detected = detect_semantic_kind(node_kind, Language::Rust);
assert_eq!(
detected.as_str(),
expected_semantic_kind,
"Semantic kind mismatch for Rust node kind '{}': got '{}', expected '{}'",
node_kind,
detected.as_str(),
expected_semantic_kind
);
}
}
#[test]
fn test_semantic_kind_detection_python() {
let test_cases = vec![
("function_definition", "function"),
("class_definition", "type"),
("assignment", "variable"),
("annotated_assignment", "variable"),
("type_alias_statement", "type_alias"),
("import_statement", "module"),
("import_from_statement", "module"),
];
for (node_kind, expected_semantic_kind) in test_cases {
let detected = detect_semantic_kind(node_kind, Language::Python);
assert_eq!(
detected.as_str(),
expected_semantic_kind,
"Semantic kind mismatch for Python node kind '{}': got '{}', expected '{}'",
node_kind,
detected.as_str(),
expected_semantic_kind
);
}
}
#[test]
fn test_semantic_kind_detection_javascript() {
let test_cases = vec![
("function_declaration", "function"),
("function_expression", "function"),
("arrow_function", "function"),
("class_declaration", "type"),
("class_expression", "type"),
("variable_declaration", "variable"),
("lexical_declaration", "variable"),
("enum_declaration", "enum"),
("import_statement", "module"),
("export_statement", "module"),
];
for (node_kind, expected_semantic_kind) in test_cases {
let detected = detect_semantic_kind(node_kind, Language::JavaScript);
assert_eq!(
detected.as_str(),
expected_semantic_kind,
"Semantic kind mismatch for JavaScript node kind '{}': got '{}', expected '{}'",
node_kind,
detected.as_str(),
expected_semantic_kind
);
}
}
#[test]
fn test_semantic_kind_detection_typescript() {
let test_cases = vec![
("function_declaration", "function"),
("arrow_function", "function"),
("function_expression", "function"),
("class_declaration", "type"),
("class_expression", "type"),
("interface_declaration", "trait"),
("type_alias_declaration", "type_alias"),
("variable_declaration", "variable"),
("lexical_declaration", "variable"),
("enum_declaration", "enum"),
("method_definition", "function"),
("namespace_declaration", "module"),
("constructor_parameters", "constructor"),
];
for (node_kind, expected_semantic_kind) in test_cases {
let detected = detect_semantic_kind(node_kind, Language::TypeScript);
assert_eq!(
detected.as_str(),
expected_semantic_kind,
"Semantic kind mismatch for TypeScript node kind '{}': got '{}', expected '{}'",
node_kind,
detected.as_str(),
expected_semantic_kind
);
}
}
#[test]
fn test_semantic_kind_detection_java() {
let test_cases = vec![
("method_declaration", "function"),
("constructor_declaration", "constructor"),
("class_declaration", "type"),
("interface_declaration", "type"),
("enum_declaration", "enum"),
("field_declaration", "variable"),
];
for (node_kind, expected_semantic_kind) in test_cases {
let detected = detect_semantic_kind(node_kind, Language::Java);
assert_eq!(
detected.as_str(),
expected_semantic_kind,
"Semantic kind mismatch for Java node kind '{}': got '{}', expected '{}'",
node_kind,
detected.as_str(),
expected_semantic_kind
);
}
}
#[test]
fn test_semantic_kind_detection_c() {
let test_cases = vec![
("function_definition", "function"),
("declaration", "variable"),
("struct_specifier", "type"),
("enum_specifier", "enum"),
("type_definition", "type_alias"),
];
for (node_kind, expected_semantic_kind) in test_cases {
let detected = detect_semantic_kind(node_kind, Language::C);
assert_eq!(
detected.as_str(),
expected_semantic_kind,
"Semantic kind mismatch for C node kind '{}': got '{}', expected '{}'",
node_kind,
detected.as_str(),
expected_semantic_kind
);
}
}
#[test]
fn test_semantic_kind_detection_cpp() {
let test_cases = vec![
("function_definition", "function"),
("declaration", "variable"),
("class_specifier", "type"),
("struct_specifier", "type"),
("enum_specifier", "enum"),
("type_definition", "type_alias"),
("constructor_definition", "constructor"),
];
for (node_kind, expected_semantic_kind) in test_cases {
let detected = detect_semantic_kind(node_kind, Language::Cpp);
assert_eq!(
detected.as_str(),
expected_semantic_kind,
"Semantic kind mismatch for C++ node kind '{}': got '{}', expected '{}'",
node_kind,
detected.as_str(),
expected_semantic_kind
);
}
}
#[test]
fn test_context_extraction_rust() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(RUST_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("pub fn add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context(file_path, offset, offset_end, 2).unwrap();
assert!(!ctx.selected.is_empty(), "Selected should not be empty");
let has_content = ctx.before.iter().any(|l| l.contains("///"))
|| ctx.selected.iter().any(|l| l.contains("add"))
|| ctx.after.iter().any(|l| l.contains("multiply"));
assert!(has_content, "Context should contain meaningful content");
}
#[test]
fn test_context_extraction_python() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(PYTHON_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("def add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context(file_path, offset, offset_end, 2).unwrap();
assert!(!ctx.selected.is_empty(), "Selected should not be empty");
let has_content = ctx.before.iter().any(|l| l.contains("Adds"))
|| ctx.selected.iter().any(|l| l.contains("def add"))
|| ctx.after.iter().any(|l| l.contains("multiply"));
assert!(has_content, "Context should contain meaningful content");
}
#[test]
fn test_context_extraction_c() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(C_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("int add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context(file_path, offset, offset_end, 1).unwrap();
assert!(!ctx.selected.is_empty(), "Selected should not be empty");
let has_content = ctx.before.iter().any(|l| l.contains("/*"))
|| ctx.selected.iter().any(|l| l.contains("int add"));
assert!(has_content, "Context should contain meaningful content");
}
#[test]
fn test_context_extraction_cpp() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(CPP_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("int add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context(file_path, offset, offset_end, 1).unwrap();
assert!(!ctx.selected.is_empty(), "Selected should not be empty");
let has_content = ctx.before.iter().any(|l| l.contains("//"))
|| ctx.selected.iter().any(|l| l.contains("int add"));
assert!(has_content, "Context should contain meaningful content");
}
#[test]
fn test_context_extraction_java() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(JAVA_CLASS.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("public int add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context(file_path, offset, offset_end, 2).unwrap();
assert!(!ctx.selected.is_empty(), "Selected should not be empty");
let has_content = ctx.before.iter().any(|l| l.contains("*"))
|| ctx.selected.iter().any(|l| l.contains("public int add"));
assert!(has_content, "Context should contain meaningful content");
}
#[test]
fn test_context_extraction_javascript() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(JAVASCRIPT_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("function add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context(file_path, offset, offset_end, 1).unwrap();
assert!(!ctx.selected.is_empty(), "Selected should not be empty");
let has_content = ctx.before.iter().any(|l| l.contains("*"))
|| ctx.selected.iter().any(|l| l.contains("function add"));
assert!(has_content, "Context should contain meaningful content");
}
#[test]
fn test_context_extraction_typescript() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(TYPESCRIPT_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("function add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context(file_path, offset, offset_end, 1).unwrap();
assert!(!ctx.selected.is_empty(), "Selected should not be empty");
let has_content = ctx.before.iter().any(|l| l.contains("*"))
|| ctx.selected.iter().any(|l| l.contains("function add"));
assert!(has_content, "Context should contain meaningful content");
}
#[test]
fn test_checksum_calculation_rust() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(RUST_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let byte_start = 10;
let byte_end = 100;
let span_checksum = checksum_span(file_path, byte_start, byte_end).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
assert_eq!(
span_checksum.as_hex().len(),
64,
"Span checksum should be 64 hex chars for Rust"
);
assert_eq!(
file_checksum.as_hex().len(),
64,
"File checksum should be 64 hex chars for Rust"
);
let span_checksum2 = checksum_span(file_path, byte_start, byte_end).unwrap();
assert_eq!(
span_checksum.as_hex(),
span_checksum2.as_hex(),
"Checksums should be deterministic for Rust"
);
}
#[test]
fn test_checksum_calculation_python() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(PYTHON_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let byte_start = 10;
let byte_end = 100;
let span_checksum = checksum_span(file_path, byte_start, byte_end).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
assert_eq!(span_checksum.as_hex().len(), 64);
assert_eq!(file_checksum.as_hex().len(), 64);
let span_checksum2 = checksum_span(file_path, 50, 150).unwrap();
assert_ne!(
span_checksum.as_hex(),
span_checksum2.as_hex(),
"Different spans should have different checksums"
);
}
#[test]
fn test_checksum_calculation_c() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(C_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let span_checksum = checksum_span(file_path, 20, 100).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
assert_eq!(span_checksum.as_hex().len(), 64);
assert_eq!(file_checksum.as_hex().len(), 64);
}
#[test]
fn test_checksum_calculation_cpp() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(CPP_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let span_checksum = checksum_span(file_path, 20, 100).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
assert_eq!(span_checksum.as_hex().len(), 64);
assert_eq!(file_checksum.as_hex().len(), 64);
}
#[test]
fn test_checksum_calculation_java() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(JAVA_CLASS.as_bytes()).unwrap();
let file_path = file.path();
let span_checksum = checksum_span(file_path, 20, 100).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
assert_eq!(span_checksum.as_hex().len(), 64);
assert_eq!(file_checksum.as_hex().len(), 64);
}
#[test]
fn test_checksum_calculation_javascript() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(JAVASCRIPT_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let span_checksum = checksum_span(file_path, 20, 100).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
assert_eq!(span_checksum.as_hex().len(), 64);
assert_eq!(file_checksum.as_hex().len(), 64);
}
#[test]
fn test_checksum_calculation_typescript() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(TYPESCRIPT_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let span_checksum = checksum_span(file_path, 20, 100).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
assert_eq!(span_checksum.as_hex().len(), 64);
assert_eq!(file_checksum.as_hex().len(), 64);
}
#[test]
fn test_error_code_formatting_all() {
let test_cases = vec![
(
"SPL-E001",
"error",
"/path/to/file.rs:10:5",
"Symbol not found",
),
(
"SPL-E002",
"warning",
"/path/to/file.py:20:10",
"Ambiguous symbol",
),
(
"SPL-E003",
"error",
"/path/to/file.ts:30:15",
"Reference failed",
),
("SPL-E011", "error", "file.c:1:1", "Parse error"),
("SPL-E021", "error", "main.cpp:5:3", "Invalid span"),
(
"SPL-E043",
"error",
"Calculator.java:10:1",
"Compiler validation failed",
),
(
"SPL-W001",
"warning",
"app.js:42:7",
"Ambiguous symbol as warning",
),
];
for (code, severity, location, hint) in test_cases {
let error_code = ErrorCode::new(code, severity, location.to_string(), hint.to_string());
assert_eq!(error_code.code, code);
assert_eq!(error_code.severity, severity);
assert_eq!(error_code.location, location);
assert_eq!(error_code.hint, hint);
}
}
#[test]
fn test_error_code_from_splice_code_all_severities() {
let error_code = ErrorCode::from_splice_code(
SpliceErrorCode::SymbolNotFound,
Some("src/main.rs"),
Some(42),
Some(10),
);
assert_eq!(error_code.code, "SPL-E001");
assert_eq!(error_code.severity, "error");
assert_eq!(error_code.location, "src/main.rs:42:10");
let warning_code = ErrorCode::from_splice_code(
SpliceErrorCode::AmbiguousSymbol,
Some("test.py"),
Some(10),
Some(5),
);
assert_eq!(warning_code.code, "SPL-E002");
assert_eq!(warning_code.severity, "warning");
assert_eq!(warning_code.location, "test.py:10:5");
let parse_error = ErrorCode::from_splice_code(
SpliceErrorCode::ParseError,
Some("file.ts"),
Some(1),
Some(1),
);
assert_eq!(parse_error.code, "SPL-E011");
assert_eq!(parse_error.severity, "error");
}
#[test]
fn test_rich_span_json_serialization_rust() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(RUST_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let byte_start = 10;
let byte_end = 100;
let mut span = SpanResult::from_byte_span(
file_path.to_string_lossy().to_string(),
byte_start,
byte_end,
);
let ctx = extract_context(file_path, byte_start, byte_end, 1).unwrap();
span = span.with_context(ctx);
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!("{}.rs:1:10", file_path.display()),
"Example hint",
);
span = span.with_error_code(error_code);
let json_value = serde_json::to_value(&span).unwrap();
assert!(json_value["context"].is_object());
assert_eq!(json_value["semantics"]["kind"], "function");
assert_eq!(json_value["semantics"]["language"], "rust");
assert!(json_value["checksums"]["checksum_before"].is_string());
assert!(json_value["checksums"]["file_checksum_before"].is_string());
assert!(json_value["error_code"].is_object());
assert_eq!(json_value["error_code"]["code"], "SPL-E001");
}
#[test]
fn test_rich_span_json_serialization_python() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(PYTHON_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let byte_start = 10;
let byte_end = 100;
let mut span = SpanResult::from_byte_span(
file_path.to_string_lossy().to_string(),
byte_start,
byte_end,
);
let ctx = extract_context(file_path, byte_start, byte_end, 1).unwrap();
span = span.with_context(ctx);
span = span.with_semantic_info("function", "python");
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 json_value = serde_json::to_value(&span).unwrap();
assert_eq!(json_value["semantics"]["kind"], "function");
assert_eq!(json_value["semantics"]["language"], "python");
assert!(json_value["checksums"]["checksum_before"].is_string());
assert!(json_value["checksums"]["file_checksum_before"].is_string());
}
#[test]
fn test_rich_span_json_serialization_typescript() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(TYPESCRIPT_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let byte_start = 10;
let byte_end = 100;
let mut span = SpanResult::from_byte_span(
file_path.to_string_lossy().to_string(),
byte_start,
byte_end,
);
let ctx = extract_context(file_path, byte_start, byte_end, 1).unwrap();
span = span.with_context(ctx);
span = span.with_semantic_info("function", "typescript");
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 json_value = serde_json::to_value(&span).unwrap();
assert_eq!(json_value["semantics"]["kind"], "function");
assert_eq!(json_value["semantics"]["language"], "typescript");
assert!(json_value["checksums"]["checksum_before"].is_string());
}
#[test]
fn test_backward_compatibility_with_new_fields() {
let old_json = json!({
"file_path": "src/lib.rs",
"symbol": null,
"kind": null,
"byte_start": 0,
"byte_end": 100,
"line_start": 1,
"line_end": 5,
"col_start": 0,
"col_end": 10,
"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_value::<SpanResult>(old_json);
assert!(parsed.is_err(), "old schema should not deserialize");
}
#[test]
fn test_context_asymmetric_rust() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(RUST_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("pub fn add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context_asymmetric(file_path, offset, offset_end, 3, 1).unwrap();
assert!(ctx.before.len() <= 3, "Should have at most 3 lines before");
assert!(ctx.after.len() <= 1, "Should have at most 1 line after");
}
#[test]
fn test_context_asymmetric_python() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(PYTHON_FUNCTION.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("def add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context_asymmetric(file_path, offset, offset_end, 2, 2).unwrap();
assert!(ctx.before.len() <= 2, "Should have at most 2 lines before");
assert!(ctx.after.len() <= 2, "Should have at most 2 lines after");
}
#[test]
fn test_context_asymmetric_java() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(JAVA_CLASS.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find("public int add").unwrap();
let offset_end = offset + 50;
let ctx = extract_context_asymmetric(file_path, offset, offset_end, 1, 3).unwrap();
assert!(ctx.before.len() <= 1, "Should have at most 1 line before");
assert!(ctx.after.len() <= 3, "Should have at most 3 lines after");
}
#[test]
fn test_semantic_kind_function_all_languages() {
let test_cases = vec![
("function_item", Language::Rust),
("function_definition", Language::Python),
("function_definition", Language::C),
("function_definition", Language::Cpp),
("method_declaration", Language::Java),
("function_declaration", Language::JavaScript),
("function_declaration", Language::TypeScript),
];
for (node_kind, lang) in test_cases {
let detected = detect_semantic_kind(node_kind, lang);
assert_eq!(
detected.as_str(),
"function",
"Function kind mismatch for {:?} node '{}'",
lang,
node_kind
);
}
}
#[test]
fn test_semantic_kind_type_all_languages() {
let test_cases = vec![
("struct_item", Language::Rust),
("class_definition", Language::Python),
("struct_specifier", Language::C),
("class_specifier", Language::Cpp),
("class_declaration", Language::Java),
("class_declaration", Language::JavaScript),
("class_declaration", Language::TypeScript),
];
for (node_kind, lang) in test_cases {
let detected = detect_semantic_kind(node_kind, lang);
assert_eq!(
detected.as_str(),
"type",
"Type kind mismatch for {:?} node '{}'",
lang,
node_kind
);
}
}
#[test]
fn test_semantic_kind_variable_all_languages() {
let test_cases = vec![
("const_item", Language::Rust, "constant"),
("assignment", Language::Python, "variable"),
("declaration", Language::C, "variable"),
("declaration", Language::Cpp, "variable"),
("field_declaration", Language::Java, "variable"),
("variable_declaration", Language::JavaScript, "variable"),
("variable_declaration", Language::TypeScript, "variable"),
];
for (node_kind, lang, expected) in test_cases {
let detected = detect_semantic_kind(node_kind, lang);
assert_eq!(
detected.as_str(),
expected,
"Kind mismatch for {:?} node '{}': got '{}', expected '{}'",
lang,
node_kind,
detected.as_str(),
expected
);
}
}
#[test]
fn test_complete_rich_span_rust() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "/// Docs").unwrap();
writeln!(file, "fn test() {{}}").unwrap();
let file_path = file.path();
let content = std::fs::read_to_string(file_path).unwrap();
let file_size = content.len();
let byte_start = 10;
let byte_end = file_size.min(25);
let mut span = SpanResult::from_byte_span(
file_path.to_string_lossy().to_string(),
byte_start,
byte_end,
);
let ctx = extract_context(file_path, byte_start, byte_end, 0).unwrap();
span = span.with_context(ctx);
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 ec = ErrorCode::new("SPL-E001", "error", "test:1:1", "hint");
span = span.with_error_code(ec);
assert!(span.context.is_some());
assert_eq!(span_kind(&span), Some("function".to_string()));
assert_eq!(span_language(&span), Some("rust".to_string()));
assert!(span_checksum_before(&span).is_some());
assert!(span_file_checksum_before(&span).is_some());
assert!(span.error_code.is_some());
}
#[test]
fn test_complete_rich_span_python() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "\"\"\"Docs\"\"\"").unwrap();
writeln!(file, "def test():").unwrap();
writeln!(file, " pass").unwrap();
let file_path = file.path();
let mut span = SpanResult::from_byte_span(file_path.to_string_lossy().to_string(), 12, 30);
let ctx = extract_context(file_path, 12, 30, 0).unwrap();
span = span.with_context(ctx);
span = span.with_semantic_info("function", "python");
let span_checksum = checksum_span(file_path, 12, 30).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
span = span.with_both_checksums(span_checksum.as_hex(), file_checksum.as_hex());
assert!(span.context.is_some());
assert_eq!(span_kind(&span), Some("function".to_string()));
assert_eq!(span_language(&span), Some("python".to_string()));
assert!(span_checksum_before(&span).is_some());
assert!(span_file_checksum_before(&span).is_some());
}
#[test]
fn test_complete_rich_span_c() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "/* Docs */").unwrap();
writeln!(file, "int test() {{").unwrap();
writeln!(file, " return 0;").unwrap();
writeln!(file, "}}").unwrap();
let file_path = file.path();
let mut span = SpanResult::from_byte_span(file_path.to_string_lossy().to_string(), 12, 35);
let ctx = extract_context(file_path, 12, 35, 0).unwrap();
span = span.with_context(ctx);
span = span.with_semantic_info("function", "c");
let span_checksum = checksum_span(file_path, 12, 35).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
span = span.with_both_checksums(span_checksum.as_hex(), file_checksum.as_hex());
assert!(span.context.is_some());
assert_eq!(span_kind(&span), Some("function".to_string()));
assert_eq!(span_language(&span), Some("c".to_string()));
assert!(span_checksum_before(&span).is_some());
assert!(span_file_checksum_before(&span).is_some());
}
#[test]
fn test_complete_rich_span_cpp() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "// Docs").unwrap();
writeln!(file, "int test() {{").unwrap();
writeln!(file, " return 0;").unwrap();
writeln!(file, "}}").unwrap();
let file_path = file.path();
let mut span = SpanResult::from_byte_span(file_path.to_string_lossy().to_string(), 8, 35);
let ctx = extract_context(file_path, 8, 35, 0).unwrap();
span = span.with_context(ctx);
span = span.with_semantic_info("function", "cpp");
let span_checksum = checksum_span(file_path, 8, 35).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
span = span.with_both_checksums(span_checksum.as_hex(), file_checksum.as_hex());
assert!(span.context.is_some());
assert_eq!(span_kind(&span), Some("function".to_string()));
assert_eq!(span_language(&span), Some("cpp".to_string()));
}
#[test]
fn test_complete_rich_span_java() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "/** Docs */").unwrap();
writeln!(file, "int test() {{").unwrap();
writeln!(file, " return 0;").unwrap();
writeln!(file, "}}").unwrap();
let file_path = file.path();
let mut span = SpanResult::from_byte_span(file_path.to_string_lossy().to_string(), 14, 40);
let ctx = extract_context(file_path, 14, 40, 0).unwrap();
span = span.with_context(ctx);
span = span.with_semantic_info("function", "java");
let span_checksum = checksum_span(file_path, 14, 40).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
span = span.with_both_checksums(span_checksum.as_hex(), file_checksum.as_hex());
assert!(span.context.is_some());
assert_eq!(span_kind(&span), Some("function".to_string()));
assert_eq!(span_language(&span), Some("java".to_string()));
}
#[test]
fn test_complete_rich_span_javascript() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "/** Docs */").unwrap();
writeln!(file, "function test() {{").unwrap();
writeln!(file, " return 0;").unwrap();
writeln!(file, "}}").unwrap();
let file_path = file.path();
let mut span = SpanResult::from_byte_span(file_path.to_string_lossy().to_string(), 14, 45);
let ctx = extract_context(file_path, 14, 45, 0).unwrap();
span = span.with_context(ctx);
span = span.with_semantic_info("function", "javascript");
let span_checksum = checksum_span(file_path, 14, 45).unwrap();
let file_checksum = checksum_file(file_path).unwrap();
span = span.with_both_checksums(span_checksum.as_hex(), file_checksum.as_hex());
assert!(span.context.is_some());
assert_eq!(span_kind(&span), Some("function".to_string()));
assert_eq!(span_language(&span), Some("javascript".to_string()));
}
#[test]
fn test_complete_rich_span_typescript() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "/** Docs */").unwrap();
writeln!(file, "function test(): number {{").unwrap();
writeln!(file, " return 0;").unwrap();
writeln!(file, "}}").unwrap();
let file_path = file.path();
let content = std::fs::read_to_string(file_path).unwrap();
let file_size = content.len();
let byte_start = 14;
let byte_end = file_size.min(55);
let mut span = SpanResult::from_byte_span(
file_path.to_string_lossy().to_string(),
byte_start,
byte_end,
);
let ctx = extract_context(file_path, byte_start, byte_end, 0).unwrap();
span = span.with_context(ctx);
span = span.with_semantic_info("function", "typescript");
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());
assert!(span.context.is_some());
assert_eq!(span_kind(&span), Some("function".to_string()));
assert_eq!(span_language(&span), Some("typescript".to_string()));
}
#[test]
fn test_error_code_severity_levels() {
let error = ErrorCode::new("SPL-E001", "error", "file.rs:1:1", "Error hint");
assert_eq!(error.severity, "error");
let warning = ErrorCode::new("SPL-W001", "warning", "file.py:2:1", "Warning hint");
assert_eq!(warning.severity, "warning");
let note = ErrorCode::new("SPL-N001", "note", "file.ts:3:1", "Note hint");
assert_eq!(note.severity, "note");
}
#[test]
fn test_checksum_different_span_sizes_rust() {
let content = b"fn test() { return 42; }";
let mut file = NamedTempFile::new().unwrap();
file.write_all(content).unwrap();
let file_path = file.path();
let file_size = content.len();
let checksum1 = checksum_span(file_path, 0, 10).unwrap();
let checksum2 = checksum_span(file_path, 0, 15).unwrap();
let checksum3 = checksum_span(file_path, 0, file_size).unwrap();
assert_ne!(checksum1.as_hex(), checksum2.as_hex());
assert_ne!(checksum2.as_hex(), checksum3.as_hex());
assert_ne!(checksum1.as_hex(), checksum3.as_hex());
}
#[test]
fn test_context_at_file_boundaries_all_languages() {
let test_cases = vec![
("test.rs", "fn test() {}\n"),
("test.py", "def test(): pass\n"),
("test.c", "int test() { return 0; }\n"),
("test.cpp", "int test() { return 0; }\n"),
("test.java", "int test() { return 0; }\n"),
("test.js", "function test() { return 0; }\n"),
("test.ts", "function test(): number { return 0; }\n"),
];
for (filename, content) in test_cases {
let mut file = NamedTempFile::new().unwrap();
file.write_all(content.as_bytes()).unwrap();
let file_path = file.path();
let ctx = extract_context(file_path, 0, 5, 5).unwrap();
assert_eq!(
ctx.before.len(),
0,
"Should have 0 lines before for {}",
filename
);
let content_len = content.len();
let ctx = extract_context(file_path, content_len - 5, content_len, 5).unwrap();
assert_eq!(
ctx.after.len(),
0,
"Should have 0 lines after for {}",
filename
);
}
}
#[test]
fn test_language_detection_edge_cases() {
let test_cases = vec![
("lib.test.rs", Some(Language::Rust)),
("file.test.py", Some(Language::Python)),
("test.RS", None),
("test.PY", None),
("test.txt", None),
("test.md", None),
("Makefile", None),
("Dockerfile", None),
];
for (filename, expected) in test_cases {
let detected = detect_language(std::path::Path::new(filename));
assert_eq!(
detected, expected,
"Language detection edge case failed for {}: got {:?}, expected {:?}",
filename, detected, expected
);
}
}
#[test]
fn test_semantic_kind_unknown_fallback() {
let languages = vec![
Language::Rust,
Language::Python,
Language::C,
Language::Cpp,
Language::Java,
Language::JavaScript,
Language::TypeScript,
];
for lang in languages {
let detected = detect_semantic_kind("completely_unknown_node_type", lang);
assert_eq!(
detected.as_str(),
"unknown",
"Unknown node type should return 'unknown' for {:?}",
lang
);
}
}
#[test]
fn test_json_serialization_roundtrip_all_fields() {
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 ctx = extract_context(file_path, 0, 13, 0).unwrap();
span = span.with_context(ctx);
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!(span_kind(&deserialized), span_kind(&span));
assert_eq!(span_language(&deserialized), span_language(&span));
assert_eq!(
span_checksum_before(&deserialized),
span_checksum_before(&span)
);
assert_eq!(
span_file_checksum_before(&deserialized),
span_file_checksum_before(&span)
);
assert!(deserialized.error_code.is_some());
}
#[test]
fn test_checksum_algorithm_consistency() {
let test_cases = vec![
("test.rs", RUST_FUNCTION),
("test.py", PYTHON_FUNCTION),
("test.c", C_FUNCTION),
("test.cpp", CPP_FUNCTION),
("test.java", JAVA_CLASS),
("test.js", JAVASCRIPT_FUNCTION),
("test.ts", TYPESCRIPT_FUNCTION),
];
for (filename, content) in test_cases {
let mut file = NamedTempFile::new().unwrap();
file.write_all(content.as_bytes()).unwrap();
let file_path = file.path();
let checksum1 = checksum_file(file_path).unwrap();
let checksum2 = checksum_file(file_path).unwrap();
assert_eq!(
checksum1.as_hex(),
checksum2.as_hex(),
"Checksum should be consistent for {}",
filename
);
assert_eq!(
checksum1.algorithm,
splice::checksum::ChecksumAlgorithm::Sha256
);
}
}
#[test]
fn test_context_utf8_multibyte_all_languages() {
let test_cases = vec![
("test.rs", "// 🦀 Rust\nfn test() {}\n"),
("test.py", "# 🐍 Python\ndef test(): pass\n"),
("test.c", "// 🦀 C\nint test() { return 0; }\n"),
("test.cpp", "// 🦀 C++\nint test() { return 0; }\n"),
("test.java", "// ☕ Java\nint test() { return 0; }\n"),
(
"test.js",
"// 📜 JavaScript\nfunction test() { return 0; }\n",
),
(
"test.ts",
"// 📘 TypeScript\nfunction test(): number { return 0; }\n",
),
];
for (_filename, content) in test_cases {
let mut file = NamedTempFile::new().unwrap();
file.write_all(content.as_bytes()).unwrap();
let file_path = file.path();
let contents = std::fs::read(file_path).unwrap();
let newline_offset = contents.iter().position(|&b| b == b'\n').unwrap();
let ctx = extract_context(file_path, newline_offset + 1, contents.len(), 0).unwrap();
assert!(!ctx.selected.is_empty() || ctx.before.len() > 0 || ctx.after.len() > 0);
}
}
#[test]
fn test_empty_context_handling() {
let test_cases = vec![
("test.rs", "fn test() {}\n"),
("test.py", "def test(): pass\n"),
("test.ts", "function test() {}\n"),
];
for (filename, content) in test_cases {
let mut file = NamedTempFile::new().unwrap();
file.write_all(content.as_bytes()).unwrap();
let file_path = file.path();
let ctx = extract_context(file_path, 0, 5, 0).unwrap();
assert_eq!(
ctx.before.len(),
0,
"Before should be empty for {}",
filename
);
assert_eq!(ctx.after.len(), 0, "After should be empty for {}", filename);
assert!(!ctx.selected.is_empty(), "Selected should not be empty");
}
}
#[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("\"error_code\""));
}
#[test]
fn test_span_result_with_context_all_languages() {
let test_cases: Vec<(&str, &str, &str)> = vec![
(RUST_FUNCTION, "rust", "pub fn add"),
(PYTHON_FUNCTION, "python", "def add"),
(C_FUNCTION, "c", "int add"),
(CPP_FUNCTION, "cpp", "int add"),
(JAVA_CLASS, "java", "public int add"),
(JAVASCRIPT_FUNCTION, "javascript", "function add"),
(TYPESCRIPT_FUNCTION, "typescript", "function add"),
];
for (content, lang, search_term) in test_cases {
let mut file = NamedTempFile::new().unwrap();
file.write_all(content.as_bytes()).unwrap();
let file_path = file.path();
let source = std::fs::read_to_string(file_path).unwrap();
let offset = source.find(search_term).unwrap_or(0);
let offset_end = offset + 50;
let mut span =
SpanResult::from_byte_span(file_path.to_string_lossy().to_string(), offset, offset_end);
let ctx = extract_context(file_path, offset, offset_end, 1).unwrap();
span = span.with_context(ctx);
assert!(
span.context.is_some(),
"Context should be Some for {}",
lang
);
let span_ctx = span.context.as_ref().unwrap();
assert!(
!span_ctx.selected.is_empty(),
"Selected should not be empty for {}",
lang
);
}
}
#[test]
fn test_all_splice_error_codes_valid_format() {
let error_codes = vec![
SpliceErrorCode::SymbolNotFound,
SpliceErrorCode::AmbiguousSymbol,
SpliceErrorCode::ReferenceFailed,
SpliceErrorCode::AmbiguousReference,
SpliceErrorCode::ParseError,
SpliceErrorCode::InvalidUtf8,
SpliceErrorCode::InvalidSyntax,
SpliceErrorCode::InvalidSpan,
SpliceErrorCode::InvalidLineRange,
SpliceErrorCode::SpanOutOfBounds,
SpliceErrorCode::FileReadError,
SpliceErrorCode::FileWriteError,
SpliceErrorCode::FileNotFound,
SpliceErrorCode::FileExternallyModified,
SpliceErrorCode::AmbiguousSymbolAsWarning,
SpliceErrorCode::FileSkipped,
SpliceErrorCode::FileExternallyModifiedWarning,
SpliceErrorCode::PreVerificationFailed,
SpliceErrorCode::ParseValidationFailed,
SpliceErrorCode::CompilerValidationFailed,
SpliceErrorCode::InvalidPlanSchema,
SpliceErrorCode::PlanExecutionFailed,
SpliceErrorCode::InvalidBatchSchema,
SpliceErrorCode::GraphError,
SpliceErrorCode::DatabaseError,
SpliceErrorCode::ExecutionLogError,
SpliceErrorCode::ExecutionNotFound,
SpliceErrorCode::AnalyzerNotAvailable,
SpliceErrorCode::AnalyzerFailed,
];
for code in error_codes {
let code_str = code.code();
let severity = code.severity();
let hint = code.hint();
assert!(
code_str.starts_with("SPL-"),
"Error code should start with 'SPL-': {}",
code_str
);
assert!(
code_str.len() >= 7,
"Error code should be at least 7 chars: {}",
code_str
);
assert!(
severity == "error" || severity == "warning" || severity == "note",
"Severity should be error/warning/note: {}",
severity
);
assert!(
!hint.is_empty(),
"Hint should not be empty for {}",
code_str
);
}
}