diff --git a/crates/cli/src/test_highlight.rs b/crates/cli/src/test_highlight.rs index d96f90c2..a3f84762 100644 --- a/crates/cli/src/test_highlight.rs +++ b/crates/cli/src/test_highlight.rs @@ -13,10 +13,10 @@ use crate::{ #[derive(Debug)] pub struct Failure { - row: usize, - column: usize, - expected_highlight: String, - actual_highlights: Vec, + pub(crate) row: usize, + pub(crate) column: usize, + pub(crate) expected_highlight: String, + pub(crate) actual_highlights: Vec, } impl std::error::Error for Failure {} @@ -126,6 +126,7 @@ pub fn test_highlights( Ok(()) } } + pub fn iterate_assertions( assertions: &[Assertion], highlights: &[(Utf8Point, Utf8Point, Highlight)], @@ -134,7 +135,6 @@ pub fn iterate_assertions( // Iterate through all of the highlighting assertions, checking each one against the // actual highlights. let mut i = 0; - let mut actual_highlights = Vec::new(); for Assertion { position, length, @@ -142,49 +142,47 @@ pub fn iterate_assertions( expected_capture_name: expected_highlight, } in assertions { + // Iterate through all of the highlights that start at or before this assertion's + // position, looking for one that matches the assertion. + let mut actual_highlights = Vec::new(); let mut passed = false; let mut end_column = position.column + length - 1; - actual_highlights.clear(); - - // The assertions are ordered by position, so skip past all of the highlights that - // end at or before this assertion's position. - 'highlight_loop: while let Some(highlight) = highlights.get(i) { + for highlight in &highlights[i..] { + // The assertions are ordered by position, so skip past all of the highlights that + // end at or before this assertion's position. if highlight.1 <= *position { i += 1; continue; } + end_column = position.column + length - 1; + if highlight.0.row >= position.row && highlight.0.column > end_column { + break; + } - // Iterate through all of the highlights that start at or before this assertion's - // position, looking for one that matches the assertion. - let mut j = i; - while let (false, Some(highlight)) = (passed, highlights.get(j)) { - end_column = position.column + length - 1; - if highlight.0.row >= position.row && highlight.0.column > end_column { - break 'highlight_loop; - } - - // If the highlight matches the assertion, or if the highlight doesn't - // match the assertion but it's negative, this test passes. Otherwise, - // add this highlight to the list of actual highlights that span the - // assertion's position, in order to generate an error message in the event - // of a failure. - let highlight_name = &highlight_names[(highlight.2).0]; - if (*highlight_name == *expected_highlight) == *negative { - actual_highlights.push(highlight_name); - } else { - passed = true; - break 'highlight_loop; - } - - j += 1; + // If the highlight matches the assertion, or if the highlight doesn't + // match the assertion but it's negative, this test passes. Otherwise, + // add this highlight to the list of actual highlights that span the + // assertion's position, in order to generate an error message in the event + // of a failure. + let highlight_name = &highlight_names[(highlight.2).0]; + if (*highlight_name == *expected_highlight) == *negative { + actual_highlights.push(highlight_name); + } else { + passed = true; + break; } } if !passed { + let mut expected = String::with_capacity(expected_highlight.len() + 1); + if *negative { + expected.push('!'); + } + expected.push_str(expected_highlight); return Err(Failure { row: position.row, column: end_column, - expected_highlight: expected_highlight.clone(), + expected_highlight: expected, actual_highlights: actual_highlights.into_iter().cloned().collect(), } .into()); diff --git a/crates/cli/src/tests/test_highlight_test.rs b/crates/cli/src/tests/test_highlight_test.rs index e8567d6d..4ef1d79c 100644 --- a/crates/cli/src/tests/test_highlight_test.rs +++ b/crates/cli/src/tests/test_highlight_test.rs @@ -4,7 +4,7 @@ use tree_sitter_highlight::{Highlight, Highlighter}; use super::helpers::fixtures::{get_highlight_config, get_language, test_loader}; use crate::{ query_testing::{parse_position_comments, Assertion, Utf8Point}, - test_highlight::get_highlight_positions, + test_highlight::{get_highlight_positions, iterate_assertions, Failure}, }; #[test] @@ -68,3 +68,221 @@ fn test_highlight_test_with_basic_test() { ] ); } + +#[test] +fn test_assertion_with_non_matching_highlight_at_same_position() { + // Test that an assertion fails when the highlight at the position does not match + + let highlight_names = vec!["keyword".to_string(), "variable".to_string()]; + + let assertions = vec![Assertion::new(1, 0, 1, false, String::from("keyword"))]; + + let highlights = vec![ + (Utf8Point::new(1, 0), Utf8Point::new(1, 5), Highlight(1)), // "variable" highlight + ]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_err()); + let err = result.unwrap_err().downcast::().unwrap(); + assert_eq!(err.row, 1); + assert_eq!(err.column, 0); + assert_eq!(err.expected_highlight, "keyword"); + assert_eq!(err.actual_highlights, vec!["variable".to_string()]); +} + +#[test] +fn test_assertion_with_exact_matching_highlight() { + // Test exact match: assertion and highlight have same start and end + + let highlight_names = vec!["keyword".to_string()]; + + let assertions = vec![Assertion::new(0, 5, 3, false, String::from("keyword"))]; + + let highlights = vec![(Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(0))]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); +} + +#[test] +fn test_assertion_contained_within_highlight() { + // Test where assertion is fully contained within a larger highlight + + let highlight_names = vec!["keyword".to_string()]; + + let assertions = vec![Assertion::new(0, 3, 2, false, String::from("keyword"))]; + + let highlights = vec![(Utf8Point::new(0, 0), Utf8Point::new(0, 10), Highlight(0))]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); +} + +#[test] +fn test_assertion_overlapping_highlight_start() { + // Test where assertion starts before highlight but overlaps with it + + let highlight_names = vec!["keyword".to_string()]; + + let assertions = vec![Assertion::new(0, 3, 4, false, String::from("keyword"))]; + + let highlights = vec![(Utf8Point::new(0, 5), Utf8Point::new(0, 10), Highlight(0))]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); +} + +#[test] +fn test_assertion_with_no_highlights() { + // Test that an assertion fails when there are no highlights at all + + let highlight_names = vec!["keyword".to_string()]; + + let assertions = vec![Assertion::new(0, 0, 1, false, String::from("keyword"))]; + + let highlights = vec![]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_err()); + let err = result.unwrap_err().downcast::().unwrap(); + assert_eq!(err.row, 0); + assert_eq!(err.column, 0); + assert_eq!(err.expected_highlight, "keyword"); + assert_eq!(err.actual_highlights, Vec::::new()); +} + +#[test] +fn test_assertion_with_highlight_ending_before() { + // Test where highlight ends before the assertion starts + + let highlight_names = vec!["keyword".to_string()]; + + let assertions = vec![Assertion::new(0, 10, 1, false, String::from("keyword"))]; + + let highlights = vec![(Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(0))]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_err()); + let err = result.unwrap_err().downcast::().unwrap(); + assert_eq!(err.row, 0); + assert_eq!(err.column, 10); + assert_eq!(err.expected_highlight, "keyword"); + assert_eq!(err.actual_highlights, Vec::::new()); +} + +#[test] +fn test_negative_assertion_with_non_matching_highlight() { + // Test that a negative assertion passes when the specified highlight is NOT present + + let highlight_names = vec!["keyword".to_string(), "variable".to_string()]; + + let assertions = vec![Assertion::new(0, 0, 1, true, String::from("keyword"))]; + + let highlights = vec![ + (Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(1)), // "variable" highlight + ]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); +} + +#[test] +fn test_negative_assertion_with_matching_highlight() { + // Test that a negative assertion fails when the specified highlight IS present + + let highlight_names = vec!["keyword".to_string()]; + + let assertions = vec![Assertion::new(0, 0, 1, true, String::from("keyword"))]; + + let highlights = vec![ + (Utf8Point::new(0, 0), Utf8Point::new(0, 5), Highlight(0)), // "keyword" highlight + ]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_err()); + let err = result.unwrap_err().downcast::().unwrap(); + assert_eq!(err.row, 0); + assert_eq!(err.column, 0); + assert_eq!(err.expected_highlight, "!keyword"); + assert_eq!(err.actual_highlights, vec!["keyword".to_string()]); +} + +#[test] +fn test_multiple_assertions_sequential() { + // Test multiple assertions in sequence with non-overlapping highlights + + let highlight_names = vec!["keyword".to_string(), "variable".to_string()]; + + let assertions = vec![ + Assertion::new(0, 0, 3, false, String::from("keyword")), + Assertion::new(0, 10, 1, false, String::from("variable")), + ]; + + let highlights = vec![ + (Utf8Point::new(0, 0), Utf8Point::new(0, 3), Highlight(0)), // "keyword" + (Utf8Point::new(0, 10), Utf8Point::new(0, 11), Highlight(1)), // "variable" + ]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 2); +} + +#[test] +fn test_multiple_highlights_at_same_position() { + // Test where multiple highlights overlap at the assertion position + + let highlight_names = vec![ + "keyword".to_string(), + "variable".to_string(), + "function".to_string(), + ]; + + let assertions = vec![Assertion::new(0, 5, 1, false, String::from("variable"))]; + + let highlights = vec![ + (Utf8Point::new(0, 0), Utf8Point::new(0, 10), Highlight(0)), // "keyword" spans entire range + (Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(1)), // "variable" at assertion position + (Utf8Point::new(0, 7), Utf8Point::new(0, 12), Highlight(2)), // "function" overlaps + ]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 1); +} + +#[test] +fn test_assertions_across_multiple_rows() { + // Test assertions on different rows + + let highlight_names = vec!["keyword".to_string(), "variable".to_string()]; + + let assertions = vec![ + Assertion::new(0, 5, 3, false, String::from("keyword")), + Assertion::new(2, 10, 1, false, String::from("variable")), + ]; + + let highlights = vec![ + (Utf8Point::new(0, 5), Utf8Point::new(0, 8), Highlight(0)), // "keyword" on row 0 + (Utf8Point::new(2, 10), Utf8Point::new(2, 11), Highlight(1)), // "variable" on row 2 + ]; + + let result = iterate_assertions(&assertions, &highlights, &highlight_names); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 2); +}