Navegador

navegador / tests / test_hcl_parser.py
Blame History Raw 504 lines
1
"""Tests for navegador.ingestion.hcl — HCLParser internal methods."""
2
3
from unittest.mock import MagicMock, patch
4
5
import pytest
6
7
from navegador.graph.schema import EdgeType, NodeLabel
8
9
10
class MockNode:
11
_id_counter = 0
12
13
def __init__(
14
self,
15
type_: str,
16
text: bytes = b"",
17
children: list = None,
18
start_byte: int = 0,
19
end_byte: int = 0,
20
start_point: tuple = (0, 0),
21
end_point: tuple = (0, 0),
22
parent=None,
23
):
24
MockNode._id_counter += 1
25
self.id = MockNode._id_counter
26
self.type = type_
27
self._text = text
28
self.children = children or []
29
self.start_byte = start_byte
30
self.end_byte = end_byte
31
self.start_point = start_point
32
self.end_point = end_point
33
self.parent = parent
34
self._fields: dict = {}
35
for child in self.children:
36
child.parent = self
37
38
def child_by_field_name(self, name: str):
39
return self._fields.get(name)
40
41
def set_field(self, name: str, node):
42
self._fields[name] = node
43
node.parent = self
44
return self
45
46
47
def _text_node(text: bytes, type_: str = "identifier") -> MockNode:
48
return MockNode(type_, text, start_byte=0, end_byte=len(text))
49
50
51
def _make_store():
52
store = MagicMock()
53
store.query.return_value = MagicMock(result_set=[])
54
return store
55
56
57
def _make_parser():
58
from navegador.ingestion.hcl import HCLParser
59
60
parser = HCLParser.__new__(HCLParser)
61
parser._parser = MagicMock()
62
return parser
63
64
65
class TestHCLGetLanguage:
66
def test_raises_when_not_installed(self):
67
from navegador.ingestion.hcl import _get_hcl_language
68
69
with patch.dict(
70
"sys.modules",
71
{
72
"tree_sitter_hcl": None,
73
"tree_sitter": None,
74
},
75
):
76
with pytest.raises(ImportError, match="tree-sitter-hcl"):
77
_get_hcl_language()
78
79
def test_returns_language_object(self):
80
from navegador.ingestion.hcl import _get_hcl_language
81
82
mock_tshcl = MagicMock()
83
mock_ts = MagicMock()
84
with patch.dict(
85
"sys.modules",
86
{
87
"tree_sitter_hcl": mock_tshcl,
88
"tree_sitter": mock_ts,
89
},
90
):
91
result = _get_hcl_language()
92
assert result is mock_ts.Language.return_value
93
94
95
class TestHCLNodeText:
96
def test_extracts_bytes(self):
97
from navegador.ingestion.hcl import _node_text
98
99
source = b'resource "aws_instance" "web" {}'
100
node = MockNode("identifier", start_byte=10, end_byte=22)
101
assert _node_text(node, source) == "aws_instance"
102
103
104
class TestHCLHandleResource:
105
def test_creates_class_node_with_semantic_type(self):
106
parser = _make_parser()
107
store = _make_store()
108
source = b'resource "aws_instance" "web" {}'
109
node = MockNode(
110
"block",
111
start_point=(0, 0),
112
end_point=(0, 30),
113
)
114
labels = ["aws_instance", "web"]
115
stats = {"functions": 0, "classes": 0, "edges": 0}
116
parser._handle_resource(node, source, "main.tf", store, stats, labels, None)
117
assert stats["classes"] == 1
118
assert stats["edges"] == 1
119
store.create_node.assert_called_once()
120
label = store.create_node.call_args[0][0]
121
props = store.create_node.call_args[0][1]
122
assert label == NodeLabel.Class
123
assert props["name"] == "aws_instance.web"
124
assert props["semantic_type"] == "terraform_resource"
125
126
def test_extracts_references_from_body(self):
127
parser = _make_parser()
128
store = _make_store()
129
source = b"var.region"
130
body = MockNode("body", start_byte=0, end_byte=10)
131
node = MockNode(
132
"block",
133
start_point=(0, 0),
134
end_point=(0, 30),
135
)
136
labels = ["aws_instance", "web"]
137
stats = {"functions": 0, "classes": 0, "edges": 0}
138
parser._handle_resource(node, source, "main.tf", store, stats, labels, body)
139
# 1 CONTAINS edge + 1 REFERENCES edge from var.region
140
assert stats["edges"] == 2
141
142
143
class TestHCLHandleVariable:
144
def test_creates_variable_node(self):
145
parser = _make_parser()
146
store = _make_store()
147
source = b'variable "region" {}'
148
node = MockNode(
149
"block",
150
start_point=(0, 0),
151
end_point=(0, 19),
152
)
153
labels = ["region"]
154
stats = {"functions": 0, "classes": 0, "edges": 0}
155
parser._handle_variable(node, source, "vars.tf", store, stats, labels, None)
156
assert stats["functions"] == 1
157
assert stats["edges"] == 1
158
label = store.create_node.call_args[0][0]
159
props = store.create_node.call_args[0][1]
160
assert label == NodeLabel.Variable
161
assert props["name"] == "region"
162
assert props["semantic_type"] == "terraform_variable"
163
164
165
class TestHCLHandleModule:
166
def test_creates_module_node(self):
167
parser = _make_parser()
168
store = _make_store()
169
source = b'module "vpc" {}'
170
node = MockNode(
171
"block",
172
start_point=(0, 0),
173
end_point=(0, 14),
174
)
175
labels = ["vpc"]
176
stats = {"functions": 0, "classes": 0, "edges": 0}
177
parser._handle_module(node, source, "main.tf", store, stats, labels, None)
178
assert stats["classes"] == 1
179
assert stats["edges"] == 1
180
label = store.create_node.call_args[0][0]
181
props = store.create_node.call_args[0][1]
182
assert label == NodeLabel.Module
183
assert props["name"] == "vpc"
184
assert props["semantic_type"] == "terraform_module"
185
186
def test_extracts_source_attribute(self):
187
parser = _make_parser()
188
store = _make_store()
189
full_src = b"source./modules/vpc"
190
ident_node = MockNode(
191
"identifier",
192
start_byte=0,
193
end_byte=6,
194
)
195
expr_node = MockNode(
196
"expression",
197
start_byte=6,
198
end_byte=19,
199
)
200
expr_node.is_named = True
201
attr_node = MockNode(
202
"attribute",
203
children=[ident_node, expr_node],
204
)
205
body_node = MockNode("body", children=[attr_node])
206
node = MockNode(
207
"block",
208
start_point=(0, 0),
209
end_point=(0, 30),
210
)
211
labels = ["vpc"]
212
stats = {"functions": 0, "classes": 0, "edges": 0}
213
parser._handle_module(node, full_src, "main.tf", store, stats, labels, body_node)
214
props = store.create_node.call_args[0][1]
215
assert props["source"] == "./modules/vpc"
216
217
218
class TestHCLHandleOutput:
219
def test_creates_variable_node(self):
220
parser = _make_parser()
221
store = _make_store()
222
source = b'output "vpc_id" {}'
223
node = MockNode(
224
"block",
225
start_point=(0, 0),
226
end_point=(0, 17),
227
)
228
labels = ["vpc_id"]
229
stats = {"functions": 0, "classes": 0, "edges": 0}
230
parser._handle_output(node, source, "outputs.tf", store, stats, labels, None)
231
assert stats["functions"] == 1
232
assert stats["edges"] == 1
233
label = store.create_node.call_args[0][0]
234
props = store.create_node.call_args[0][1]
235
assert label == NodeLabel.Variable
236
assert props["semantic_type"] == "terraform_output"
237
238
def test_extracts_references_from_body(self):
239
parser = _make_parser()
240
store = _make_store()
241
source = b"module.vpc"
242
body = MockNode("body", start_byte=0, end_byte=10)
243
node = MockNode(
244
"block",
245
start_point=(0, 0),
246
end_point=(0, 17),
247
)
248
labels = ["vpc_id"]
249
stats = {"functions": 0, "classes": 0, "edges": 0}
250
parser._handle_output(node, source, "outputs.tf", store, stats, labels, body)
251
# 1 CONTAINS + 1 REFERENCES (module.vpc)
252
assert stats["edges"] == 2
253
254
255
class TestHCLHandleProvider:
256
def test_creates_class_node(self):
257
parser = _make_parser()
258
store = _make_store()
259
source = b'provider "aws" {}'
260
node = MockNode(
261
"block",
262
start_point=(0, 0),
263
end_point=(0, 16),
264
)
265
labels = ["aws"]
266
stats = {"functions": 0, "classes": 0, "edges": 0}
267
parser._handle_provider(node, source, "provider.tf", store, stats, labels, None)
268
assert stats["classes"] == 1
269
assert stats["edges"] == 1
270
label = store.create_node.call_args[0][0]
271
props = store.create_node.call_args[0][1]
272
assert label == NodeLabel.Class
273
assert props["name"] == "aws"
274
assert props["semantic_type"] == "terraform_provider"
275
276
277
class TestHCLHandleLocals:
278
def test_creates_variable_nodes(self):
279
parser = _make_parser()
280
store = _make_store()
281
source = b"region"
282
ident = MockNode(
283
"identifier",
284
start_byte=0,
285
end_byte=6,
286
)
287
attr = MockNode(
288
"attribute",
289
children=[ident],
290
start_point=(1, 0),
291
end_point=(1, 20),
292
)
293
body = MockNode("body", children=[attr])
294
node = MockNode(
295
"block",
296
start_point=(0, 0),
297
end_point=(2, 1),
298
)
299
stats = {"functions": 0, "classes": 0, "edges": 0}
300
parser._handle_locals(node, source, "locals.tf", store, stats, body)
301
assert stats["functions"] == 1
302
assert stats["edges"] >= 1
303
label = store.create_node.call_args[0][0]
304
props = store.create_node.call_args[0][1]
305
assert label == NodeLabel.Variable
306
assert props["semantic_type"] == "terraform_local"
307
308
def test_skips_when_no_body(self):
309
parser = _make_parser()
310
store = _make_store()
311
node = MockNode("block", start_point=(0, 0), end_point=(0, 5))
312
stats = {"functions": 0, "classes": 0, "edges": 0}
313
parser._handle_locals(node, b"", "locals.tf", store, stats, None)
314
assert stats["functions"] == 0
315
store.create_node.assert_not_called()
316
317
318
class TestHCLWalkDispatch:
319
def test_walk_dispatches_block_in_body(self):
320
parser = _make_parser()
321
store = _make_store()
322
# Build: root > body > block(variable "region")
323
source = b'variable "region" {}'
324
ident = MockNode(
325
"identifier",
326
start_byte=0,
327
end_byte=8,
328
)
329
string_lit_inner = MockNode(
330
"template_literal",
331
start_byte=10,
332
end_byte=16,
333
)
334
string_lit = MockNode(
335
"string_lit",
336
children=[string_lit_inner],
337
start_byte=9,
338
end_byte=17,
339
)
340
block = MockNode(
341
"block",
342
children=[ident, string_lit],
343
start_point=(0, 0),
344
end_point=(0, 19),
345
)
346
body = MockNode("body", children=[block])
347
root = MockNode("config_file", children=[body])
348
stats = {"functions": 0, "classes": 0, "edges": 0}
349
parser._walk(root, source, "vars.tf", store, stats)
350
assert stats["functions"] == 1
351
352
def test_walk_dispatches_top_level_block(self):
353
parser = _make_parser()
354
store = _make_store()
355
source = b'provider "aws" {}'
356
ident = MockNode(
357
"identifier",
358
start_byte=0,
359
end_byte=8,
360
)
361
string_lit_inner = MockNode(
362
"template_literal",
363
start_byte=10,
364
end_byte=13,
365
)
366
string_lit = MockNode(
367
"string_lit",
368
children=[string_lit_inner],
369
start_byte=9,
370
end_byte=14,
371
)
372
block = MockNode(
373
"block",
374
children=[ident, string_lit],
375
start_point=(0, 0),
376
end_point=(0, 16),
377
)
378
root = MockNode("config_file", children=[block])
379
stats = {"functions": 0, "classes": 0, "edges": 0}
380
parser._walk(root, source, "main.tf", store, stats)
381
assert stats["classes"] == 1
382
383
384
class TestHCLExtractReferences:
385
def test_finds_var_reference(self):
386
parser = _make_parser()
387
store = _make_store()
388
source = b"var.region"
389
node = MockNode("body", start_byte=0, end_byte=10)
390
stats = {"functions": 0, "classes": 0, "edges": 0}
391
parser._extract_references(
392
node,
393
source,
394
"main.tf",
395
"aws_instance.web",
396
NodeLabel.Class,
397
store,
398
stats,
399
)
400
assert stats["edges"] == 1
401
edge_call = store.create_edge.call_args[0]
402
assert edge_call[2] == EdgeType.REFERENCES
403
assert edge_call[4]["name"] == "region"
404
405
def test_finds_resource_reference(self):
406
parser = _make_parser()
407
store = _make_store()
408
source = b"aws_security_group.default"
409
node = MockNode("body", start_byte=0, end_byte=25)
410
stats = {"functions": 0, "classes": 0, "edges": 0}
411
parser._extract_references(
412
node,
413
source,
414
"main.tf",
415
"aws_instance.web",
416
NodeLabel.Class,
417
store,
418
stats,
419
)
420
assert stats["edges"] == 1
421
edge_call = store.create_edge.call_args[0]
422
assert edge_call[2] == EdgeType.DEPENDS_ON
423
424
def test_finds_local_reference(self):
425
parser = _make_parser()
426
store = _make_store()
427
source = b"local.common_tags"
428
node = MockNode("body", start_byte=0, end_byte=17)
429
stats = {"functions": 0, "classes": 0, "edges": 0}
430
parser._extract_references(
431
node,
432
source,
433
"main.tf",
434
"aws_instance.web",
435
NodeLabel.Class,
436
store,
437
stats,
438
)
439
assert stats["edges"] == 1
440
441
def test_finds_module_reference(self):
442
parser = _make_parser()
443
store = _make_store()
444
source = b"module.vpc"
445
node = MockNode("body", start_byte=0, end_byte=10)
446
stats = {"functions": 0, "classes": 0, "edges": 0}
447
parser._extract_references(
448
node,
449
source,
450
"main.tf",
451
"output_vpc",
452
NodeLabel.Variable,
453
store,
454
stats,
455
)
456
assert stats["edges"] == 1
457
edge_call = store.create_edge.call_args[0]
458
assert edge_call[3] == NodeLabel.Module
459
460
def test_finds_data_reference(self):
461
parser = _make_parser()
462
store = _make_store()
463
source = b"data.http.myip"
464
node = MockNode("body", start_byte=0, end_byte=14)
465
stats = {"functions": 0, "classes": 0, "edges": 0}
466
parser._extract_references(
467
node,
468
source,
469
"main.tf",
470
"aws_instance.web",
471
NodeLabel.Class,
472
store,
473
stats,
474
)
475
assert stats["edges"] == 1
476
edge_call = store.create_edge.call_args[0]
477
assert edge_call[2] == EdgeType.DEPENDS_ON
478
assert edge_call[4]["name"] == "http.myip"
479
480
481
class TestHCLParseFile:
482
def test_creates_file_node(self):
483
import tempfile
484
from pathlib import Path
485
486
parser = _make_parser()
487
store = _make_store()
488
mock_tree = MagicMock()
489
mock_tree.root_node.type = "config_file"
490
mock_tree.root_node.children = []
491
parser._parser.parse.return_value = mock_tree
492
with tempfile.NamedTemporaryFile(suffix=".tf", delete=False) as f:
493
f.write(b'resource "aws_instance" "web" {}\n')
494
fpath = Path(f.name)
495
try:
496
parser.parse_file(fpath, fpath.parent, store)
497
store.create_node.assert_called_once()
498
label = store.create_node.call_args[0][0]
499
props = store.create_node.call_args[0][1]
500
assert label == NodeLabel.File
501
assert props["language"] == "hcl"
502
finally:
503
fpath.unlink()
504

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button