use magellan::output::command::Span;
fn make_test_span(file_path: &str, source: &str, byte_start: usize, byte_end: usize) -> Span {
let (start_line, start_col) =
byte_offset_to_line_col(source, byte_start).expect("Invalid start offset");
let (end_line, end_col) =
byte_offset_to_line_col(source, byte_end).expect("Invalid end offset");
Span::new(
file_path.to_string(),
byte_start,
byte_end,
start_line + 1, start_col,
end_line + 1, end_col,
)
}
fn byte_offset_to_line_col(source: &str, byte_offset: usize) -> Option<(usize, usize)> {
if byte_offset > source.len() {
return None;
}
let mut line = 0;
let mut line_start = 0;
for (i, ch) in source.char_indices() {
if i == byte_offset {
return Some((line, byte_offset - line_start));
}
if ch == '\n' {
line += 1;
line_start = i + 1;
}
}
if byte_offset == source.len() {
return Some((line, byte_offset - line_start));
}
None
}
fn line_col_to_byte_offset(source: &str, line: usize, col: usize) -> Option<usize> {
let mut current_line = 0;
let mut line_start = 0;
for (i, ch) in source.char_indices() {
if current_line == line && i - line_start >= col {
return Some(i);
}
if ch == '\n' {
current_line += 1;
line_start = i + 1;
}
}
if current_line == line {
return Some(source.len());
}
None
}
#[test]
fn test_half_open_span_extraction() {
let source = "fn main() {\n println!(\"Hello\");\n}";
let byte_start = 3;
let byte_end = 7;
let span = make_test_span("test.rs", source, byte_start, byte_end);
let extracted = source.get(span.byte_start..span.byte_end);
assert_eq!(
extracted,
Some("main"),
"Half-open extraction should get 'main'"
);
}
#[test]
fn test_span_length_equals_byte_end_minus_start() {
let source = "fn main() {}";
let span = make_test_span("test.rs", source, 3, 7);
assert_eq!(
span.byte_end - span.byte_start,
4,
"Length should be end - start"
);
assert_eq!(
span.byte_end - span.byte_start,
"main".len(),
"Length should match content"
);
}
#[test]
fn test_adjacent_spans_no_overlap() {
let source = "fn main() {}";
let span1 = make_test_span("test.rs", source, 0, 5); let span2 = make_test_span("test.rs", source, 5, 10);
assert_eq!(
span1.byte_end, span2.byte_start,
"Adjacent spans should meet exactly"
);
let combined = format!(
"{}{}",
source.get(span1.byte_start..span1.byte_end).unwrap(),
source.get(span2.byte_start..span2.byte_end).unwrap()
);
assert_eq!(
combined, "fn main() ",
"Adjacent spans should concatenate without gap"
);
}
#[test]
fn test_empty_span_valid() {
let source = "fn main() {}";
let span = make_test_span("test.rs", source, 5, 5);
assert_eq!(
span.byte_start, span.byte_end,
"Empty span has start == end"
);
assert_eq!(span.byte_end - span.byte_start, 0, "Empty span length is 0");
let extracted = source.get(span.byte_start..span.byte_end);
assert_eq!(
extracted,
Some(""),
"Empty span should extract empty string"
);
}
#[test]
fn test_end_position_exclusive() {
let source = "fn main() {\n return 1;\n}";
let byte_start = 3;
let byte_end = 7;
let span = make_test_span("test.rs", source, byte_start, byte_end);
assert_eq!(span.start_line, 1, "Start line should be 1-indexed");
assert_eq!(span.start_col, 3, "Start column should point to 'm'");
assert_eq!(
span.end_line, 1,
"End line should be same as start for single-line span"
);
assert_eq!(
span.end_col, 7,
"End column should point to '(' (after 'main')"
);
let char_after = source.chars().nth(span.byte_end);
assert_eq!(
char_after,
Some('('),
"Character at byte_end is NOT included in span"
);
}
#[test]
fn test_multiline_span() {
let source = "fn main() {\n return 1;\n}";
let byte_start = 3; let byte_end = 23; let span = make_test_span("test.rs", source, byte_start, byte_end);
assert_eq!(span.start_line, 1, "Start on line 1");
assert_eq!(span.end_line, 2, "End on line 2");
let extracted = source.get(span.byte_start..span.byte_end).unwrap();
assert!(extracted.contains("main"), "Should include 'main'");
assert!(extracted.contains('\n'), "Should include newline");
assert!(extracted.contains("return"), "Should include 'return'");
}
#[test]
fn test_span_at_line_start() {
let source = "fn main() {\n let x = 1;\n}";
let byte_start = 12; let byte_end = 27; let span = make_test_span("test.rs", source, byte_start, byte_end);
assert_eq!(span.start_line, 2, "Start of line 2");
assert_eq!(span.start_col, 0, "At column 0 of line 2");
assert_eq!(span.end_line, 3, "End on line 3 (after \\n)");
assert_eq!(span.end_col, 0, "At column 0 of line 3");
let extracted = source.get(span.byte_start..span.byte_end).unwrap();
assert_eq!(
extracted, " let x = 1;\n",
"Should extract whole line segment including newline"
);
}
#[test]
fn test_span_extract_with_newlines() {
let source = "line1\nline2\nline3";
let span = make_test_span("test.rs", source, 3, 14);
let extracted = source.get(span.byte_start..span.byte_end).unwrap();
assert_eq!(
extracted, "e1\nline2\nli",
"Should include newlines correctly"
);
}
#[test]
fn test_span_bytes_vs_characters() {
let source = "test\u{4e2d}";
let span_ascii = make_test_span("test.rs", source, 0, 4);
assert_eq!(
span_ascii.byte_end - span_ascii.byte_start,
4,
"ASCII length = 4 bytes"
);
let extracted_ascii = source
.get(span_ascii.byte_start..span_ascii.byte_end)
.unwrap();
assert_eq!(extracted_ascii, "test", "ASCII part extracted correctly");
let span_cjk = make_test_span("test.rs", source, 4, 7);
assert_eq!(
span_cjk.byte_end - span_cjk.byte_start,
3,
"CJK char = 3 bytes"
);
let extracted_cjk = source.get(span_cjk.byte_start..span_cjk.byte_end).unwrap();
assert_eq!(extracted_cjk.chars().count(), 1, "CJK char is 1 character");
assert_eq!(extracted_cjk.len(), 3, "CJK char is 3 bytes");
assert_eq!(extracted_cjk, "\u{4e2d}", "CJK char extracted correctly");
}
#[test]
fn test_span_extraction_with_tabs() {
let source = "fn\tmain() {\n\treturn;\n}";
let span = make_test_span("test.rs", source, 2, 8);
let extracted = source.get(span.byte_start..span.byte_end).unwrap();
assert_eq!(extracted, "\tmain(", "Tabs should be included correctly");
assert_eq!(
extracted.as_bytes().first(),
Some(&b'\t'),
"First char is tab (byte 0x09)"
);
}
#[test]
fn test_span_overlapping_validation() {
let source = "abcdefghijklmnopqrstuvwxyz";
let span1 = make_test_span("test.rs", source, 0, 5); let span2 = make_test_span("test.rs", source, 5, 10);
assert_eq!(
span1.byte_end, span2.byte_start,
"Adjacent spans don't overlap"
);
assert!(
span1.byte_end <= span2.byte_start,
"span1 ends before or at span2 start"
);
}
#[test]
fn test_byte_offset_to_line_col() {
let source = "line1\nline2\nline3";
let (line, col) = byte_offset_to_line_col(source, 0).unwrap();
assert_eq!(line, 0, "Byte 0 is on line 0");
assert_eq!(col, 0, "Byte 0 is at column 0");
let (line, col) = byte_offset_to_line_col(source, 3).unwrap();
assert_eq!(line, 0, "Byte 3 is on line 0");
assert_eq!(col, 3, "Byte 3 is at column 3");
let (line, col) = byte_offset_to_line_col(source, 6).unwrap();
assert_eq!(line, 1, "Byte 6 is on line 1");
assert_eq!(col, 0, "Byte 6 is at column 0 of line 1");
let (line, col) = byte_offset_to_line_col(source, 8).unwrap();
assert_eq!(line, 1, "Byte 8 is on line 1");
assert_eq!(col, 2, "Byte 8 is at column 2 of line 1");
let (line, col) = byte_offset_to_line_col(source, source.len()).unwrap();
assert_eq!(line, 2, "End is on line 2");
assert_eq!(col, 5, "End is at column 5 of line 2");
}
#[test]
fn test_line_col_to_byte_offset() {
let source = "line1\nline2\nline3";
let offset = line_col_to_byte_offset(source, 0, 0).unwrap();
assert_eq!(offset, 0, "Line 0, col 0 is byte 0");
let offset = line_col_to_byte_offset(source, 0, 3).unwrap();
assert_eq!(offset, 3, "Line 0, col 3 is byte 3");
let offset = line_col_to_byte_offset(source, 1, 0).unwrap();
assert_eq!(offset, 6, "Line 1, col 0 is byte 6 (after newline)");
let offset = line_col_to_byte_offset(source, 1, 2).unwrap();
assert_eq!(offset, 8, "Line 1, col 2 is byte 8");
}
#[test]
fn test_span_roundtrip_conversion() {
let source = "fn main() {\n let x = 42;\n return x;\n}";
let test_offsets = [(0, 3), (3, 7), (7, 9), (20, 28)];
for (start, end) in test_offsets {
let (start_line, start_col) =
byte_offset_to_line_col(source, start).expect("Invalid start offset");
let (end_line, end_col) = byte_offset_to_line_col(source, end).expect("Invalid end offset");
let recovered_start = line_col_to_byte_offset(source, start_line, start_col)
.expect("Failed to recover start");
let recovered_end =
line_col_to_byte_offset(source, end_line, end_col).expect("Failed to recover end");
assert_eq!(
recovered_start, start,
"Roundtrip failed: start {} became {}",
start, recovered_start
);
assert_eq!(
recovered_end, end,
"Roundtrip failed: end {} became {}",
end, recovered_end
);
}
}
#[test]
fn test_multibyte_column_is_byte_based() {
let source = "abc\u{4e2d}";
let result = byte_offset_to_line_col(source, 6);
assert!(
result.is_some(),
"Should return result for byte offset 6 (after CJK char)"
);
let (line, col) = result.unwrap();
assert_eq!(line, 0, "Still on line 0");
assert_eq!(
col, 6,
"Column is byte offset (6), not character offset (4)"
);
assert_eq!(col, 6, "Column counts all 6 bytes");
let byte = source.as_bytes().get(3);
assert!(byte.is_some(), "Byte 3 exists");
assert_eq!(
byte.unwrap(),
&0xe4,
"Byte 3 is start of multi-byte CJK char (0xe4)"
);
}
#[test]
fn test_empty_lines_in_conversion() {
let source = "line1\n\nline3";
let offset = line_col_to_byte_offset(source, 1, 0).unwrap();
assert_eq!(offset, 6, "Start of empty line 1 is byte 6");
let (line, col) = byte_offset_to_line_col(source, offset).unwrap();
assert_eq!(line, 1, "At line 1 (empty line)");
assert_eq!(col, 0, "At column 0");
}
#[test]
fn test_line_col_conversion_with_carriage_return() {
let source = "line1\nline2\n";
let (line, col) = byte_offset_to_line_col(source, 6).unwrap();
assert_eq!(line, 1, "After \\n should be on line 1");
assert_eq!(col, 0, "At start of line 1");
let offset = line_col_to_byte_offset(source, 1, 0).unwrap();
assert_eq!(offset, 6, "Line 1, col 0 should be byte 6");
}
#[test]
fn test_byte_offset_beyond_source_returns_none() {
let source = "short";
assert!(
byte_offset_to_line_col(source, 100).is_none(),
"Offset beyond source should return None"
);
assert!(
byte_offset_to_line_col(source, source.len() + 1).is_none(),
"Offset past end should return None"
);
}
#[test]
fn test_line_col_beyond_source_returns_none() {
let source = "line1\nline2";
assert!(
line_col_to_byte_offset(source, 10, 0).is_none(),
"Line beyond source should return None"
);
}
#[test]
fn test_span_id_integration() {
let source = "fn main() {}";
let span = make_test_span("src/main.rs", source, 3, 7);
assert_eq!(
span.span_id.len(),
16,
"Span ID should be 16 hex characters"
);
assert!(
span.span_id.chars().all(|c| c.is_ascii_hexdigit()),
"Span ID should be all hex"
);
let span2 = make_test_span("src/main.rs", source, 3, 7);
assert_eq!(
span.span_id, span2.span_id,
"Same span data produces same ID"
);
}