1use std::{fs, path::Path};
2
3use anyhow::{Result, anyhow};
4use tree_sitter::Point;
5use tree_sitter_highlight::{Highlight, HighlightConfiguration, HighlightEvent, Highlighter};
6use tree_sitter_loader::{Config, Loader};
7
8use crate::{
9 query_testing::{Assertion, Utf8Point, parse_position_comments, to_utf8_point},
10 test::{TestInfo, TestOutcome, TestResult, TestSummary},
11 util,
12};
13
14#[derive(Debug)]
15pub struct Failure {
16 pub(crate) row: usize,
17 pub(crate) column: usize,
18 pub(crate) expected_highlight: String,
19 pub(crate) actual_highlights: Vec<String>,
20}
21
22impl std::error::Error for Failure {}
23
24impl std::fmt::Display for Failure {
25 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
26 write!(
27 f,
28 "Failure - row: {}, column: {}, expected highlight '{}', actual highlights: ",
29 self.row, self.column, self.expected_highlight
30 )?;
31 if self.actual_highlights.is_empty() {
32 write!(f, "none.")?;
33 } else {
34 for (i, actual_highlight) in self.actual_highlights.iter().enumerate() {
35 if i > 0 {
36 write!(f, ", ")?;
37 }
38 write!(f, "'{actual_highlight}'")?;
39 }
40 }
41 Ok(())
42 }
43}
44
45pub fn test_highlights(
46 loader: &Loader,
47 loader_config: &Config,
48 highlighter: &mut Highlighter,
49 directory: &Path,
50 test_summary: &mut TestSummary,
51) -> Result<()> {
52 let mut failed = false;
53
54 for highlight_test_file in fs::read_dir(directory)? {
55 let highlight_test_file = highlight_test_file?;
56 let test_file_path = highlight_test_file.path();
57 let test_file_name = highlight_test_file.file_name();
58 if test_file_path.is_dir() && test_file_path.read_dir()?.next().is_some() {
59 test_summary
60 .highlight_results
61 .add_group(test_file_name.to_string_lossy().as_ref());
62 if test_highlights(
63 loader,
64 loader_config,
65 highlighter,
66 &test_file_path,
67 test_summary,
68 )
69 .is_err()
70 {
71 failed = true;
72 }
73 test_summary.highlight_results.pop_traversal();
74 } else {
75 let (language, language_config) = loader
76 .language_configuration_for_file_name(&test_file_path)?
77 .ok_or_else(|| {
78 anyhow!(
79 "{}",
80 util::lang_not_found_for_path(test_file_path.as_path(), loader_config)
81 )
82 })?;
83 let highlight_config = language_config
84 .highlight_config(language, None)?
85 .ok_or_else(|| {
86 anyhow!(
87 "No highlighting config found for {}",
88 test_file_path.display()
89 )
90 })?;
91 match test_highlight(
92 loader,
93 highlighter,
94 highlight_config,
95 fs::read(&test_file_path)?.as_slice(),
96 ) {
97 Ok(assertion_count) => {
98 test_summary.highlight_results.add_case(TestResult {
99 name: test_file_name.to_string_lossy().to_string(),
100 info: TestInfo::AssertionTest {
101 outcome: TestOutcome::AssertionPassed { assertion_count },
102 test_num: test_summary.test_num,
103 },
104 });
105 }
106 Err(e) => {
107 test_summary.highlight_results.add_case(TestResult {
108 name: test_file_name.to_string_lossy().to_string(),
109 info: TestInfo::AssertionTest {
110 outcome: TestOutcome::AssertionFailed {
111 error: e.to_string(),
112 },
113 test_num: test_summary.test_num,
114 },
115 });
116 failed = true;
117 }
118 }
119 test_summary.test_num += 1;
120 }
121 }
122
123 if failed { Err(anyhow!("")) } else { Ok(()) }
124}
125
126pub fn iterate_assertions(
127 assertions: &[Assertion],
128 highlights: &[(Utf8Point, Utf8Point, Highlight)],
129 highlight_names: &[String],
130) -> Result<usize> {
131 let mut i = 0;
134 let mut actual_highlights = Vec::new();
135 for Assertion {
136 position,
137 length,
138 negative,
139 expected_capture_name: expected_highlight,
140 } in assertions
141 {
142 actual_highlights.clear();
145 let mut passed = false;
146 let end_column = position.column + length - 1;
147 for highlight in &highlights[i..] {
148 if highlight.1 <= *position {
151 i += 1;
152 continue;
153 }
154 if (highlight.0.row > position.row)
155 || (highlight.0.row == position.row && highlight.0.column > end_column)
156 {
157 break;
158 }
159
160 let highlight_name = &highlight_names[(highlight.2).0];
166 if (*highlight_name == *expected_highlight) == *negative {
167 actual_highlights.push(highlight_name);
168 } else {
169 passed = true;
170 break;
171 }
172 }
173
174 if !passed {
175 let mut expected = String::with_capacity(expected_highlight.len() + 1);
176 if *negative {
177 expected.push('!');
178 }
179 expected.push_str(expected_highlight);
180 return Err(Failure {
181 row: position.row,
182 column: end_column,
183 expected_highlight: expected,
184 actual_highlights: actual_highlights.into_iter().cloned().collect(),
185 }
186 .into());
187 }
188 }
189
190 Ok(assertions.len())
191}
192
193pub fn test_highlight(
194 loader: &Loader,
195 highlighter: &mut Highlighter,
196 highlight_config: &HighlightConfiguration,
197 source: &[u8],
198) -> Result<usize> {
199 let highlight_names = loader.highlight_names();
201 let highlights = get_highlight_positions(loader, highlighter, highlight_config, source)?;
202 let assertions =
203 parse_position_comments(highlighter.parser(), &highlight_config.language, source)?;
204
205 iterate_assertions(&assertions, &highlights, &highlight_names)
206}
207
208pub fn get_highlight_positions(
209 loader: &Loader,
210 highlighter: &mut Highlighter,
211 highlight_config: &HighlightConfiguration,
212 source: &[u8],
213) -> Result<Vec<(Utf8Point, Utf8Point, Highlight)>> {
214 let mut row = 0;
215 let mut column = 0;
216 let mut byte_offset = 0;
217 let mut was_newline = false;
218 let mut result = Vec::new();
219 let mut highlight_stack = Vec::new();
220 let source = String::from_utf8_lossy(source);
221 let mut char_indices = source.char_indices();
222 for event in
223 highlighter.highlight(highlight_config, source.as_bytes(), None, None, |string| {
224 loader.highlight_config_for_injection_string(string)
225 })?
226 {
227 match event? {
228 HighlightEvent::HighlightStart(h) => highlight_stack.push(h),
229 HighlightEvent::HighlightEnd => {
230 highlight_stack.pop();
231 }
232 HighlightEvent::Source { start, end } => {
233 let mut start_position = Point::new(row, column);
234 while byte_offset < end {
235 if byte_offset <= start {
236 start_position = Point::new(row, column);
237 }
238 if let Some((i, c)) = char_indices.next() {
239 if was_newline {
240 row += 1;
241 column = 0;
242 } else {
243 column += i - byte_offset;
244 }
245 was_newline = c == '\n';
246 byte_offset = i;
247 } else {
248 break;
249 }
250 }
251 if let Some(highlight) = highlight_stack.last() {
252 let utf8_start_position = to_utf8_point(start_position, source.as_bytes());
253 let utf8_end_position =
254 to_utf8_point(Point::new(row, column), source.as_bytes());
255 result.push((utf8_start_position, utf8_end_position, *highlight));
256 }
257 }
258 }
259 }
260 Ok(result)
261}