Coverage for tests / unit / parsers / test_sqlfluff_parser.py: 100%
113 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Unit tests for SQLFluff parser."""
3from __future__ import annotations
5import pytest
6from assertpy import assert_that
8from lintro.parsers.sqlfluff.sqlfluff_parser import parse_sqlfluff_output
11@pytest.mark.parametrize(
12 "output",
13 [
14 "",
15 None,
16 " \n \n ",
17 "[]",
18 "{}",
19 ],
20 ids=["empty", "none", "whitespace_only", "empty_array", "empty_object"],
21)
22def test_parse_sqlfluff_output_returns_empty_for_no_content(
23 output: str | None,
24) -> None:
25 """Parse empty or whitespace-only output returns empty list.
27 Args:
28 output: The SQLFluff output to parse.
29 """
30 result = parse_sqlfluff_output(output)
31 assert_that(result).is_empty()
34def test_parse_sqlfluff_output_single_violation() -> None:
35 """Parse single violation from SQLFluff JSON output."""
36 output = """[
37 {
38 "filepath": "query.sql",
39 "violations": [
40 {
41 "start_line_no": 1,
42 "start_line_pos": 1,
43 "end_line_no": 1,
44 "end_line_pos": 6,
45 "code": "L010",
46 "description": "Keywords must be upper case.",
47 "name": "capitalisation.keywords"
48 }
49 ]
50 }
51]"""
52 result = parse_sqlfluff_output(output)
53 assert_that(result).is_length(1)
54 assert_that(result[0].file).is_equal_to("query.sql")
55 assert_that(result[0].line).is_equal_to(1)
56 assert_that(result[0].column).is_equal_to(1)
57 assert_that(result[0].end_line).is_equal_to(1)
58 assert_that(result[0].end_column).is_equal_to(6)
59 assert_that(result[0].code).is_equal_to("L010")
60 assert_that(result[0].rule_name).is_equal_to("capitalisation.keywords")
61 assert_that(result[0].message).is_equal_to("Keywords must be upper case.")
64def test_parse_sqlfluff_output_multiple_violations_single_file() -> None:
65 """Parse multiple violations from a single file."""
66 output = """[
67 {
68 "filepath": "query.sql",
69 "violations": [
70 {
71 "start_line_no": 1,
72 "start_line_pos": 1,
73 "code": "L010",
74 "description": "Keywords must be upper case.",
75 "name": "capitalisation.keywords"
76 },
77 {
78 "start_line_no": 3,
79 "start_line_pos": 5,
80 "code": "L011",
81 "description": "Implicit aliasing not allowed.",
82 "name": "aliasing.table"
83 }
84 ]
85 }
86]"""
87 result = parse_sqlfluff_output(output)
88 assert_that(result).is_length(2)
89 assert_that(result[0].line).is_equal_to(1)
90 assert_that(result[0].code).is_equal_to("L010")
91 assert_that(result[1].line).is_equal_to(3)
92 assert_that(result[1].code).is_equal_to("L011")
95def test_parse_sqlfluff_output_multiple_files() -> None:
96 """Parse violations from multiple files."""
97 output = """[
98 {
99 "filepath": "query1.sql",
100 "violations": [
101 {
102 "start_line_no": 1,
103 "start_line_pos": 1,
104 "code": "L010",
105 "description": "Error 1",
106 "name": "rule.one"
107 }
108 ]
109 },
110 {
111 "filepath": "query2.sql",
112 "violations": [
113 {
114 "start_line_no": 5,
115 "start_line_pos": 10,
116 "code": "L020",
117 "description": "Error 2",
118 "name": "rule.two"
119 }
120 ]
121 }
122]"""
123 result = parse_sqlfluff_output(output)
124 assert_that(result).is_length(2)
125 assert_that(result[0].file).is_equal_to("query1.sql")
126 assert_that(result[1].file).is_equal_to("query2.sql")
129def test_parse_sqlfluff_output_file_with_no_violations() -> None:
130 """Parse output where a file has no violations."""
131 output = """[
132 {
133 "filepath": "clean.sql",
134 "violations": []
135 }
136]"""
137 result = parse_sqlfluff_output(output)
138 assert_that(result).is_empty()
141def test_parse_sqlfluff_output_mixed_files() -> None:
142 """Parse output with some files having violations and some not."""
143 output = """[
144 {
145 "filepath": "clean.sql",
146 "violations": []
147 },
148 {
149 "filepath": "dirty.sql",
150 "violations": [
151 {
152 "start_line_no": 1,
153 "start_line_pos": 1,
154 "code": "L010",
155 "description": "Error",
156 "name": "rule.name"
157 }
158 ]
159 }
160]"""
161 result = parse_sqlfluff_output(output)
162 assert_that(result).is_length(1)
163 assert_that(result[0].file).is_equal_to("dirty.sql")
166def test_parse_sqlfluff_output_missing_optional_fields() -> None:
167 """Parse violations with missing optional fields."""
168 output = """[
169 {
170 "filepath": "query.sql",
171 "violations": [
172 {
173 "start_line_no": 1,
174 "start_line_pos": 1,
175 "code": "L010",
176 "description": "Error message"
177 }
178 ]
179 }
180]"""
181 result = parse_sqlfluff_output(output)
182 assert_that(result).is_length(1)
183 assert_that(result[0].end_line).is_none()
184 assert_that(result[0].end_column).is_none()
185 assert_that(result[0].rule_name).is_equal_to("")
188def test_parse_sqlfluff_output_invalid_json() -> None:
189 """Handle invalid JSON gracefully."""
190 output = "not valid json"
191 result = parse_sqlfluff_output(output)
192 assert_that(result).is_empty()
195def test_parse_sqlfluff_output_not_a_list() -> None:
196 """Handle non-list JSON gracefully."""
197 output = '{"filepath": "query.sql"}'
198 result = parse_sqlfluff_output(output)
199 assert_that(result).is_empty()
202# =============================================================================
203# Edge case tests
204# =============================================================================
207def test_parse_sqlfluff_output_unicode_in_message() -> None:
208 """Handle Unicode characters in error messages."""
209 output = """[
210 {
211 "filepath": "query.sql",
212 "violations": [
213 {
214 "start_line_no": 1,
215 "start_line_pos": 1,
216 "code": "L010",
217 "description": "Palavras-chave devem estar em maiusculas",
218 "name": "rule.name"
219 }
220 ]
221 }
222]"""
223 result = parse_sqlfluff_output(output)
224 assert_that(result).is_length(1)
225 assert_that(result[0].message).contains("Palavras-chave")
228def test_parse_sqlfluff_output_file_path_with_spaces() -> None:
229 """Handle file paths with spaces."""
230 output = """[
231 {
232 "filepath": "my project/sql files/query.sql",
233 "violations": [
234 {
235 "start_line_no": 1,
236 "start_line_pos": 1,
237 "code": "L010",
238 "description": "Error",
239 "name": "rule.name"
240 }
241 ]
242 }
243]"""
244 result = parse_sqlfluff_output(output)
245 assert_that(result).is_length(1)
246 assert_that(result[0].file).contains("my project")
249def test_parse_sqlfluff_output_deeply_nested_path() -> None:
250 """Handle deeply nested file paths."""
251 deep_path = "a/b/c/d/e/f/g/h/i/j/query.sql"
252 output = f"""[
253 {{
254 "filepath": "{deep_path}",
255 "violations": [
256 {{
257 "start_line_no": 1,
258 "start_line_pos": 1,
259 "code": "L010",
260 "description": "Error",
261 "name": "rule.name"
262 }}
263 ]
264 }}
265]"""
266 result = parse_sqlfluff_output(output)
267 assert_that(result).is_length(1)
268 assert_that(result[0].file).is_equal_to(deep_path)
271def test_parse_sqlfluff_output_very_large_line_number() -> None:
272 """Handle very large line numbers."""
273 output = """[
274 {
275 "filepath": "query.sql",
276 "violations": [
277 {
278 "start_line_no": 999999,
279 "start_line_pos": 1,
280 "code": "L010",
281 "description": "Error",
282 "name": "rule.name"
283 }
284 ]
285 }
286]"""
287 result = parse_sqlfluff_output(output)
288 assert_that(result).is_length(1)
289 assert_that(result[0].line).is_equal_to(999999)
292def test_parse_sqlfluff_output_very_long_message() -> None:
293 """Handle extremely long error messages."""
294 long_message = "x" * 5000
295 output = f"""[
296 {{
297 "filepath": "query.sql",
298 "violations": [
299 {{
300 "start_line_no": 1,
301 "start_line_pos": 1,
302 "code": "L010",
303 "description": "{long_message}",
304 "name": "rule.name"
305 }}
306 ]
307 }}
308]"""
309 result = parse_sqlfluff_output(output)
310 assert_that(result).is_length(1)
311 assert_that(len(result[0].message)).is_equal_to(5000)
314def test_parse_sqlfluff_output_special_chars_in_message() -> None:
315 """Handle special characters in error messages."""
316 output = """[
317 {
318 "filepath": "query.sql",
319 "violations": [
320 {
321 "start_line_no": 1,
322 "start_line_pos": 1,
323 "code": "L010",
324 "description": "Use \\"quotes\\" and <brackets>",
325 "name": "rule.name"
326 }
327 ]
328 }
329]"""
330 result = parse_sqlfluff_output(output)
331 assert_that(result).is_length(1)
332 assert_that(result[0].message).contains("quotes")
333 assert_that(result[0].message).contains("<brackets>")
336def test_parse_sqlfluff_output_zero_line_number() -> None:
337 """Handle zero line number (edge case)."""
338 output = """[
339 {
340 "filepath": "query.sql",
341 "violations": [
342 {
343 "start_line_no": 0,
344 "start_line_pos": 1,
345 "code": "L010",
346 "description": "Error",
347 "name": "rule.name"
348 }
349 ]
350 }
351]"""
352 result = parse_sqlfluff_output(output)
353 assert_that(result).is_length(1)
354 assert_that(result[0].line).is_equal_to(0)
357def test_parse_sqlfluff_output_null_violations() -> None:
358 """Handle null violations array gracefully."""
359 output = """[
360 {
361 "filepath": "query.sql",
362 "violations": null
363 }
364]"""
365 result = parse_sqlfluff_output(output)
366 assert_that(result).is_empty()
369def test_parse_sqlfluff_output_missing_violations_key() -> None:
370 """Handle missing violations key gracefully."""
371 output = """[
372 {
373 "filepath": "query.sql"
374 }
375]"""
376 result = parse_sqlfluff_output(output)
377 assert_that(result).is_empty()
380def test_parse_sqlfluff_output_alternative_field_names() -> None:
381 """Parse output with alternative field name mappings."""
382 output = """[
383 {
384 "file": "query.sql",
385 "violations": [
386 {
387 "line": 5,
388 "column": 10,
389 "code": "L010",
390 "message": "Alternative message format",
391 "rule_name": "alternative.rule"
392 }
393 ]
394 }
395]"""
396 result = parse_sqlfluff_output(output)
397 assert_that(result).is_length(1)
398 assert_that(result[0].file).is_equal_to("query.sql")
399 assert_that(result[0].line).is_equal_to(5)
400 assert_that(result[0].column).is_equal_to(10)
401 assert_that(result[0].message).is_equal_to("Alternative message format")
402 assert_that(result[0].rule_name).is_equal_to("alternative.rule")