|
1
|
""" |
|
2
|
Targeted tests to boost coverage from ~91% to 95%+. |
|
3
|
|
|
4
|
One test class per module. All external dependencies (Redis, tree-sitter, |
|
5
|
HTTP) are mocked; no real infrastructure is required. |
|
6
|
""" |
|
7
|
|
|
8
|
from __future__ import annotations |
|
9
|
|
|
10
|
import json |
|
11
|
import tempfile |
|
12
|
from pathlib import Path |
|
13
|
from unittest.mock import MagicMock, patch |
|
14
|
|
|
15
|
import pytest |
|
16
|
|
|
17
|
|
|
18
|
# ── Helpers ─────────────────────────────────────────────────────────────────── |
|
19
|
|
|
20
|
|
|
21
|
def _make_store(): |
|
22
|
store = MagicMock() |
|
23
|
store.query.return_value = MagicMock(result_set=[]) |
|
24
|
store.node_count.return_value = 0 |
|
25
|
store.edge_count.return_value = 0 |
|
26
|
return store |
|
27
|
|
|
28
|
|
|
29
|
def _write(path: Path, content: str) -> None: |
|
30
|
path.parent.mkdir(parents=True, exist_ok=True) |
|
31
|
path.write_text(content, encoding="utf-8") |
|
32
|
|
|
33
|
|
|
34
|
# ── MockNode for AST tests ──────────────────────────────────────────────────── |
|
35
|
|
|
36
|
|
|
37
|
class MockNode: |
|
38
|
def __init__( |
|
39
|
self, |
|
40
|
type_: str, |
|
41
|
text: bytes = b"", |
|
42
|
children: list = None, |
|
43
|
start_byte: int = 0, |
|
44
|
end_byte: int = 0, |
|
45
|
start_point: tuple = (0, 0), |
|
46
|
end_point: tuple = (5, 0), |
|
47
|
): |
|
48
|
self.type = type_ |
|
49
|
self.children = children or [] |
|
50
|
self.start_byte = start_byte |
|
51
|
self.end_byte = end_byte |
|
52
|
self.start_point = start_point |
|
53
|
self.end_point = end_point |
|
54
|
self._fields: dict = {} |
|
55
|
|
|
56
|
def child_by_field_name(self, name: str): |
|
57
|
return self._fields.get(name) |
|
58
|
|
|
59
|
def set_field(self, name: str, node: "MockNode") -> "MockNode": |
|
60
|
self._fields[name] = node |
|
61
|
return self |
|
62
|
|
|
63
|
|
|
64
|
def _text_node(text: bytes, type_: str = "identifier") -> MockNode: |
|
65
|
return MockNode(type_, text, start_byte=0, end_byte=len(text)) |
|
66
|
|
|
67
|
|
|
68
|
def _make_mock_tree(root_node: MockNode): |
|
69
|
tree = MagicMock() |
|
70
|
tree.root_node = root_node |
|
71
|
return tree |
|
72
|
|
|
73
|
|
|
74
|
def _mock_ts(lang_module_name: str): |
|
75
|
mock_lang_module = MagicMock() |
|
76
|
mock_ts = MagicMock() |
|
77
|
return patch.dict("sys.modules", {lang_module_name: mock_lang_module, "tree_sitter": mock_ts}) |
|
78
|
|
|
79
|
|
|
80
|
# =========================================================================== |
|
81
|
# navegador.api_schema (57% → target ~90%) |
|
82
|
# =========================================================================== |
|
83
|
|
|
84
|
|
|
85
|
class TestAPISchemaIngesterOpenAPI: |
|
86
|
"""Cover lines 72, 105, 213, 217-225, 234-241, 255-294, 299-319.""" |
|
87
|
|
|
88
|
def _make(self): |
|
89
|
from navegador.api_schema import APISchemaIngester |
|
90
|
|
|
91
|
return APISchemaIngester(_make_store()) |
|
92
|
|
|
93
|
def test_ingest_openapi_yaml_with_paths(self, tmp_path): |
|
94
|
content = """ |
|
95
|
openapi: 3.0.0 |
|
96
|
info: |
|
97
|
title: Test |
|
98
|
paths: |
|
99
|
/users: |
|
100
|
get: |
|
101
|
operationId: listUsers |
|
102
|
summary: List users |
|
103
|
tags: |
|
104
|
- users |
|
105
|
post: |
|
106
|
summary: Create user |
|
107
|
components: |
|
108
|
schemas: |
|
109
|
User: |
|
110
|
type: object |
|
111
|
description: A user object |
|
112
|
""" |
|
113
|
f = tmp_path / "api.yaml" |
|
114
|
f.write_text(content) |
|
115
|
ingester = self._make() |
|
116
|
stats = ingester.ingest_openapi(str(f)) |
|
117
|
assert stats["endpoints"] >= 2 |
|
118
|
assert stats["schemas"] >= 1 |
|
119
|
|
|
120
|
def test_ingest_openapi_json(self, tmp_path): |
|
121
|
spec = { |
|
122
|
"openapi": "3.0.0", |
|
123
|
"paths": { |
|
124
|
"/items": { |
|
125
|
"get": {"operationId": "listItems", "summary": "List"}, |
|
126
|
"delete": {"summary": "Delete"}, |
|
127
|
} |
|
128
|
}, |
|
129
|
"components": {"schemas": {"Item": {"description": "item"}}}, |
|
130
|
} |
|
131
|
f = tmp_path / "api.json" |
|
132
|
f.write_text(json.dumps(spec)) |
|
133
|
ingester = self._make() |
|
134
|
stats = ingester.ingest_openapi(str(f)) |
|
135
|
assert stats["endpoints"] >= 2 |
|
136
|
assert stats["schemas"] >= 1 |
|
137
|
|
|
138
|
def test_ingest_openapi_missing_file(self, tmp_path): |
|
139
|
ingester = self._make() |
|
140
|
stats = ingester.ingest_openapi(str(tmp_path / "missing.yaml")) |
|
141
|
assert stats == {"endpoints": 0, "schemas": 0} |
|
142
|
|
|
143
|
def test_ingest_openapi_swagger2_definitions(self, tmp_path): |
|
144
|
spec = { |
|
145
|
"swagger": "2.0", |
|
146
|
"paths": { |
|
147
|
"/pets": {"get": {"summary": "List pets"}} |
|
148
|
}, |
|
149
|
"definitions": {"Pet": {"description": "pet"}}, |
|
150
|
} |
|
151
|
f = tmp_path / "swagger.json" |
|
152
|
f.write_text(json.dumps(spec)) |
|
153
|
ingester = self._make() |
|
154
|
stats = ingester.ingest_openapi(str(f)) |
|
155
|
assert stats["schemas"] >= 1 |
|
156
|
|
|
157
|
def test_ingest_openapi_operation_no_id(self, tmp_path): |
|
158
|
# operationId absent → synthesised as "METHOD /path" |
|
159
|
spec = { |
|
160
|
"paths": {"/x": {"put": {"summary": "update"}}}, |
|
161
|
} |
|
162
|
f = tmp_path / "api.json" |
|
163
|
f.write_text(json.dumps(spec)) |
|
164
|
ingester = self._make() |
|
165
|
stats = ingester.ingest_openapi(str(f)) |
|
166
|
assert stats["endpoints"] == 1 |
|
167
|
|
|
168
|
def test_ingest_openapi_invalid_json(self, tmp_path): |
|
169
|
f = tmp_path / "bad.json" |
|
170
|
f.write_text("{not valid json") |
|
171
|
ingester = self._make() |
|
172
|
stats = ingester.ingest_openapi(str(f)) |
|
173
|
assert stats == {"endpoints": 0, "schemas": 0} |
|
174
|
|
|
175
|
def test_ingest_openapi_unknown_extension_tries_json(self, tmp_path): |
|
176
|
spec = {"paths": {"/x": {"get": {"summary": "hi"}}}} |
|
177
|
f = tmp_path / "api.txt" |
|
178
|
f.write_text(json.dumps(spec)) |
|
179
|
ingester = self._make() |
|
180
|
stats = ingester.ingest_openapi(str(f)) |
|
181
|
assert stats["endpoints"] == 1 |
|
182
|
|
|
183
|
def test_ingest_openapi_unknown_extension_falls_back_to_yaml(self, tmp_path): |
|
184
|
# Not valid JSON → falls back to _parse_yaml |
|
185
|
f = tmp_path / "api.txt" |
|
186
|
f.write_text("paths:\n /x:\n get:\n summary: hi\n") |
|
187
|
ingester = self._make() |
|
188
|
# Should not raise |
|
189
|
ingester.ingest_openapi(str(f)) |
|
190
|
|
|
191
|
|
|
192
|
class TestAPISchemaIngesterGraphQL: |
|
193
|
def _make(self): |
|
194
|
from navegador.api_schema import APISchemaIngester |
|
195
|
|
|
196
|
return APISchemaIngester(_make_store()) |
|
197
|
|
|
198
|
def test_ingest_graphql_types_and_fields(self, tmp_path): |
|
199
|
sdl = """ |
|
200
|
type Query { |
|
201
|
user(id: ID!): User |
|
202
|
users: [User] |
|
203
|
} |
|
204
|
type Mutation { |
|
205
|
createUser(name: String!): User |
|
206
|
} |
|
207
|
type User { |
|
208
|
id: ID |
|
209
|
name: String |
|
210
|
} |
|
211
|
input CreateUserInput { |
|
212
|
name: String! |
|
213
|
} |
|
214
|
""" |
|
215
|
f = tmp_path / "schema.graphql" |
|
216
|
f.write_text(sdl) |
|
217
|
ingester = self._make() |
|
218
|
stats = ingester.ingest_graphql(str(f)) |
|
219
|
assert stats["fields"] >= 2 # Query fields |
|
220
|
assert stats["types"] >= 2 # User + CreateUserInput |
|
221
|
|
|
222
|
def test_ingest_graphql_missing_file(self, tmp_path): |
|
223
|
ingester = self._make() |
|
224
|
stats = ingester.ingest_graphql(str(tmp_path / "missing.graphql")) |
|
225
|
assert stats == {"types": 0, "fields": 0} |
|
226
|
|
|
227
|
def test_ingest_graphql_empty_schema(self, tmp_path): |
|
228
|
f = tmp_path / "empty.graphql" |
|
229
|
f.write_text("# just a comment") |
|
230
|
ingester = self._make() |
|
231
|
stats = ingester.ingest_graphql(str(f)) |
|
232
|
assert stats["types"] == 0 |
|
233
|
assert stats["fields"] == 0 |
|
234
|
|
|
235
|
|
|
236
|
class TestMinimalYamlLoad: |
|
237
|
def test_simple_key_value(self): |
|
238
|
from navegador.api_schema import _minimal_yaml_load |
|
239
|
|
|
240
|
result = _minimal_yaml_load("title: My API\nversion: 3") |
|
241
|
assert result.get("title") == "My API" |
|
242
|
|
|
243
|
def test_boolean_scalars(self): |
|
244
|
from navegador.api_schema import _yaml_scalar |
|
245
|
|
|
246
|
assert _yaml_scalar("true") is True |
|
247
|
assert _yaml_scalar("false") is False |
|
248
|
assert _yaml_scalar("yes") is True |
|
249
|
assert _yaml_scalar("no") is False |
|
250
|
|
|
251
|
def test_null_scalars(self): |
|
252
|
from navegador.api_schema import _yaml_scalar |
|
253
|
|
|
254
|
assert _yaml_scalar("null") is None |
|
255
|
assert _yaml_scalar("~") is None |
|
256
|
assert _yaml_scalar("") is None |
|
257
|
|
|
258
|
def test_quoted_strings(self): |
|
259
|
from navegador.api_schema import _yaml_scalar |
|
260
|
|
|
261
|
assert _yaml_scalar('"hello"') == "hello" |
|
262
|
assert _yaml_scalar("'world'") == "world" |
|
263
|
|
|
264
|
def test_int_float(self): |
|
265
|
from navegador.api_schema import _yaml_scalar |
|
266
|
|
|
267
|
assert _yaml_scalar("42") == 42 |
|
268
|
assert _yaml_scalar("3.14") == pytest.approx(3.14) |
|
269
|
|
|
270
|
def test_bare_string(self): |
|
271
|
from navegador.api_schema import _yaml_scalar |
|
272
|
|
|
273
|
assert _yaml_scalar("application/json") == "application/json" |
|
274
|
|
|
275
|
def test_list_items(self): |
|
276
|
from navegador.api_schema import _minimal_yaml_load |
|
277
|
|
|
278
|
text = "tags:\n - users\n - admin\n" |
|
279
|
# Should not raise even if list parsing is minimal |
|
280
|
result = _minimal_yaml_load(text) |
|
281
|
assert isinstance(result, dict) |
|
282
|
|
|
283
|
def test_comments_skipped(self): |
|
284
|
from navegador.api_schema import _minimal_yaml_load |
|
285
|
|
|
286
|
text = "# comment\ntitle: test\n" |
|
287
|
result = _minimal_yaml_load(text) |
|
288
|
assert result.get("title") == "test" |
|
289
|
|
|
290
|
def test_block_scalar_placeholder(self): |
|
291
|
from navegador.api_schema import _minimal_yaml_load |
|
292
|
|
|
293
|
text = "description: |\n some text\ntitle: test\n" |
|
294
|
result = _minimal_yaml_load(text) |
|
295
|
# Should have a nested dict for the block scalar key |
|
296
|
assert "description" in result |
|
297
|
|
|
298
|
def test_parse_yaml_uses_pyyaml_if_available(self, tmp_path): |
|
299
|
from navegador.api_schema import APISchemaIngester |
|
300
|
|
|
301
|
ingester = APISchemaIngester(_make_store()) |
|
302
|
mock_yaml = MagicMock() |
|
303
|
mock_yaml.safe_load.return_value = {"openapi": "3.0.0", "paths": {}} |
|
304
|
with patch.dict("sys.modules", {"yaml": mock_yaml}): |
|
305
|
result = ingester._parse_yaml("openapi: 3.0.0") |
|
306
|
assert result == {"openapi": "3.0.0", "paths": {}} |
|
307
|
|
|
308
|
def test_parse_yaml_falls_back_when_no_pyyaml(self): |
|
309
|
from navegador.api_schema import APISchemaIngester |
|
310
|
|
|
311
|
ingester = APISchemaIngester(_make_store()) |
|
312
|
import sys |
|
313
|
|
|
314
|
original = sys.modules.pop("yaml", None) |
|
315
|
try: |
|
316
|
# Simulate no yaml installed |
|
317
|
with patch.dict("sys.modules", {"yaml": None}): |
|
318
|
result = ingester._parse_yaml("title: test\n") |
|
319
|
assert isinstance(result, dict) |
|
320
|
finally: |
|
321
|
if original is not None: |
|
322
|
sys.modules["yaml"] = original |
|
323
|
|
|
324
|
|
|
325
|
# =========================================================================== |
|
326
|
# navegador.cluster.core (50% → target ~85%) |
|
327
|
# =========================================================================== |
|
328
|
|
|
329
|
|
|
330
|
class TestClusterManagerLocalVersion: |
|
331
|
"""Cover _local_version, _set_local_version, snapshot_to_local, push_to_shared, sync.""" |
|
332
|
|
|
333
|
def _make(self, tmp_path): |
|
334
|
from navegador.cluster.core import ClusterManager |
|
335
|
|
|
336
|
r = MagicMock() |
|
337
|
pipe = MagicMock() |
|
338
|
pipe.execute.return_value = [True, True, True] |
|
339
|
r.pipeline.return_value = pipe |
|
340
|
r.get.return_value = b"3" |
|
341
|
return ClusterManager( |
|
342
|
"redis://localhost:6379", |
|
343
|
local_db_path=str(tmp_path / "graph.db"), |
|
344
|
redis_client=r, |
|
345
|
), r, pipe |
|
346
|
|
|
347
|
def test_local_version_zero_when_no_meta(self, tmp_path): |
|
348
|
mgr, _, _ = self._make(tmp_path) |
|
349
|
assert mgr._local_version() == 0 |
|
350
|
|
|
351
|
def test_set_and_read_local_version(self, tmp_path): |
|
352
|
mgr, _, _ = self._make(tmp_path) |
|
353
|
mgr._set_local_version(7) |
|
354
|
assert mgr._local_version() == 7 |
|
355
|
|
|
356
|
def test_set_local_version_merges_existing_keys(self, tmp_path): |
|
357
|
mgr, _, _ = self._make(tmp_path) |
|
358
|
mgr._set_local_version(1) |
|
359
|
mgr._set_local_version(2) |
|
360
|
assert mgr._local_version() == 2 |
|
361
|
|
|
362
|
def test_local_version_handles_corrupt_json(self, tmp_path): |
|
363
|
mgr, _, _ = self._make(tmp_path) |
|
364
|
meta = tmp_path / ".navegador" / "cluster_meta.json" |
|
365
|
meta.parent.mkdir(parents=True, exist_ok=True) |
|
366
|
meta.write_text("{bad json") |
|
367
|
# Should fall back to 0 |
|
368
|
# local_db_path is tmp_path/graph.db, so meta is tmp_path/cluster_meta.json |
|
369
|
mgr2, _, _ = self._make(tmp_path) |
|
370
|
assert mgr2._local_version() == 0 |
|
371
|
|
|
372
|
def test_snapshot_to_local_no_snapshot(self, tmp_path): |
|
373
|
mgr, r, _ = self._make(tmp_path) |
|
374
|
r.get.return_value = None # no snapshot key |
|
375
|
# Should log warning and return without error |
|
376
|
with patch("navegador.cluster.core.logger") as mock_log: |
|
377
|
mgr.snapshot_to_local() |
|
378
|
mock_log.warning.assert_called_once() |
|
379
|
|
|
380
|
def test_snapshot_to_local_imports_data(self, tmp_path): |
|
381
|
mgr, r, _ = self._make(tmp_path) |
|
382
|
snapshot_data = {"nodes": [], "edges": []} |
|
383
|
# First call returns version key, subsequent get returns snapshot |
|
384
|
r.get.side_effect = [json.dumps(snapshot_data).encode(), b"5"] |
|
385
|
|
|
386
|
with patch.object(mgr, "_import_to_local_graph") as mock_import: |
|
387
|
with patch.object(mgr, "_set_local_version") as mock_set: |
|
388
|
with patch.object(mgr, "_redis_version", return_value=5): |
|
389
|
r.get.side_effect = None |
|
390
|
r.get.return_value = json.dumps(snapshot_data).encode() |
|
391
|
mgr.snapshot_to_local() |
|
392
|
mock_import.assert_called_once_with(snapshot_data) |
|
393
|
|
|
394
|
def test_push_to_shared(self, tmp_path): |
|
395
|
mgr, r, pipe = self._make(tmp_path) |
|
396
|
r.get.return_value = b"2" |
|
397
|
|
|
398
|
with patch.object(mgr, "_export_local_graph", return_value={"nodes": [], "edges": []}): |
|
399
|
mgr.push_to_shared() |
|
400
|
|
|
401
|
pipe.set.assert_called() |
|
402
|
pipe.execute.assert_called_once() |
|
403
|
|
|
404
|
def test_sync_pulls_when_shared_newer(self, tmp_path): |
|
405
|
mgr, r, _ = self._make(tmp_path) |
|
406
|
with patch.object(mgr, "_local_version", return_value=1): |
|
407
|
with patch.object(mgr, "_redis_version", return_value=5): |
|
408
|
with patch.object(mgr, "snapshot_to_local") as mock_pull: |
|
409
|
mgr.sync() |
|
410
|
mock_pull.assert_called_once() |
|
411
|
|
|
412
|
def test_sync_pushes_when_local_current(self, tmp_path): |
|
413
|
mgr, r, _ = self._make(tmp_path) |
|
414
|
with patch.object(mgr, "_local_version", return_value=5): |
|
415
|
with patch.object(mgr, "_redis_version", return_value=3): |
|
416
|
with patch.object(mgr, "push_to_shared") as mock_push: |
|
417
|
mgr.sync() |
|
418
|
mock_push.assert_called_once() |
|
419
|
|
|
420
|
def test_connect_redis_raises_on_missing_dep(self): |
|
421
|
from navegador.cluster.core import ClusterManager |
|
422
|
|
|
423
|
with patch.dict("sys.modules", {"redis": None}): |
|
424
|
with pytest.raises(ImportError, match="redis"): |
|
425
|
ClusterManager._connect_redis("redis://localhost") |
|
426
|
|
|
427
|
def test_redis_version_returns_zero_when_none(self, tmp_path): |
|
428
|
mgr, r, _ = self._make(tmp_path) |
|
429
|
r.get.return_value = None |
|
430
|
assert mgr._redis_version() == 0 |
|
431
|
|
|
432
|
def test_export_local_graph_calls_store(self, tmp_path): |
|
433
|
mgr, r, _ = self._make(tmp_path) |
|
434
|
mock_store = MagicMock() |
|
435
|
nodes_result = MagicMock() |
|
436
|
nodes_result.result_set = [] |
|
437
|
edges_result = MagicMock() |
|
438
|
edges_result.result_set = [] |
|
439
|
mock_store.query.side_effect = [nodes_result, edges_result] |
|
440
|
|
|
441
|
with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store): |
|
442
|
data = mgr._export_local_graph() |
|
443
|
assert data == {"nodes": [], "edges": []} |
|
444
|
|
|
445
|
def test_import_to_local_graph_creates_nodes(self, tmp_path): |
|
446
|
mgr, r, _ = self._make(tmp_path) |
|
447
|
mock_store = MagicMock() |
|
448
|
|
|
449
|
data = { |
|
450
|
"nodes": [{"labels": ["Function"], "properties": {"name": "foo"}}], |
|
451
|
"edges": [ |
|
452
|
{ |
|
453
|
"src_labels": ["Function"], |
|
454
|
"src_props": {"name": "foo", "file_path": "f.py"}, |
|
455
|
"rel_type": "CALLS", |
|
456
|
"dst_labels": ["Function"], |
|
457
|
"dst_props": {"name": "bar", "file_path": "f.py"}, |
|
458
|
"rel_props": {}, |
|
459
|
} |
|
460
|
], |
|
461
|
} |
|
462
|
with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store): |
|
463
|
mgr._import_to_local_graph(data) |
|
464
|
mock_store.create_node.assert_called_once() |
|
465
|
mock_store.create_edge.assert_called_once() |
|
466
|
|
|
467
|
def test_import_to_local_graph_skips_edge_without_src_key(self, tmp_path): |
|
468
|
mgr, r, _ = self._make(tmp_path) |
|
469
|
mock_store = MagicMock() |
|
470
|
|
|
471
|
data = { |
|
472
|
"nodes": [], |
|
473
|
"edges": [ |
|
474
|
{ |
|
475
|
"src_labels": ["Function"], |
|
476
|
"src_props": {}, # no name/file_path → no key |
|
477
|
"rel_type": "CALLS", |
|
478
|
"dst_labels": ["Function"], |
|
479
|
"dst_props": {"name": "bar", "file_path": "f.py"}, |
|
480
|
"rel_props": {}, |
|
481
|
} |
|
482
|
], |
|
483
|
} |
|
484
|
with patch("navegador.graph.store.GraphStore.sqlite", return_value=mock_store): |
|
485
|
mgr._import_to_local_graph(data) |
|
486
|
mock_store.create_edge.assert_not_called() |
|
487
|
|
|
488
|
|
|
489
|
# =========================================================================== |
|
490
|
# navegador.monorepo (76% → target ~90%) |
|
491
|
# =========================================================================== |
|
492
|
|
|
493
|
|
|
494
|
class TestWorkspaceDetectorEdgeCases: |
|
495
|
"""Cover lines 89-90, 124-125, 128, 142-143, 165-166, 180-181, 193, |
|
496
|
223, 235-236, 253-255, 270-274, 288-289.""" |
|
497
|
|
|
498
|
def test_yarn_workspaces_berry_format(self, tmp_path): |
|
499
|
from navegador.monorepo import WorkspaceDetector |
|
500
|
|
|
501
|
pkg_json = { |
|
502
|
"name": "root", |
|
503
|
"workspaces": {"packages": ["packages/*"]}, |
|
504
|
} |
|
505
|
_write(tmp_path / "package.json", json.dumps(pkg_json)) |
|
506
|
(tmp_path / "packages" / "app").mkdir(parents=True) |
|
507
|
_write(tmp_path / "packages" / "app" / "package.json", '{"name":"app"}') |
|
508
|
config = WorkspaceDetector().detect(tmp_path) |
|
509
|
assert config is not None |
|
510
|
assert config.type == "yarn" |
|
511
|
|
|
512
|
def test_yarn_package_json_parse_error(self, tmp_path): |
|
513
|
from navegador.monorepo import WorkspaceDetector |
|
514
|
|
|
515
|
_write(tmp_path / "package.json", "{bad json") |
|
516
|
# No workspaces key (parse failed) → no yarn config returned |
|
517
|
config = WorkspaceDetector().detect(tmp_path) |
|
518
|
assert config is None |
|
519
|
|
|
520
|
def test_js_workspace_packages_berry_patterns(self, tmp_path): |
|
521
|
from navegador.monorepo import WorkspaceDetector |
|
522
|
|
|
523
|
pkg_json = {"workspaces": {"packages": ["packages/*"]}} |
|
524
|
_write(tmp_path / "package.json", json.dumps(pkg_json)) |
|
525
|
(tmp_path / "packages" / "a").mkdir(parents=True) |
|
526
|
det = WorkspaceDetector() |
|
527
|
packages = det._js_workspace_packages(tmp_path) |
|
528
|
assert any(p.name == "a" for p in packages) |
|
529
|
|
|
530
|
def test_js_workspace_packages_no_package_json(self, tmp_path): |
|
531
|
from navegador.monorepo import WorkspaceDetector |
|
532
|
|
|
533
|
# fallback: scan for package.json one level down |
|
534
|
(tmp_path / "pkg_a").mkdir() |
|
535
|
_write(tmp_path / "pkg_a" / "package.json", '{"name":"pkg_a"}') |
|
536
|
det = WorkspaceDetector() |
|
537
|
packages = det._js_workspace_packages(tmp_path) |
|
538
|
assert any(p.name == "pkg_a" for p in packages) |
|
539
|
|
|
540
|
def test_js_workspace_packages_parse_error(self, tmp_path): |
|
541
|
from navegador.monorepo import WorkspaceDetector |
|
542
|
|
|
543
|
_write(tmp_path / "package.json", "{bad json") |
|
544
|
# Falls back to _fallback_packages |
|
545
|
det = WorkspaceDetector() |
|
546
|
packages = det._js_workspace_packages(tmp_path) |
|
547
|
assert isinstance(packages, list) |
|
548
|
|
|
549
|
def test_nx_packages_from_subdirs(self, tmp_path): |
|
550
|
from navegador.monorepo import WorkspaceDetector |
|
551
|
|
|
552
|
_write(tmp_path / "nx.json", '{}') |
|
553
|
(tmp_path / "apps" / "app1").mkdir(parents=True) |
|
554
|
(tmp_path / "libs" / "lib1").mkdir(parents=True) |
|
555
|
config = WorkspaceDetector().detect(tmp_path) |
|
556
|
assert config is not None |
|
557
|
assert config.type == "nx" |
|
558
|
pkg_names = [p.name for p in config.packages] |
|
559
|
assert "app1" in pkg_names |
|
560
|
assert "lib1" in pkg_names |
|
561
|
|
|
562
|
def test_nx_packages_fallback_to_js_workspaces(self, tmp_path): |
|
563
|
from navegador.monorepo import WorkspaceDetector |
|
564
|
|
|
565
|
# nx.json exists but no apps/libs/packages dirs |
|
566
|
_write(tmp_path / "nx.json", '{}') |
|
567
|
# fallback triggers _js_workspace_packages → _fallback_packages |
|
568
|
config = WorkspaceDetector().detect(tmp_path) |
|
569
|
assert config is not None |
|
570
|
assert config.type == "nx" |
|
571
|
|
|
572
|
def test_pnpm_workspace_parse(self, tmp_path): |
|
573
|
from navegador.monorepo import WorkspaceDetector |
|
574
|
|
|
575
|
_write( |
|
576
|
tmp_path / "pnpm-workspace.yaml", |
|
577
|
"packages:\n - 'packages/*'\n - 'apps/*'\n", |
|
578
|
) |
|
579
|
(tmp_path / "packages" / "core").mkdir(parents=True) |
|
580
|
config = WorkspaceDetector().detect(tmp_path) |
|
581
|
assert config is not None |
|
582
|
assert config.type == "pnpm" |
|
583
|
|
|
584
|
def test_pnpm_workspace_read_error(self, tmp_path): |
|
585
|
from navegador.monorepo import WorkspaceDetector |
|
586
|
|
|
587
|
# pnpm-workspace.yaml exists but cannot be read → IOError path |
|
588
|
yaml_path = tmp_path / "pnpm-workspace.yaml" |
|
589
|
yaml_path.touch() |
|
590
|
det = WorkspaceDetector() |
|
591
|
with patch.object(Path, "read_text", side_effect=OSError("perm")): |
|
592
|
packages = det._pnpm_packages(tmp_path) |
|
593
|
assert isinstance(packages, list) |
|
594
|
|
|
595
|
def test_pnpm_no_patterns_fallback(self, tmp_path): |
|
596
|
from navegador.monorepo import WorkspaceDetector |
|
597
|
|
|
598
|
_write(tmp_path / "pnpm-workspace.yaml", "# empty\n") |
|
599
|
(tmp_path / "sub").mkdir() |
|
600
|
_write(tmp_path / "sub" / "package.json", '{"name":"sub"}') |
|
601
|
det = WorkspaceDetector() |
|
602
|
packages = det._pnpm_packages(tmp_path) |
|
603
|
assert any(p.name == "sub" for p in packages) |
|
604
|
|
|
605
|
def test_cargo_workspace_parse(self, tmp_path): |
|
606
|
from navegador.monorepo import WorkspaceDetector |
|
607
|
|
|
608
|
cargo_toml = """ |
|
609
|
[workspace] |
|
610
|
members = [ |
|
611
|
"crates/core", |
|
612
|
"crates/cli", |
|
613
|
] |
|
614
|
""" |
|
615
|
_write(tmp_path / "Cargo.toml", cargo_toml) |
|
616
|
(tmp_path / "crates" / "core").mkdir(parents=True) |
|
617
|
(tmp_path / "crates" / "cli").mkdir(parents=True) |
|
618
|
config = WorkspaceDetector().detect(tmp_path) |
|
619
|
assert config is not None |
|
620
|
assert config.type == "cargo" |
|
621
|
assert len(config.packages) == 2 |
|
622
|
|
|
623
|
def test_cargo_workspace_not_workspace(self, tmp_path): |
|
624
|
from navegador.monorepo import WorkspaceDetector |
|
625
|
|
|
626
|
_write(tmp_path / "Cargo.toml", "[package]\nname = \"myapp\"\n") |
|
627
|
config = WorkspaceDetector().detect(tmp_path) |
|
628
|
assert config is None |
|
629
|
|
|
630
|
def test_cargo_read_error(self, tmp_path): |
|
631
|
from navegador.monorepo import WorkspaceDetector |
|
632
|
|
|
633
|
cargo_toml = tmp_path / "Cargo.toml" |
|
634
|
cargo_toml.touch() |
|
635
|
det = WorkspaceDetector() |
|
636
|
with patch.object(Path, "read_text", side_effect=OSError("perm")): |
|
637
|
result = det._cargo_packages(tmp_path, cargo_toml) |
|
638
|
assert result is None |
|
639
|
|
|
640
|
def test_cargo_wildcard_members(self, tmp_path): |
|
641
|
from navegador.monorepo import WorkspaceDetector |
|
642
|
|
|
643
|
cargo_toml = "[workspace]\nmembers = [\"crates/*\"]\n" |
|
644
|
_write(tmp_path / "Cargo.toml", cargo_toml) |
|
645
|
(tmp_path / "crates" / "a").mkdir(parents=True) |
|
646
|
(tmp_path / "crates" / "b").mkdir(parents=True) |
|
647
|
det = WorkspaceDetector() |
|
648
|
pkgs = det._cargo_packages(tmp_path, tmp_path / "Cargo.toml") |
|
649
|
assert pkgs is not None |
|
650
|
assert len(pkgs) == 2 |
|
651
|
|
|
652
|
def test_go_workspace_parse(self, tmp_path): |
|
653
|
from navegador.monorepo import WorkspaceDetector |
|
654
|
|
|
655
|
(tmp_path / "cmd").mkdir() |
|
656
|
(tmp_path / "pkg").mkdir() |
|
657
|
_write(tmp_path / "go.work", "go 1.21\nuse ./cmd\nuse ./pkg\n") |
|
658
|
config = WorkspaceDetector().detect(tmp_path) |
|
659
|
assert config is not None |
|
660
|
assert config.type == "go" |
|
661
|
|
|
662
|
def test_go_workspace_read_error(self, tmp_path): |
|
663
|
from navegador.monorepo import WorkspaceDetector |
|
664
|
|
|
665
|
go_work = tmp_path / "go.work" |
|
666
|
go_work.touch() |
|
667
|
det = WorkspaceDetector() |
|
668
|
with patch.object(Path, "read_text", side_effect=OSError("perm")): |
|
669
|
packages = det._go_packages(tmp_path) |
|
670
|
assert isinstance(packages, list) |
|
671
|
|
|
672
|
def test_glob_packages_negation_skipped(self, tmp_path): |
|
673
|
from navegador.monorepo import WorkspaceDetector |
|
674
|
|
|
675
|
(tmp_path / "packages" / "a").mkdir(parents=True) |
|
676
|
det = WorkspaceDetector() |
|
677
|
pkgs = det._glob_packages(tmp_path, ["!packages/*"]) |
|
678
|
assert pkgs == [] |
|
679
|
|
|
680
|
def test_glob_packages_literal_path(self, tmp_path): |
|
681
|
from navegador.monorepo import WorkspaceDetector |
|
682
|
|
|
683
|
(tmp_path / "myapp").mkdir() |
|
684
|
det = WorkspaceDetector() |
|
685
|
pkgs = det._glob_packages(tmp_path, ["myapp"]) |
|
686
|
assert any(p.name == "myapp" for p in pkgs) |
|
687
|
|
|
688
|
def test_fallback_packages_skips_dotdirs(self, tmp_path): |
|
689
|
from navegador.monorepo import WorkspaceDetector |
|
690
|
|
|
691
|
(tmp_path / ".git").mkdir() |
|
692
|
_write(tmp_path / ".git" / "package.json", '{}') |
|
693
|
(tmp_path / "real").mkdir() |
|
694
|
_write(tmp_path / "real" / "package.json", '{}') |
|
695
|
det = WorkspaceDetector() |
|
696
|
pkgs = det._fallback_packages(tmp_path) |
|
697
|
names = [p.name for p in pkgs] |
|
698
|
assert ".git" not in names |
|
699
|
assert "real" in names |
|
700
|
|
|
701
|
|
|
702
|
class TestMonorepoIngesterEdgeCases: |
|
703
|
"""Cover lines 373, 404-405, 451-452, 466, 471, 474-475, 485-503, 509-531.""" |
|
704
|
|
|
705
|
def test_ingest_fallback_when_no_workspace(self, tmp_path): |
|
706
|
from navegador.monorepo import MonorepoIngester |
|
707
|
|
|
708
|
store = _make_store() |
|
709
|
ingester = MonorepoIngester(store) |
|
710
|
with patch("navegador.monorepo.RepoIngester") as MockRI: |
|
711
|
instance = MockRI.return_value |
|
712
|
instance.ingest.return_value = { |
|
713
|
"files": 1, "functions": 2, "classes": 0, "edges": 0, "skipped": 0 |
|
714
|
} |
|
715
|
stats = ingester.ingest(str(tmp_path)) |
|
716
|
assert stats["packages"] == 0 |
|
717
|
assert stats["workspace_type"] == "none" |
|
718
|
|
|
719
|
def test_ingest_raises_on_missing_path(self, tmp_path): |
|
720
|
from navegador.monorepo import MonorepoIngester |
|
721
|
|
|
722
|
ingester = MonorepoIngester(_make_store()) |
|
723
|
with pytest.raises(FileNotFoundError): |
|
724
|
ingester.ingest(str(tmp_path / "does_not_exist")) |
|
725
|
|
|
726
|
def test_ingest_package_exception_logged(self, tmp_path): |
|
727
|
from navegador.monorepo import MonorepoIngester, WorkspaceConfig |
|
728
|
|
|
729
|
store = _make_store() |
|
730
|
pkg_dir = tmp_path / "pkg" |
|
731
|
pkg_dir.mkdir() |
|
732
|
config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_dir]) |
|
733
|
|
|
734
|
with patch("navegador.monorepo.WorkspaceDetector") as MockDet: |
|
735
|
MockDet.return_value.detect.return_value = config |
|
736
|
with patch("navegador.monorepo.RepoIngester") as MockRI: |
|
737
|
MockRI.return_value.ingest.side_effect = RuntimeError("parse fail") |
|
738
|
stats = MonorepoIngester(store).ingest(str(tmp_path)) |
|
739
|
assert stats["packages"] == 0 |
|
740
|
|
|
741
|
def test_js_deps(self, tmp_path): |
|
742
|
from navegador.monorepo import MonorepoIngester |
|
743
|
|
|
744
|
ingester = MonorepoIngester(_make_store()) |
|
745
|
pkg_json = { |
|
746
|
"dependencies": {"react": "^18"}, |
|
747
|
"devDependencies": {"jest": "^29"}, |
|
748
|
"peerDependencies": {"typescript": ">=5"}, |
|
749
|
} |
|
750
|
_write(tmp_path / "package.json", json.dumps(pkg_json)) |
|
751
|
deps = ingester._js_deps(tmp_path) |
|
752
|
assert "react" in deps |
|
753
|
assert "jest" in deps |
|
754
|
assert "typescript" in deps |
|
755
|
|
|
756
|
def test_js_deps_no_file(self, tmp_path): |
|
757
|
from navegador.monorepo import MonorepoIngester |
|
758
|
|
|
759
|
ingester = MonorepoIngester(_make_store()) |
|
760
|
assert ingester._js_deps(tmp_path) == [] |
|
761
|
|
|
762
|
def test_js_deps_parse_error(self, tmp_path): |
|
763
|
from navegador.monorepo import MonorepoIngester |
|
764
|
|
|
765
|
ingester = MonorepoIngester(_make_store()) |
|
766
|
_write(tmp_path / "package.json", "{bad json") |
|
767
|
assert ingester._js_deps(tmp_path) == [] |
|
768
|
|
|
769
|
def test_cargo_deps(self, tmp_path): |
|
770
|
from navegador.monorepo import MonorepoIngester |
|
771
|
|
|
772
|
ingester = MonorepoIngester(_make_store()) |
|
773
|
cargo = "[dependencies]\nserde = \"1.0\"\ntokio = { version = \"1\" }\n[dev-dependencies]\ntempfile = \"3\"\n" |
|
774
|
_write(tmp_path / "Cargo.toml", cargo) |
|
775
|
deps = ingester._cargo_deps(tmp_path) |
|
776
|
assert "serde" in deps |
|
777
|
assert "tokio" in deps |
|
778
|
assert "tempfile" in deps |
|
779
|
|
|
780
|
def test_cargo_deps_no_file(self, tmp_path): |
|
781
|
from navegador.monorepo import MonorepoIngester |
|
782
|
|
|
783
|
ingester = MonorepoIngester(_make_store()) |
|
784
|
assert ingester._cargo_deps(tmp_path) == [] |
|
785
|
|
|
786
|
def test_cargo_deps_read_error(self, tmp_path): |
|
787
|
from navegador.monorepo import MonorepoIngester |
|
788
|
|
|
789
|
ingester = MonorepoIngester(_make_store()) |
|
790
|
cargo = tmp_path / "Cargo.toml" |
|
791
|
cargo.touch() |
|
792
|
with patch.object(Path, "read_text", side_effect=OSError("perm")): |
|
793
|
result = ingester._cargo_deps(tmp_path) |
|
794
|
assert result == [] |
|
795
|
|
|
796
|
def test_go_deps(self, tmp_path): |
|
797
|
from navegador.monorepo import MonorepoIngester |
|
798
|
|
|
799
|
ingester = MonorepoIngester(_make_store()) |
|
800
|
go_mod = "module example.com/myapp\ngo 1.21\n\nrequire (\n github.com/pkg/errors v0.9.1\n golang.org/x/net v0.17.0\n)\n\nrequire github.com/single/dep v1.0.0\n" |
|
801
|
_write(tmp_path / "go.mod", go_mod) |
|
802
|
deps = ingester._go_deps(tmp_path) |
|
803
|
assert "github.com/pkg/errors" in deps |
|
804
|
assert "golang.org/x/net" in deps |
|
805
|
assert "github.com/single/dep" in deps |
|
806
|
|
|
807
|
def test_go_deps_no_file(self, tmp_path): |
|
808
|
from navegador.monorepo import MonorepoIngester |
|
809
|
|
|
810
|
ingester = MonorepoIngester(_make_store()) |
|
811
|
assert ingester._go_deps(tmp_path) == [] |
|
812
|
|
|
813
|
def test_go_deps_read_error(self, tmp_path): |
|
814
|
from navegador.monorepo import MonorepoIngester |
|
815
|
|
|
816
|
ingester = MonorepoIngester(_make_store()) |
|
817
|
go_mod = tmp_path / "go.mod" |
|
818
|
go_mod.touch() |
|
819
|
with patch.object(Path, "read_text", side_effect=OSError("perm")): |
|
820
|
result = ingester._go_deps(tmp_path) |
|
821
|
assert result == [] |
|
822
|
|
|
823
|
def test_read_package_deps_unknown_type(self, tmp_path): |
|
824
|
from navegador.monorepo import MonorepoIngester |
|
825
|
|
|
826
|
ingester = MonorepoIngester(_make_store()) |
|
827
|
assert ingester._read_package_deps("unknown", tmp_path) == [] |
|
828
|
|
|
829
|
def test_dependency_edges_scoped_package(self, tmp_path): |
|
830
|
from navegador.monorepo import MonorepoIngester, WorkspaceConfig |
|
831
|
|
|
832
|
store = _make_store() |
|
833
|
ingester = MonorepoIngester(store) |
|
834
|
|
|
835
|
pkg_a = tmp_path / "pkg_a" |
|
836
|
pkg_b = tmp_path / "pkg_b" |
|
837
|
pkg_a.mkdir() |
|
838
|
pkg_b.mkdir() |
|
839
|
|
|
840
|
_write(pkg_a / "package.json", json.dumps({ |
|
841
|
"dependencies": {"@scope/pkg_b": "^1.0"} |
|
842
|
})) |
|
843
|
|
|
844
|
config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_a, pkg_b]) |
|
845
|
packages = [("pkg_a", pkg_a), ("pkg_b", pkg_b)] |
|
846
|
|
|
847
|
ingester._create_dependency_edges(config, packages) |
|
848
|
# store.create_edge should have been called at least once for the dependency |
|
849
|
# (pkg_b matches the bare name) |
|
850
|
store.create_edge.assert_called() |
|
851
|
|
|
852
|
def test_dependency_edges_exception_logged(self, tmp_path): |
|
853
|
from navegador.monorepo import MonorepoIngester, WorkspaceConfig |
|
854
|
|
|
855
|
store = _make_store() |
|
856
|
store.create_edge.side_effect = Exception("DB error") |
|
857
|
ingester = MonorepoIngester(store) |
|
858
|
|
|
859
|
pkg_a = tmp_path / "pkg_a" |
|
860
|
pkg_b = tmp_path / "pkg_b" |
|
861
|
pkg_a.mkdir() |
|
862
|
pkg_b.mkdir() |
|
863
|
_write(pkg_a / "package.json", json.dumps({"dependencies": {"pkg_b": "^1"}})) |
|
864
|
|
|
865
|
config = WorkspaceConfig(type="yarn", root=tmp_path, packages=[pkg_a, pkg_b]) |
|
866
|
packages = [("pkg_a", pkg_a), ("pkg_b", pkg_b)] |
|
867
|
# Should not raise |
|
868
|
count = ingester._create_dependency_edges(config, packages) |
|
869
|
assert count == 0 |
|
870
|
|
|
871
|
|
|
872
|
# =========================================================================== |
|
873
|
# navegador.pm (79% → target ~90%) |
|
874
|
# =========================================================================== |
|
875
|
|
|
876
|
|
|
877
|
class TestTicketIngester: |
|
878
|
"""Cover lines 243-245, 261-287.""" |
|
879
|
|
|
880
|
def _make(self): |
|
881
|
from navegador.pm import TicketIngester |
|
882
|
|
|
883
|
return TicketIngester(_make_store()) |
|
884
|
|
|
885
|
def test_ingest_linear_raises(self): |
|
886
|
ing = self._make() |
|
887
|
with pytest.raises(NotImplementedError, match="Linear"): |
|
888
|
ing.ingest_linear(api_key="lin_xxx") |
|
889
|
|
|
890
|
def test_ingest_jira_raises(self): |
|
891
|
ing = self._make() |
|
892
|
with pytest.raises(NotImplementedError, match="Jira"): |
|
893
|
ing.ingest_jira(url="https://co.atlassian.net", token="tok") |
|
894
|
|
|
895
|
def test_github_severity_critical(self): |
|
896
|
from navegador.pm import TicketIngester |
|
897
|
|
|
898
|
assert TicketIngester._github_severity(["critical"]) == "critical" |
|
899
|
assert TicketIngester._github_severity(["blocker"]) == "critical" |
|
900
|
assert TicketIngester._github_severity(["p0", "other"]) == "critical" |
|
901
|
|
|
902
|
def test_github_severity_warning(self): |
|
903
|
from navegador.pm import TicketIngester |
|
904
|
|
|
905
|
assert TicketIngester._github_severity(["bug"]) == "warning" |
|
906
|
assert TicketIngester._github_severity(["high"]) == "warning" |
|
907
|
assert TicketIngester._github_severity(["important"]) == "warning" |
|
908
|
|
|
909
|
def test_github_severity_info(self): |
|
910
|
from navegador.pm import TicketIngester |
|
911
|
|
|
912
|
assert TicketIngester._github_severity([]) == "info" |
|
913
|
assert TicketIngester._github_severity(["enhancement"]) == "info" |
|
914
|
|
|
915
|
def test_link_to_code_returns_zero_on_empty_graph(self): |
|
916
|
from navegador.pm import TicketIngester |
|
917
|
|
|
918
|
store = _make_store() |
|
919
|
store.query.return_value = MagicMock(result_set=[]) |
|
920
|
ing = TicketIngester(store) |
|
921
|
result = ing._link_to_code("myrepo") |
|
922
|
assert result == 0 |
|
923
|
|
|
924
|
def test_link_to_code_returns_zero_on_query_failure(self): |
|
925
|
from navegador.pm import TicketIngester |
|
926
|
|
|
927
|
store = _make_store() |
|
928
|
store.query.side_effect = Exception("DB down") |
|
929
|
ing = TicketIngester(store) |
|
930
|
result = ing._link_to_code("myrepo") |
|
931
|
assert result == 0 |
|
932
|
|
|
933
|
def test_link_to_code_matches_tokens(self): |
|
934
|
from navegador.pm import TicketIngester |
|
935
|
|
|
936
|
store = _make_store() |
|
937
|
# First call: tickets |
|
938
|
ticket_result = MagicMock() |
|
939
|
ticket_result.result_set = [("#1: authenticate user", "fix auth flow")] |
|
940
|
# Second call: code nodes |
|
941
|
code_result = MagicMock() |
|
942
|
code_result.result_set = [("Function", "authenticate"), ("Function", "unrelated")] |
|
943
|
store.query.side_effect = [ticket_result, code_result, None] |
|
944
|
|
|
945
|
ing = TicketIngester(store) |
|
946
|
result = ing._link_to_code("myrepo") |
|
947
|
assert result >= 1 |
|
948
|
|
|
949
|
def test_link_to_code_skips_short_tokens(self): |
|
950
|
from navegador.pm import TicketIngester |
|
951
|
|
|
952
|
store = _make_store() |
|
953
|
ticket_result = MagicMock() |
|
954
|
# Only short words (< 4 chars) |
|
955
|
ticket_result.result_set = [("#1: fix", "x")] |
|
956
|
code_result = MagicMock() |
|
957
|
code_result.result_set = [("Function", "fix")] |
|
958
|
store.query.side_effect = [ticket_result, code_result] |
|
959
|
|
|
960
|
ing = TicketIngester(store) |
|
961
|
# "fix" is exactly 3 chars → skipped as a token |
|
962
|
result = ing._link_to_code("myrepo") |
|
963
|
assert result == 0 |
|
964
|
|
|
965
|
def test_ingest_github_issues_http_error(self): |
|
966
|
from navegador.pm import TicketIngester |
|
967
|
|
|
968
|
store = _make_store() |
|
969
|
ing = TicketIngester(store) |
|
970
|
with patch("urllib.request.urlopen", side_effect=Exception("network err")): |
|
971
|
with pytest.raises(RuntimeError, match="Failed to fetch"): |
|
972
|
ing.ingest_github_issues("owner/repo", token="tok") |
|
973
|
|
|
974
|
def test_ingest_github_issues_success(self): |
|
975
|
from navegador.pm import TicketIngester |
|
976
|
|
|
977
|
store = _make_store() |
|
978
|
ing = TicketIngester(store) |
|
979
|
issues = [ |
|
980
|
{"number": 1, "title": "Fix auth", "body": "desc", "html_url": "http://x", |
|
981
|
"labels": [{"name": "bug"}], "assignees": [{"login": "alice"}]}, |
|
982
|
] |
|
983
|
mock_resp = MagicMock() |
|
984
|
mock_resp.__enter__ = MagicMock(return_value=mock_resp) |
|
985
|
mock_resp.__exit__ = MagicMock(return_value=False) |
|
986
|
mock_resp.read.return_value = json.dumps(issues).encode() |
|
987
|
|
|
988
|
with patch("urllib.request.urlopen", return_value=mock_resp): |
|
989
|
stats = ing.ingest_github_issues("owner/repo") |
|
990
|
assert stats["tickets"] == 1 |
|
991
|
|
|
992
|
|
|
993
|
# =========================================================================== |
|
994
|
# navegador.completions — install path (lines 66-70) |
|
995
|
# =========================================================================== |
|
996
|
|
|
997
|
|
|
998
|
class TestGetInstallInstruction: |
|
999
|
def test_bash(self): |
|
1000
|
from navegador.completions import get_install_instruction |
|
1001
|
|
|
1002
|
instruction = get_install_instruction("bash") |
|
1003
|
assert "~/.bashrc" in instruction |
|
1004
|
assert "bash_source" in instruction |
|
1005
|
|
|
1006
|
def test_zsh(self): |
|
1007
|
from navegador.completions import get_install_instruction |
|
1008
|
|
|
1009
|
instruction = get_install_instruction("zsh") |
|
1010
|
assert "~/.zshrc" in instruction |
|
1011
|
assert "zsh_source" in instruction |
|
1012
|
|
|
1013
|
def test_fish(self): |
|
1014
|
from navegador.completions import get_install_instruction |
|
1015
|
|
|
1016
|
instruction = get_install_instruction("fish") |
|
1017
|
assert "config.fish" in instruction |
|
1018
|
assert "fish_source" in instruction |
|
1019
|
|
|
1020
|
def test_invalid_raises(self): |
|
1021
|
from navegador.completions import get_install_instruction |
|
1022
|
|
|
1023
|
with pytest.raises(ValueError, match="Unsupported"): |
|
1024
|
get_install_instruction("pwsh") |
|
1025
|
|
|
1026
|
|
|
1027
|
# =========================================================================== |
|
1028
|
# navegador.cli.commands — watch callback (lines ~179-185) |
|
1029
|
# =========================================================================== |
|
1030
|
|
|
1031
|
|
|
1032
|
class TestIngestWatchCallback: |
|
1033
|
"""Exercise the _on_cycle callback inside the watch branch of ingest.""" |
|
1034
|
|
|
1035
|
def test_watch_callback_with_changed_files(self, tmp_path): |
|
1036
|
from click.testing import CliRunner |
|
1037
|
|
|
1038
|
from navegador.cli.commands import main |
|
1039
|
|
|
1040
|
runner = CliRunner() |
|
1041
|
repo = str(tmp_path) |
|
1042
|
|
|
1043
|
cycle_calls = [] |
|
1044
|
|
|
1045
|
def fake_watch(path, interval=2.0, callback=None): |
|
1046
|
if callback: |
|
1047
|
# Simulate a cycle with changed files |
|
1048
|
result = callback({"files": 3, "skipped": 10}) |
|
1049
|
cycle_calls.append(result) |
|
1050
|
# Second call: simulate KeyboardInterrupt to exit |
|
1051
|
raise KeyboardInterrupt |
|
1052
|
|
|
1053
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1054
|
mock_gs.return_value = _make_store() |
|
1055
|
with patch("navegador.ingestion.RepoIngester") as MockRI: |
|
1056
|
MockRI.return_value.watch.side_effect = fake_watch |
|
1057
|
result = runner.invoke(main, ["ingest", repo, "--watch", "--interval", "1"]) |
|
1058
|
|
|
1059
|
assert result.exit_code == 0 |
|
1060
|
assert cycle_calls == [True] |
|
1061
|
|
|
1062
|
def test_watch_callback_no_changed_files(self, tmp_path): |
|
1063
|
"""Callback with 0 changed files should still return True.""" |
|
1064
|
from click.testing import CliRunner |
|
1065
|
|
|
1066
|
from navegador.cli.commands import main |
|
1067
|
|
|
1068
|
runner = CliRunner() |
|
1069
|
|
|
1070
|
def fake_watch(path, interval=2.0, callback=None): |
|
1071
|
if callback: |
|
1072
|
result = callback({"files": 0, "skipped": 5}) |
|
1073
|
assert result is True |
|
1074
|
raise KeyboardInterrupt |
|
1075
|
|
|
1076
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1077
|
mock_gs.return_value = _make_store() |
|
1078
|
with patch("navegador.ingestion.RepoIngester") as MockRI: |
|
1079
|
MockRI.return_value.watch.side_effect = fake_watch |
|
1080
|
result = runner.invoke(main, ["ingest", str(tmp_path), "--watch"]) |
|
1081
|
|
|
1082
|
assert result.exit_code == 0 |
|
1083
|
|
|
1084
|
|
|
1085
|
# =========================================================================== |
|
1086
|
# navegador.cli.commands — additional uncovered CLI branches |
|
1087
|
# =========================================================================== |
|
1088
|
|
|
1089
|
|
|
1090
|
class TestCLIBranchesDeadcode: |
|
1091
|
"""Cover lines 1395-1407 (unreachable_classes and orphan_files branches).""" |
|
1092
|
|
|
1093
|
def test_deadcode_shows_unreachable_classes(self, tmp_path): |
|
1094
|
from click.testing import CliRunner |
|
1095
|
|
|
1096
|
from navegador.cli.commands import main |
|
1097
|
from navegador.analysis.deadcode import DeadCodeReport |
|
1098
|
|
|
1099
|
runner = CliRunner() |
|
1100
|
report = DeadCodeReport( |
|
1101
|
unreachable_functions=[], |
|
1102
|
unreachable_classes=[ |
|
1103
|
{"name": "OldClass", "file_path": "old.py", "line_start": 1, "type": "Class"} |
|
1104
|
], |
|
1105
|
orphan_files=["orphan.py"], |
|
1106
|
) |
|
1107
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1108
|
mock_gs.return_value = _make_store() |
|
1109
|
with patch("navegador.analysis.deadcode.DeadCodeDetector") as MockDC: |
|
1110
|
MockDC.return_value.detect.return_value = report |
|
1111
|
result = runner.invoke(main, ["deadcode"]) |
|
1112
|
assert result.exit_code == 0 |
|
1113
|
assert "OldClass" in result.output |
|
1114
|
assert "orphan.py" in result.output |
|
1115
|
|
|
1116
|
def test_deadcode_no_dead_code_message(self, tmp_path): |
|
1117
|
from click.testing import CliRunner |
|
1118
|
|
|
1119
|
from navegador.cli.commands import main |
|
1120
|
from navegador.analysis.deadcode import DeadCodeReport |
|
1121
|
|
|
1122
|
runner = CliRunner() |
|
1123
|
report = DeadCodeReport( |
|
1124
|
unreachable_functions=[], unreachable_classes=[], orphan_files=[] |
|
1125
|
) |
|
1126
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1127
|
mock_gs.return_value = _make_store() |
|
1128
|
with patch("navegador.analysis.deadcode.DeadCodeDetector") as MockDC: |
|
1129
|
MockDC.return_value.detect.return_value = report |
|
1130
|
result = runner.invoke(main, ["deadcode"]) |
|
1131
|
assert result.exit_code == 0 |
|
1132
|
assert "No dead code" in result.output |
|
1133
|
|
|
1134
|
|
|
1135
|
class TestCLIBranchesTestmap: |
|
1136
|
"""Cover lines 1439-1452 (testmap table and unmatched branches).""" |
|
1137
|
|
|
1138
|
def test_testmap_shows_table_and_unmatched(self): |
|
1139
|
from click.testing import CliRunner |
|
1140
|
|
|
1141
|
from navegador.cli.commands import main |
|
1142
|
from navegador.analysis.testmap import TestMapResult, TestLink |
|
1143
|
|
|
1144
|
runner = CliRunner() |
|
1145
|
link = TestLink( |
|
1146
|
test_name="test_foo", |
|
1147
|
test_file="test_foo.py", |
|
1148
|
prod_name="foo", |
|
1149
|
prod_file="foo.py", |
|
1150
|
prod_type="Function", |
|
1151
|
source="name", |
|
1152
|
) |
|
1153
|
result_obj = TestMapResult( |
|
1154
|
links=[link], |
|
1155
|
unmatched_tests=[{"name": "test_orphan", "file_path": "test_x.py"}], |
|
1156
|
edges_created=1, |
|
1157
|
) |
|
1158
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1159
|
mock_gs.return_value = _make_store() |
|
1160
|
with patch("navegador.analysis.testmap.TestMapper") as MockTM: |
|
1161
|
MockTM.return_value.map_tests.return_value = result_obj |
|
1162
|
result = runner.invoke(main, ["testmap"]) |
|
1163
|
assert result.exit_code == 0 |
|
1164
|
assert "test_foo" in result.output |
|
1165
|
assert "test_orphan" in result.output |
|
1166
|
|
|
1167
|
|
|
1168
|
class TestCLIBranchesRename: |
|
1169
|
"""Cover lines 1640-1650 (rename non-JSON output).""" |
|
1170
|
|
|
1171
|
def test_rename_preview_output(self): |
|
1172
|
from click.testing import CliRunner |
|
1173
|
|
|
1174
|
from navegador.cli.commands import main |
|
1175
|
from navegador.refactor import RenameResult |
|
1176
|
|
|
1177
|
runner = CliRunner() |
|
1178
|
rename_result = RenameResult( |
|
1179
|
old_name="old_func", |
|
1180
|
new_name="new_func", |
|
1181
|
affected_nodes=[{"name": "old_func", "file_path": "f.py", "type": "Function", "line_start": 1}], |
|
1182
|
affected_files=["f.py", "g.py"], |
|
1183
|
edges_updated=3, |
|
1184
|
) |
|
1185
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1186
|
mock_gs.return_value = _make_store() |
|
1187
|
with patch("navegador.refactor.SymbolRenamer") as MockSR: |
|
1188
|
MockSR.return_value.preview_rename.return_value = rename_result |
|
1189
|
result = runner.invoke(main, ["rename", "old_func", "new_func", "--preview"]) |
|
1190
|
assert result.exit_code == 0 |
|
1191
|
assert "old_func" in result.output |
|
1192
|
assert "f.py" in result.output |
|
1193
|
|
|
1194
|
def test_rename_apply_output(self): |
|
1195
|
from click.testing import CliRunner |
|
1196
|
|
|
1197
|
from navegador.cli.commands import main |
|
1198
|
from navegador.refactor import RenameResult |
|
1199
|
|
|
1200
|
runner = CliRunner() |
|
1201
|
rename_result = RenameResult( |
|
1202
|
old_name="old_func", |
|
1203
|
new_name="new_func", |
|
1204
|
affected_nodes=[], |
|
1205
|
affected_files=[], |
|
1206
|
edges_updated=0, |
|
1207
|
) |
|
1208
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1209
|
mock_gs.return_value = _make_store() |
|
1210
|
with patch("navegador.refactor.SymbolRenamer") as MockSR: |
|
1211
|
MockSR.return_value.apply_rename.return_value = rename_result |
|
1212
|
result = runner.invoke(main, ["rename", "old_func", "new_func"]) |
|
1213
|
assert result.exit_code == 0 |
|
1214
|
assert "Renamed" in result.output |
|
1215
|
|
|
1216
|
|
|
1217
|
class TestCLIBranchesSemantic: |
|
1218
|
"""Cover lines 2068-2080 (semantic-search table output).""" |
|
1219
|
|
|
1220
|
def test_semantic_search_table_output(self): |
|
1221
|
from click.testing import CliRunner |
|
1222
|
|
|
1223
|
from navegador.cli.commands import main |
|
1224
|
|
|
1225
|
runner = CliRunner() |
|
1226
|
search_results = [ |
|
1227
|
{"score": 0.95, "type": "Function", "name": "authenticate", "file_path": "auth.py"}, |
|
1228
|
] |
|
1229
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1230
|
mock_gs.return_value = _make_store() |
|
1231
|
with patch("navegador.intelligence.search.SemanticSearch") as MockSS: |
|
1232
|
MockSS.return_value.search.return_value = search_results |
|
1233
|
with patch("navegador.llm.auto_provider") as mock_ap: |
|
1234
|
mock_ap.return_value = MagicMock() |
|
1235
|
result = runner.invoke(main, ["semantic-search", "auth tokens"]) |
|
1236
|
assert result.exit_code == 0 |
|
1237
|
assert "authenticate" in result.output |
|
1238
|
|
|
1239
|
def test_semantic_search_no_results(self): |
|
1240
|
from click.testing import CliRunner |
|
1241
|
|
|
1242
|
from navegador.cli.commands import main |
|
1243
|
|
|
1244
|
runner = CliRunner() |
|
1245
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1246
|
mock_gs.return_value = _make_store() |
|
1247
|
with patch("navegador.intelligence.search.SemanticSearch") as MockSS: |
|
1248
|
MockSS.return_value.search.return_value = [] |
|
1249
|
with patch("navegador.llm.auto_provider") as mock_ap: |
|
1250
|
mock_ap.return_value = MagicMock() |
|
1251
|
result = runner.invoke(main, ["semantic-search", "nothing"]) |
|
1252
|
assert result.exit_code == 0 |
|
1253
|
assert "--index" in result.output |
|
1254
|
|
|
1255
|
def test_semantic_search_with_index_flag(self): |
|
1256
|
from click.testing import CliRunner |
|
1257
|
|
|
1258
|
from navegador.cli.commands import main |
|
1259
|
|
|
1260
|
runner = CliRunner() |
|
1261
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1262
|
mock_gs.return_value = _make_store() |
|
1263
|
with patch("navegador.intelligence.search.SemanticSearch") as MockSS: |
|
1264
|
inst = MockSS.return_value |
|
1265
|
inst.index.return_value = 42 |
|
1266
|
inst.search.return_value = [] |
|
1267
|
with patch("navegador.llm.auto_provider") as mock_ap: |
|
1268
|
mock_ap.return_value = MagicMock() |
|
1269
|
result = runner.invoke(main, ["semantic-search", "auth", "--index"]) |
|
1270
|
assert result.exit_code == 0 |
|
1271
|
assert "42" in result.output |
|
1272
|
|
|
1273
|
|
|
1274
|
class TestCLIBranchesRepoCommands: |
|
1275
|
"""Cover lines 1539-1572 (repo list/ingest-all table output).""" |
|
1276
|
|
|
1277
|
def test_repo_list_table_output(self): |
|
1278
|
from click.testing import CliRunner |
|
1279
|
|
|
1280
|
from navegador.cli.commands import main |
|
1281
|
|
|
1282
|
runner = CliRunner() |
|
1283
|
repos = [{"name": "myrepo", "path": "/path/to/myrepo"}] |
|
1284
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1285
|
mock_gs.return_value = _make_store() |
|
1286
|
with patch("navegador.multirepo.MultiRepoManager") as MockMRM: |
|
1287
|
MockMRM.return_value.list_repos.return_value = repos |
|
1288
|
result = runner.invoke(main, ["repo", "list"]) |
|
1289
|
assert result.exit_code == 0 |
|
1290
|
assert "myrepo" in result.output |
|
1291
|
|
|
1292
|
def test_repo_list_empty(self): |
|
1293
|
from click.testing import CliRunner |
|
1294
|
|
|
1295
|
from navegador.cli.commands import main |
|
1296
|
|
|
1297
|
runner = CliRunner() |
|
1298
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1299
|
mock_gs.return_value = _make_store() |
|
1300
|
with patch("navegador.multirepo.MultiRepoManager") as MockMRM: |
|
1301
|
MockMRM.return_value.list_repos.return_value = [] |
|
1302
|
result = runner.invoke(main, ["repo", "list"]) |
|
1303
|
assert result.exit_code == 0 |
|
1304
|
assert "No repositories" in result.output |
|
1305
|
|
|
1306
|
def test_repo_ingest_all_table(self): |
|
1307
|
from click.testing import CliRunner |
|
1308
|
|
|
1309
|
from navegador.cli.commands import main |
|
1310
|
|
|
1311
|
runner = CliRunner() |
|
1312
|
summary = {"myrepo": {"files": 5, "functions": 10, "classes": 2}} |
|
1313
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1314
|
mock_gs.return_value = _make_store() |
|
1315
|
with patch("navegador.multirepo.MultiRepoManager") as MockMRM: |
|
1316
|
MockMRM.return_value.ingest_all.return_value = summary |
|
1317
|
result = runner.invoke(main, ["repo", "ingest-all"]) |
|
1318
|
assert result.exit_code == 0 |
|
1319
|
assert "myrepo" in result.output |
|
1320
|
|
|
1321
|
def test_repo_search_table(self): |
|
1322
|
from click.testing import CliRunner |
|
1323
|
|
|
1324
|
from navegador.cli.commands import main |
|
1325
|
|
|
1326
|
runner = CliRunner() |
|
1327
|
results = [{"label": "Function", "name": "foo", "file_path": "foo.py"}] |
|
1328
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1329
|
mock_gs.return_value = _make_store() |
|
1330
|
with patch("navegador.multirepo.MultiRepoManager") as MockMRM: |
|
1331
|
MockMRM.return_value.cross_repo_search.return_value = results |
|
1332
|
result = runner.invoke(main, ["repo", "search", "foo"]) |
|
1333
|
assert result.exit_code == 0 |
|
1334
|
assert "foo" in result.output |
|
1335
|
|
|
1336
|
def test_repo_search_empty(self): |
|
1337
|
from click.testing import CliRunner |
|
1338
|
|
|
1339
|
from navegador.cli.commands import main |
|
1340
|
|
|
1341
|
runner = CliRunner() |
|
1342
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1343
|
mock_gs.return_value = _make_store() |
|
1344
|
with patch("navegador.multirepo.MultiRepoManager") as MockMRM: |
|
1345
|
MockMRM.return_value.cross_repo_search.return_value = [] |
|
1346
|
result = runner.invoke(main, ["repo", "search", "nothing"]) |
|
1347
|
assert result.exit_code == 0 |
|
1348
|
assert "No results" in result.output |
|
1349
|
|
|
1350
|
|
|
1351
|
class TestCLIBranchesPM: |
|
1352
|
"""Cover lines 1793-1806 (pm ingest output).""" |
|
1353
|
|
|
1354
|
def test_pm_ingest_no_github_raises(self): |
|
1355
|
from click.testing import CliRunner |
|
1356
|
|
|
1357
|
from navegador.cli.commands import main |
|
1358
|
|
|
1359
|
runner = CliRunner() |
|
1360
|
result = runner.invoke(main, ["pm", "ingest"]) |
|
1361
|
assert result.exit_code != 0 |
|
1362
|
|
|
1363
|
def test_pm_ingest_table_output(self): |
|
1364
|
from click.testing import CliRunner |
|
1365
|
|
|
1366
|
from navegador.cli.commands import main |
|
1367
|
|
|
1368
|
runner = CliRunner() |
|
1369
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1370
|
mock_gs.return_value = _make_store() |
|
1371
|
with patch("navegador.pm.TicketIngester") as MockTI: |
|
1372
|
MockTI.return_value.ingest_github_issues.return_value = { |
|
1373
|
"tickets": 5, "linked": 2 |
|
1374
|
} |
|
1375
|
result = runner.invoke(main, ["pm", "ingest", "--github", "owner/repo"]) |
|
1376
|
assert result.exit_code == 0 |
|
1377
|
assert "5" in result.output |
|
1378
|
|
|
1379
|
def test_pm_ingest_json(self): |
|
1380
|
from click.testing import CliRunner |
|
1381
|
|
|
1382
|
from navegador.cli.commands import main |
|
1383
|
|
|
1384
|
runner = CliRunner() |
|
1385
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1386
|
mock_gs.return_value = _make_store() |
|
1387
|
with patch("navegador.pm.TicketIngester") as MockTI: |
|
1388
|
MockTI.return_value.ingest_github_issues.return_value = { |
|
1389
|
"tickets": 3, "linked": 1 |
|
1390
|
} |
|
1391
|
result = runner.invoke( |
|
1392
|
main, ["pm", "ingest", "--github", "owner/repo", "--json"] |
|
1393
|
) |
|
1394
|
assert result.exit_code == 0 |
|
1395
|
data = json.loads(result.output) |
|
1396
|
assert data["tickets"] == 3 |
|
1397
|
|
|
1398
|
|
|
1399
|
class TestCLIBranchesIngest: |
|
1400
|
"""Cover lines 179-185 (ingest --monorepo table output).""" |
|
1401
|
|
|
1402
|
def test_ingest_monorepo_table(self, tmp_path): |
|
1403
|
from click.testing import CliRunner |
|
1404
|
|
|
1405
|
from navegador.cli.commands import main |
|
1406
|
|
|
1407
|
runner = CliRunner() |
|
1408
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1409
|
mock_gs.return_value = _make_store() |
|
1410
|
with patch("navegador.monorepo.MonorepoIngester") as MockMI: |
|
1411
|
MockMI.return_value.ingest.return_value = { |
|
1412
|
"files": 10, "functions": 20, "packages": 3, "workspace_type": "yarn" |
|
1413
|
} |
|
1414
|
result = runner.invoke( |
|
1415
|
main, ["ingest", str(tmp_path), "--monorepo"] |
|
1416
|
) |
|
1417
|
assert result.exit_code == 0 |
|
1418
|
|
|
1419
|
def test_ingest_monorepo_json(self, tmp_path): |
|
1420
|
from click.testing import CliRunner |
|
1421
|
|
|
1422
|
from navegador.cli.commands import main |
|
1423
|
|
|
1424
|
runner = CliRunner() |
|
1425
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1426
|
mock_gs.return_value = _make_store() |
|
1427
|
with patch("navegador.monorepo.MonorepoIngester") as MockMI: |
|
1428
|
MockMI.return_value.ingest.return_value = { |
|
1429
|
"files": 5, "packages": 2 |
|
1430
|
} |
|
1431
|
result = runner.invoke( |
|
1432
|
main, ["ingest", str(tmp_path), "--monorepo", "--json"] |
|
1433
|
) |
|
1434
|
assert result.exit_code == 0 |
|
1435
|
data = json.loads(result.output) |
|
1436
|
assert data["files"] == 5 |
|
1437
|
|
|
1438
|
|
|
1439
|
class TestCLIBranchesSubmodulesIngest: |
|
1440
|
"""Cover lines 1901-1916 (submodules ingest output).""" |
|
1441
|
|
|
1442
|
def test_submodules_ingest_output(self, tmp_path): |
|
1443
|
from click.testing import CliRunner |
|
1444
|
|
|
1445
|
from navegador.cli.commands import main |
|
1446
|
|
|
1447
|
runner = CliRunner() |
|
1448
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1449
|
mock_gs.return_value = _make_store() |
|
1450
|
with patch("navegador.submodules.SubmoduleIngester") as MockSI: |
|
1451
|
MockSI.return_value.ingest_with_submodules.return_value = { |
|
1452
|
"total_files": 10, |
|
1453
|
"submodules": {"sub1": {}, "sub2": {}}, |
|
1454
|
} |
|
1455
|
result = runner.invoke(main, ["submodules", "ingest", str(tmp_path)]) |
|
1456
|
assert result.exit_code == 0 |
|
1457
|
assert "sub1" in result.output |
|
1458
|
|
|
1459
|
def test_submodules_ingest_json(self, tmp_path): |
|
1460
|
from click.testing import CliRunner |
|
1461
|
|
|
1462
|
from navegador.cli.commands import main |
|
1463
|
|
|
1464
|
runner = CliRunner() |
|
1465
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1466
|
mock_gs.return_value = _make_store() |
|
1467
|
with patch("navegador.submodules.SubmoduleIngester") as MockSI: |
|
1468
|
MockSI.return_value.ingest_with_submodules.return_value = { |
|
1469
|
"total_files": 5, |
|
1470
|
"submodules": {}, |
|
1471
|
} |
|
1472
|
result = runner.invoke( |
|
1473
|
main, ["submodules", "ingest", str(tmp_path), "--json"] |
|
1474
|
) |
|
1475
|
assert result.exit_code == 0 |
|
1476
|
data = json.loads(result.output) |
|
1477
|
assert data["total_files"] == 5 |
|
1478
|
|
|
1479
|
|
|
1480
|
class TestCLIBranchesCommunities: |
|
1481
|
"""Cover lines 2105-2141 (communities store-labels + table).""" |
|
1482
|
|
|
1483
|
def test_communities_store_labels(self): |
|
1484
|
from click.testing import CliRunner |
|
1485
|
|
|
1486
|
from navegador.cli.commands import main |
|
1487
|
from navegador.intelligence.community import Community |
|
1488
|
|
|
1489
|
runner = CliRunner() |
|
1490
|
comm = Community(name="c1", members=["a", "b", "c"], density=0.5) |
|
1491
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1492
|
mock_gs.return_value = _make_store() |
|
1493
|
with patch("navegador.intelligence.community.CommunityDetector") as MockCD: |
|
1494
|
inst = MockCD.return_value |
|
1495
|
inst.detect.return_value = [comm] |
|
1496
|
inst.store_communities.return_value = 3 |
|
1497
|
result = runner.invoke(main, ["communities", "--store-labels"]) |
|
1498
|
assert result.exit_code == 0 |
|
1499
|
assert "3" in result.output |
|
1500
|
|
|
1501
|
def test_communities_empty(self): |
|
1502
|
from click.testing import CliRunner |
|
1503
|
|
|
1504
|
from navegador.cli.commands import main |
|
1505
|
|
|
1506
|
runner = CliRunner() |
|
1507
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1508
|
mock_gs.return_value = _make_store() |
|
1509
|
with patch("navegador.intelligence.community.CommunityDetector") as MockCD: |
|
1510
|
MockCD.return_value.detect.return_value = [] |
|
1511
|
result = runner.invoke(main, ["communities"]) |
|
1512
|
assert result.exit_code == 0 |
|
1513
|
assert "No communities" in result.output |
|
1514
|
|
|
1515
|
def test_communities_large_preview(self): |
|
1516
|
from click.testing import CliRunner |
|
1517
|
|
|
1518
|
from navegador.cli.commands import main |
|
1519
|
from navegador.intelligence.community import Community |
|
1520
|
|
|
1521
|
runner = CliRunner() |
|
1522
|
comm = Community(name="big", members=list("abcdefgh"), density=0.8) |
|
1523
|
with patch("navegador.cli.commands._get_store") as mock_gs: |
|
1524
|
mock_gs.return_value = _make_store() |
|
1525
|
with patch("navegador.intelligence.community.CommunityDetector") as MockCD: |
|
1526
|
MockCD.return_value.detect.return_value = [comm] |
|
1527
|
result = runner.invoke(main, ["communities"]) |
|
1528
|
assert result.exit_code == 0 |
|
1529
|
assert "+" in result.output # preview truncation |
|
1530
|
|
|
1531
|
|
|
1532
|
# =========================================================================== |
|
1533
|
# navegador.cluster.messaging (86% → target ~95%) |
|
1534
|
# =========================================================================== |
|
1535
|
|
|
1536
|
|
|
1537
|
class TestMessageBus: |
|
1538
|
"""Cover lines 74-78, 154, 178-182, 208.""" |
|
1539
|
|
|
1540
|
def _make(self): |
|
1541
|
from navegador.cluster.messaging import MessageBus |
|
1542
|
|
|
1543
|
r = MagicMock() |
|
1544
|
r.smembers.return_value = set() |
|
1545
|
r.lrange.return_value = [] |
|
1546
|
return MessageBus("redis://localhost:6379", _redis_client=r), r |
|
1547
|
|
|
1548
|
def test_send_returns_message_id(self): |
|
1549
|
bus, r = self._make() |
|
1550
|
msg_id = bus.send("agent1", "agent2", "task", {"k": "v"}) |
|
1551
|
assert isinstance(msg_id, str) and len(msg_id) == 36 |
|
1552
|
r.rpush.assert_called_once() |
|
1553
|
|
|
1554
|
def test_receive_returns_unacked_messages(self): |
|
1555
|
import json as _json |
|
1556
|
from navegador.cluster.messaging import Message |
|
1557
|
import time |
|
1558
|
|
|
1559
|
bus, r = self._make() |
|
1560
|
msg = Message( |
|
1561
|
id="abc123", from_agent="a1", to_agent="a2", |
|
1562
|
type="task", payload={}, timestamp=time.time() |
|
1563
|
) |
|
1564
|
r.lrange.return_value = [_json.dumps(msg.to_dict()).encode()] |
|
1565
|
r.smembers.return_value = set() |
|
1566
|
messages = bus.receive("a2") |
|
1567
|
assert len(messages) == 1 |
|
1568
|
assert messages[0].id == "abc123" |
|
1569
|
|
|
1570
|
def test_receive_filters_acked(self): |
|
1571
|
import json as _json |
|
1572
|
from navegador.cluster.messaging import Message |
|
1573
|
import time |
|
1574
|
|
|
1575
|
bus, r = self._make() |
|
1576
|
msg = Message( |
|
1577
|
id="acked_id", from_agent="a1", to_agent="a2", |
|
1578
|
type="task", payload={}, timestamp=time.time() |
|
1579
|
) |
|
1580
|
r.lrange.return_value = [_json.dumps(msg.to_dict()).encode()] |
|
1581
|
r.smembers.return_value = {b"acked_id"} |
|
1582
|
messages = bus.receive("a2") |
|
1583
|
assert messages == [] |
|
1584
|
|
|
1585
|
def test_acknowledge_with_agent_id(self): |
|
1586
|
bus, r = self._make() |
|
1587
|
bus.acknowledge("msg123", agent_id="a2") |
|
1588
|
r.sadd.assert_called() |
|
1589
|
|
|
1590
|
def test_acknowledge_without_agent_id_broadcasts(self): |
|
1591
|
bus, r = self._make() |
|
1592
|
r.smembers.return_value = {b"agent1", b"agent2"} |
|
1593
|
bus.acknowledge("msg123") |
|
1594
|
assert r.sadd.call_count == 2 |
|
1595
|
|
|
1596
|
def test_broadcast_skips_sender(self): |
|
1597
|
bus, r = self._make() |
|
1598
|
r.smembers.return_value = {b"sender", b"agent2", b"agent3"} |
|
1599
|
with patch.object(bus, "send", return_value="mid") as mock_send: |
|
1600
|
ids = bus.broadcast("sender", "task", {}) |
|
1601
|
assert len(ids) == 2 |
|
1602
|
for call_args in mock_send.call_args_list: |
|
1603
|
assert call_args[0][0] == "sender" |
|
1604
|
assert call_args[0][1] != "sender" |
|
1605
|
|
|
1606
|
def test_client_lazy_init_raises_on_missing_redis(self): |
|
1607
|
from navegador.cluster.messaging import MessageBus |
|
1608
|
|
|
1609
|
bus = MessageBus("redis://localhost:6379") |
|
1610
|
with patch.dict("sys.modules", {"redis": None}): |
|
1611
|
with pytest.raises(ImportError, match="redis"): |
|
1612
|
bus._client() |
|
1613
|
|
|
1614
|
|
|
1615
|
# =========================================================================== |
|
1616
|
# navegador.cluster.locking (90% → target ~97%) |
|
1617
|
# =========================================================================== |
|
1618
|
|
|
1619
|
|
|
1620
|
class TestDistributedLock: |
|
1621
|
"""Cover lines 72-76, 120.""" |
|
1622
|
|
|
1623
|
def _make(self): |
|
1624
|
from navegador.cluster.locking import DistributedLock |
|
1625
|
|
|
1626
|
r = MagicMock() |
|
1627
|
return DistributedLock("redis://localhost:6379", "test-lock", _redis_client=r), r |
|
1628
|
|
|
1629
|
def test_acquire_success(self): |
|
1630
|
lock, r = self._make() |
|
1631
|
r.set.return_value = True |
|
1632
|
assert lock.acquire() is True |
|
1633
|
assert lock._token is not None |
|
1634
|
|
|
1635
|
def test_acquire_failure(self): |
|
1636
|
lock, r = self._make() |
|
1637
|
r.set.return_value = None |
|
1638
|
assert lock.acquire() is False |
|
1639
|
|
|
1640
|
def test_release_when_token_matches(self): |
|
1641
|
lock, r = self._make() |
|
1642
|
lock._token = "mytoken" |
|
1643
|
r.get.return_value = b"mytoken" |
|
1644
|
lock.release() |
|
1645
|
r.delete.assert_called_once() |
|
1646
|
assert lock._token is None |
|
1647
|
|
|
1648
|
def test_release_when_token_not_held(self): |
|
1649
|
lock, r = self._make() |
|
1650
|
lock._token = None |
|
1651
|
lock.release() |
|
1652
|
r.delete.assert_not_called() |
|
1653
|
|
|
1654
|
def test_release_when_stored_token_differs(self): |
|
1655
|
lock, r = self._make() |
|
1656
|
lock._token = "mytoken" |
|
1657
|
r.get.return_value = b"other_token" |
|
1658
|
lock.release() |
|
1659
|
r.delete.assert_not_called() |
|
1660
|
|
|
1661
|
def test_context_manager_acquires_and_releases(self): |
|
1662
|
lock, r = self._make() |
|
1663
|
r.set.return_value = True |
|
1664
|
r.get.return_value = None # simulate already released |
|
1665
|
with lock: |
|
1666
|
pass |
|
1667
|
assert lock._token is None |
|
1668
|
|
|
1669
|
def test_context_manager_raises_on_timeout(self): |
|
1670
|
import time |
|
1671
|
from navegador.cluster.locking import LockTimeout |
|
1672
|
|
|
1673
|
lock, r = self._make() |
|
1674
|
r.set.return_value = None # never acquired |
|
1675
|
lock._timeout = 0 |
|
1676
|
lock._retry_interval = 0 |
|
1677
|
|
|
1678
|
with pytest.raises(LockTimeout): |
|
1679
|
with lock: |
|
1680
|
pass |
|
1681
|
|
|
1682
|
def test_client_lazy_init_raises_on_missing_redis(self): |
|
1683
|
from navegador.cluster.locking import DistributedLock |
|
1684
|
|
|
1685
|
lock = DistributedLock("redis://localhost:6379", "x") |
|
1686
|
with patch.dict("sys.modules", {"redis": None}): |
|
1687
|
with pytest.raises(ImportError, match="redis"): |
|
1688
|
lock._client() |
|
1689
|
|
|
1690
|
|
|
1691
|
# =========================================================================== |
|
1692
|
# navegador.cluster.observability (89% → target ~97%) |
|
1693
|
# =========================================================================== |
|
1694
|
|
|
1695
|
|
|
1696
|
class TestSwarmDashboard: |
|
1697
|
"""Cover lines 44-48, 93, 108, 160.""" |
|
1698
|
|
|
1699
|
def _make(self): |
|
1700
|
from navegador.cluster.observability import SwarmDashboard |
|
1701
|
|
|
1702
|
r = MagicMock() |
|
1703
|
r.keys.return_value = [] |
|
1704
|
r.get.return_value = None |
|
1705
|
return SwarmDashboard("redis://localhost:6379", _redis_client=r), r |
|
1706
|
|
|
1707
|
def test_register_agent(self): |
|
1708
|
dash, r = self._make() |
|
1709
|
dash.register_agent("agent1", metadata={"role": "ingester"}) |
|
1710
|
r.setex.assert_called_once() |
|
1711
|
|
|
1712
|
def test_register_agent_no_metadata(self): |
|
1713
|
dash, r = self._make() |
|
1714
|
dash.register_agent("agent1") |
|
1715
|
r.setex.assert_called_once() |
|
1716
|
|
|
1717
|
def test_agent_status_empty(self): |
|
1718
|
dash, r = self._make() |
|
1719
|
r.keys.return_value = [] |
|
1720
|
agents = dash.agent_status() |
|
1721
|
assert agents == [] |
|
1722
|
|
|
1723
|
def test_agent_status_returns_active_agents(self): |
|
1724
|
import json as _json |
|
1725
|
dash, r = self._make() |
|
1726
|
payload = {"agent_id": "a1", "last_seen": 12345, "state": "active"} |
|
1727
|
r.keys.return_value = [b"navegador:obs:agent:a1"] |
|
1728
|
r.get.return_value = _json.dumps(payload).encode() |
|
1729
|
agents = dash.agent_status() |
|
1730
|
assert len(agents) == 1 |
|
1731
|
assert agents[0]["agent_id"] == "a1" |
|
1732
|
|
|
1733
|
def test_task_metrics_default(self): |
|
1734
|
dash, r = self._make() |
|
1735
|
r.get.return_value = None |
|
1736
|
metrics = dash.task_metrics() |
|
1737
|
assert metrics == {"pending": 0, "active": 0, "completed": 0, "failed": 0} |
|
1738
|
|
|
1739
|
def test_task_metrics_from_redis(self): |
|
1740
|
import json as _json |
|
1741
|
dash, r = self._make() |
|
1742
|
stored = {"pending": 3, "active": 1, "completed": 10, "failed": 0} |
|
1743
|
r.get.return_value = _json.dumps(stored).encode() |
|
1744
|
metrics = dash.task_metrics() |
|
1745
|
assert metrics["pending"] == 3 |
|
1746
|
|
|
1747
|
def test_update_task_metrics(self): |
|
1748
|
import json as _json |
|
1749
|
dash, r = self._make() |
|
1750
|
r.get.return_value = _json.dumps( |
|
1751
|
{"pending": 0, "active": 0, "completed": 0, "failed": 0} |
|
1752
|
).encode() |
|
1753
|
dash.update_task_metrics(pending=5, active=2) |
|
1754
|
r.set.assert_called_once() |
|
1755
|
|
|
1756
|
def test_graph_metrics(self): |
|
1757
|
store = _make_store() |
|
1758
|
store.node_count.return_value = 42 |
|
1759
|
store.edge_count.return_value = 100 |
|
1760
|
dash, r = self._make() |
|
1761
|
result = dash.graph_metrics(store) |
|
1762
|
assert result["node_count"] == 42 |
|
1763
|
assert result["edge_count"] == 100 |
|
1764
|
|
|
1765
|
def test_to_json_structure(self): |
|
1766
|
import json as _json |
|
1767
|
dash, r = self._make() |
|
1768
|
r.keys.return_value = [] |
|
1769
|
r.get.side_effect = [None, None] # graph_meta + task_metrics |
|
1770
|
snapshot = _json.loads(dash.to_json()) |
|
1771
|
assert "agents" in snapshot |
|
1772
|
assert "task_metrics" in snapshot |
|
1773
|
|
|
1774
|
def test_client_lazy_init_raises_on_missing_redis(self): |
|
1775
|
from navegador.cluster.observability import SwarmDashboard |
|
1776
|
|
|
1777
|
dash = SwarmDashboard("redis://localhost:6379") |
|
1778
|
with patch.dict("sys.modules", {"redis": None}): |
|
1779
|
with pytest.raises(ImportError, match="redis"): |
|
1780
|
dash._client() |
|
1781
|
|
|
1782
|
|
|
1783
|
# =========================================================================== |
|
1784
|
# navegador.cluster.pubsub (86% → target ~97%) |
|
1785
|
# =========================================================================== |
|
1786
|
|
|
1787
|
|
|
1788
|
class TestGraphNotifier: |
|
1789
|
"""Cover lines 72-76, 159-162.""" |
|
1790
|
|
|
1791
|
def _make(self): |
|
1792
|
from navegador.cluster.pubsub import GraphNotifier |
|
1793
|
|
|
1794
|
r = MagicMock() |
|
1795
|
pubsub_mock = MagicMock() |
|
1796
|
pubsub_mock.listen.return_value = iter([]) |
|
1797
|
r.pubsub.return_value = pubsub_mock |
|
1798
|
r.publish.return_value = 1 |
|
1799
|
return GraphNotifier("redis://localhost:6379", redis_client=r), r |
|
1800
|
|
|
1801
|
def test_publish_returns_receiver_count(self): |
|
1802
|
from navegador.cluster.pubsub import EventType |
|
1803
|
|
|
1804
|
notifier, r = self._make() |
|
1805
|
count = notifier.publish(EventType.NODE_CREATED, {"name": "foo"}) |
|
1806
|
assert count == 1 |
|
1807
|
r.publish.assert_called_once() |
|
1808
|
|
|
1809
|
def test_publish_with_string_event_type(self): |
|
1810
|
notifier, r = self._make() |
|
1811
|
count = notifier.publish("custom_event", {"key": "val"}) |
|
1812
|
assert count == 1 |
|
1813
|
|
|
1814
|
def test_subscribe_run_in_thread(self): |
|
1815
|
from navegador.cluster.pubsub import EventType |
|
1816
|
|
|
1817
|
notifier, r = self._make() |
|
1818
|
import json as _json |
|
1819
|
import threading |
|
1820
|
|
|
1821
|
# Return one message then stop |
|
1822
|
msg = { |
|
1823
|
"type": "message", |
|
1824
|
"data": _json.dumps({"event_type": "node_created", "data": {"k": "v"}}).encode(), |
|
1825
|
} |
|
1826
|
r.pubsub.return_value.listen.return_value = iter([msg]) |
|
1827
|
|
|
1828
|
received = [] |
|
1829
|
|
|
1830
|
def callback(et, data): |
|
1831
|
received.append((et, data)) |
|
1832
|
|
|
1833
|
t = notifier.subscribe([EventType.NODE_CREATED], callback, run_in_thread=True) |
|
1834
|
assert isinstance(t, threading.Thread) |
|
1835
|
t.join(timeout=2) |
|
1836
|
assert received == [("node_created", {"k": "v"})] |
|
1837
|
|
|
1838
|
def test_close(self): |
|
1839
|
notifier, r = self._make() |
|
1840
|
notifier.close() |
|
1841
|
r.close.assert_called_once() |
|
1842
|
|
|
1843
|
def test_close_ignores_exception(self): |
|
1844
|
notifier, r = self._make() |
|
1845
|
r.close.side_effect = Exception("closed") |
|
1846
|
notifier.close() # should not raise |
|
1847
|
|
|
1848
|
def test_connect_redis_raises_on_missing_dep(self): |
|
1849
|
from navegador.cluster.pubsub import GraphNotifier |
|
1850
|
|
|
1851
|
with patch.dict("sys.modules", {"redis": None}): |
|
1852
|
with pytest.raises(ImportError, match="redis"): |
|
1853
|
GraphNotifier._connect_redis("redis://localhost") |
|
1854
|
|
|
1855
|
|
|
1856
|
# =========================================================================== |
|
1857
|
# navegador.ingestion.ruby — extra branches (66% → target ~85%) |
|
1858
|
# =========================================================================== |
|
1859
|
|
|
1860
|
|
|
1861
|
class TestRubyParserBranches: |
|
1862
|
"""Exercise _handle_class superclass, _handle_module body, _maybe_handle_require, |
|
1863
|
_extract_calls and fallback paths.""" |
|
1864
|
|
|
1865
|
def _make_parser(self): |
|
1866
|
with _mock_ts("tree_sitter_ruby"): |
|
1867
|
from navegador.ingestion.ruby import RubyParser |
|
1868
|
return RubyParser() |
|
1869
|
|
|
1870
|
def test_handle_class_with_superclass(self): |
|
1871
|
with _mock_ts("tree_sitter_ruby"): |
|
1872
|
from navegador.ingestion.ruby import RubyParser |
|
1873
|
|
|
1874
|
parser = RubyParser() |
|
1875
|
store = _make_store() |
|
1876
|
|
|
1877
|
name_node = _text_node(b"MyClass") |
|
1878
|
superclass_node = _text_node(b"< BaseClass", "constant") |
|
1879
|
class_node = MockNode("class", children=[name_node, superclass_node]) |
|
1880
|
class_node.set_field("name", name_node) |
|
1881
|
class_node.set_field("superclass", superclass_node) |
|
1882
|
|
|
1883
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
1884
|
parser._handle_class(class_node, b"class MyClass < BaseClass\nend", "f.rb", store, stats) |
|
1885
|
assert stats["classes"] == 1 |
|
1886
|
assert stats["edges"] >= 2 # CONTAINS + INHERITS |
|
1887
|
|
|
1888
|
def test_handle_class_no_name_node(self): |
|
1889
|
with _mock_ts("tree_sitter_ruby"): |
|
1890
|
from navegador.ingestion.ruby import RubyParser |
|
1891
|
|
|
1892
|
parser = RubyParser() |
|
1893
|
store = _make_store() |
|
1894
|
anon_class = MockNode("class") |
|
1895
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
1896
|
parser._handle_class(anon_class, b"class; end", "f.rb", store, stats) |
|
1897
|
assert stats["classes"] == 0 |
|
1898
|
|
|
1899
|
def test_handle_module_with_body(self): |
|
1900
|
with _mock_ts("tree_sitter_ruby"): |
|
1901
|
from navegador.ingestion.ruby import RubyParser |
|
1902
|
|
|
1903
|
parser = RubyParser() |
|
1904
|
store = _make_store() |
|
1905
|
|
|
1906
|
name_node = _text_node(b"MyModule") |
|
1907
|
method_name = _text_node(b"my_method") |
|
1908
|
method_node = MockNode("method", children=[method_name]) |
|
1909
|
method_node.set_field("name", method_name) |
|
1910
|
body_node = MockNode("body_statement", children=[method_node]) |
|
1911
|
mod_node = MockNode("module", children=[name_node, body_node]) |
|
1912
|
mod_node.set_field("name", name_node) |
|
1913
|
# body found via body_statement child (no "body" field) |
|
1914
|
|
|
1915
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
1916
|
src = b"module MyModule\n def my_method; end\nend" |
|
1917
|
parser._handle_module(mod_node, src, "f.rb", store, stats) |
|
1918
|
assert stats["classes"] == 1 |
|
1919
|
|
|
1920
|
def test_handle_method_standalone_function(self): |
|
1921
|
with _mock_ts("tree_sitter_ruby"): |
|
1922
|
from navegador.ingestion.ruby import RubyParser |
|
1923
|
|
|
1924
|
parser = RubyParser() |
|
1925
|
store = _make_store() |
|
1926
|
|
|
1927
|
name_node = _text_node(b"standalone_fn") |
|
1928
|
fn_node = MockNode("method", children=[name_node]) |
|
1929
|
fn_node.set_field("name", name_node) |
|
1930
|
|
|
1931
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
1932
|
parser._handle_method(fn_node, b"def standalone_fn; end", "f.rb", store, stats, class_name=None) |
|
1933
|
assert stats["functions"] == 1 |
|
1934
|
# No class → Function node |
|
1935
|
create_call = store.create_node.call_args_list[-1] |
|
1936
|
from navegador.graph.schema import NodeLabel |
|
1937
|
assert create_call[0][0] == NodeLabel.Function |
|
1938
|
|
|
1939
|
def test_handle_method_no_name(self): |
|
1940
|
with _mock_ts("tree_sitter_ruby"): |
|
1941
|
from navegador.ingestion.ruby import RubyParser |
|
1942
|
|
|
1943
|
parser = RubyParser() |
|
1944
|
store = _make_store() |
|
1945
|
anon_method = MockNode("method") |
|
1946
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
1947
|
parser._handle_method(anon_method, b"", "f.rb", store, stats, class_name=None) |
|
1948
|
assert stats["functions"] == 0 |
|
1949
|
|
|
1950
|
def test_maybe_handle_require(self): |
|
1951
|
with _mock_ts("tree_sitter_ruby"): |
|
1952
|
from navegador.ingestion.ruby import RubyParser |
|
1953
|
|
|
1954
|
parser = RubyParser() |
|
1955
|
store = _make_store() |
|
1956
|
|
|
1957
|
method_node = _text_node(b"require", "identifier") |
|
1958
|
string_node = _text_node(b"'json'", "string") |
|
1959
|
args_node = MockNode("argument_list", children=[string_node]) |
|
1960
|
call_node = MockNode("call", children=[method_node, args_node]) |
|
1961
|
call_node.set_field("method", method_node) |
|
1962
|
call_node.set_field("arguments", args_node) |
|
1963
|
|
|
1964
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
1965
|
src = b"require 'json'" |
|
1966
|
parser._maybe_handle_require(call_node, src, "f.rb", store, stats) |
|
1967
|
store.create_node.assert_called_once() |
|
1968
|
|
|
1969
|
def test_maybe_handle_require_skips_non_require(self): |
|
1970
|
with _mock_ts("tree_sitter_ruby"): |
|
1971
|
from navegador.ingestion.ruby import RubyParser |
|
1972
|
|
|
1973
|
parser = RubyParser() |
|
1974
|
store = _make_store() |
|
1975
|
|
|
1976
|
method_node = _text_node(b"puts", "identifier") |
|
1977
|
call_node = MockNode("call", children=[method_node]) |
|
1978
|
call_node.set_field("method", method_node) |
|
1979
|
|
|
1980
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
1981
|
parser._maybe_handle_require(call_node, b"puts 'hi'", "f.rb", store, stats) |
|
1982
|
store.create_node.assert_not_called() |
|
1983
|
|
|
1984
|
def test_extract_calls(self): |
|
1985
|
with _mock_ts("tree_sitter_ruby"): |
|
1986
|
from navegador.ingestion.ruby import RubyParser |
|
1987
|
from navegador.graph.schema import NodeLabel |
|
1988
|
|
|
1989
|
parser = RubyParser() |
|
1990
|
store = _make_store() |
|
1991
|
|
|
1992
|
callee_node = _text_node(b"helper", "identifier") |
|
1993
|
call_node = MockNode("call", children=[callee_node]) |
|
1994
|
call_node.set_field("method", callee_node) |
|
1995
|
body_node = MockNode("body_statement", children=[call_node]) |
|
1996
|
fn_node = MockNode("method", children=[body_node]) |
|
1997
|
|
|
1998
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
1999
|
parser._extract_calls(fn_node, b"def foo; helper; end", "f.rb", "foo", NodeLabel.Function, store, stats) |
|
2000
|
store.create_edge.assert_called() |
|
2001
|
|
|
2002
|
|
|
2003
|
# =========================================================================== |
|
2004
|
# navegador.ingestion.cpp — extra branches (73% → target ~90%) |
|
2005
|
# =========================================================================== |
|
2006
|
|
|
2007
|
|
|
2008
|
class TestCppParserBranches: |
|
2009
|
def _make_parser(self): |
|
2010
|
with _mock_ts("tree_sitter_cpp"): |
|
2011
|
from navegador.ingestion.cpp import CppParser |
|
2012
|
return CppParser() |
|
2013
|
|
|
2014
|
def test_handle_class_with_inheritance(self): |
|
2015
|
with _mock_ts("tree_sitter_cpp"): |
|
2016
|
from navegador.ingestion.cpp import CppParser |
|
2017
|
|
|
2018
|
parser = CppParser() |
|
2019
|
store = _make_store() |
|
2020
|
|
|
2021
|
name_node = _text_node(b"MyClass", "type_identifier") |
|
2022
|
parent_node = _text_node(b"BaseClass", "type_identifier") |
|
2023
|
base_clause_node = MockNode("base_class_clause", children=[parent_node]) |
|
2024
|
class_node = MockNode("class_specifier", children=[name_node, base_clause_node]) |
|
2025
|
class_node.set_field("name", name_node) |
|
2026
|
class_node.set_field("base_clause", base_clause_node) |
|
2027
|
|
|
2028
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2029
|
parser._handle_class(class_node, b"class MyClass : public BaseClass {};", "f.cpp", store, stats) |
|
2030
|
assert stats["classes"] == 1 |
|
2031
|
assert stats["edges"] >= 2 |
|
2032
|
|
|
2033
|
def test_handle_class_no_name(self): |
|
2034
|
with _mock_ts("tree_sitter_cpp"): |
|
2035
|
from navegador.ingestion.cpp import CppParser |
|
2036
|
|
|
2037
|
parser = CppParser() |
|
2038
|
store = _make_store() |
|
2039
|
anon_class = MockNode("class_specifier") |
|
2040
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2041
|
parser._handle_class(anon_class, b"struct {};", "f.cpp", store, stats) |
|
2042
|
assert stats["classes"] == 0 |
|
2043
|
|
|
2044
|
def test_handle_function_with_class_name(self): |
|
2045
|
with _mock_ts("tree_sitter_cpp"): |
|
2046
|
from navegador.ingestion.cpp import CppParser |
|
2047
|
|
|
2048
|
parser = CppParser() |
|
2049
|
store = _make_store() |
|
2050
|
|
|
2051
|
fn_name_node = _text_node(b"myMethod", "identifier") |
|
2052
|
declarator = MockNode("function_declarator", children=[fn_name_node]) |
|
2053
|
declarator.set_field("declarator", fn_name_node) |
|
2054
|
body = MockNode("compound_statement") |
|
2055
|
fn_node = MockNode("function_definition", children=[declarator, body]) |
|
2056
|
fn_node.set_field("declarator", declarator) |
|
2057
|
fn_node.set_field("body", body) |
|
2058
|
|
|
2059
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2060
|
parser._handle_function(fn_node, b"void myMethod() {}", "f.cpp", store, stats, class_name="MyClass") |
|
2061
|
assert stats["functions"] == 1 |
|
2062
|
from navegador.graph.schema import NodeLabel |
|
2063
|
assert store.create_node.call_args[0][0] == NodeLabel.Method |
|
2064
|
|
|
2065
|
def test_extract_function_name_qualified(self): |
|
2066
|
with _mock_ts("tree_sitter_cpp"): |
|
2067
|
from navegador.ingestion.cpp import CppParser |
|
2068
|
|
|
2069
|
parser = CppParser() |
|
2070
|
src = b"method" |
|
2071
|
name_node = MockNode("identifier", start_byte=0, end_byte=len(src)) |
|
2072
|
qualified = MockNode("qualified_identifier", children=[name_node]) |
|
2073
|
qualified.set_field("name", name_node) |
|
2074
|
result = parser._extract_function_name(qualified, src) |
|
2075
|
assert result == "method" |
|
2076
|
|
|
2077
|
def test_extract_function_name_qualified_fallback(self): |
|
2078
|
with _mock_ts("tree_sitter_cpp"): |
|
2079
|
from navegador.ingestion.cpp import CppParser |
|
2080
|
|
|
2081
|
parser = CppParser() |
|
2082
|
src = b"method" |
|
2083
|
id_node = MockNode("identifier", start_byte=0, end_byte=len(src)) |
|
2084
|
qualified = MockNode("qualified_identifier", children=[id_node]) |
|
2085
|
# No name field → fallback to last identifier child |
|
2086
|
result = parser._extract_function_name(qualified, src) |
|
2087
|
assert result == "method" |
|
2088
|
|
|
2089
|
def test_extract_function_name_pointer_declarator(self): |
|
2090
|
with _mock_ts("tree_sitter_cpp"): |
|
2091
|
from navegador.ingestion.cpp import CppParser |
|
2092
|
|
|
2093
|
parser = CppParser() |
|
2094
|
src = b"fp" |
|
2095
|
inner = MockNode("identifier", start_byte=0, end_byte=len(src)) |
|
2096
|
ptr_decl = MockNode("pointer_declarator", children=[inner]) |
|
2097
|
ptr_decl.set_field("declarator", inner) |
|
2098
|
result = parser._extract_function_name(ptr_decl, src) |
|
2099
|
assert result == "fp" |
|
2100
|
|
|
2101
|
def test_extract_function_name_fallback_child(self): |
|
2102
|
with _mock_ts("tree_sitter_cpp"): |
|
2103
|
from navegador.ingestion.cpp import CppParser |
|
2104
|
|
|
2105
|
parser = CppParser() |
|
2106
|
src = b"fallbackFn" |
|
2107
|
id_node = MockNode("identifier", start_byte=0, end_byte=len(src)) |
|
2108
|
unknown_decl = MockNode("unknown_declarator", children=[id_node]) |
|
2109
|
result = parser._extract_function_name(unknown_decl, src) |
|
2110
|
assert result == "fallbackFn" |
|
2111
|
|
|
2112
|
def test_extract_function_name_none(self): |
|
2113
|
with _mock_ts("tree_sitter_cpp"): |
|
2114
|
from navegador.ingestion.cpp import CppParser |
|
2115
|
|
|
2116
|
parser = CppParser() |
|
2117
|
assert parser._extract_function_name(None, b"") is None |
|
2118
|
|
|
2119
|
def test_handle_include(self): |
|
2120
|
with _mock_ts("tree_sitter_cpp"): |
|
2121
|
from navegador.ingestion.cpp import CppParser |
|
2122
|
|
|
2123
|
parser = CppParser() |
|
2124
|
store = _make_store() |
|
2125
|
|
|
2126
|
path_node = _text_node(b'"vector"', "string_literal") |
|
2127
|
include_node = MockNode("preproc_include", children=[path_node]) |
|
2128
|
include_node.set_field("path", path_node) |
|
2129
|
|
|
2130
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2131
|
parser._handle_include(include_node, b'#include "vector"', "f.cpp", store, stats) |
|
2132
|
store.create_node.assert_called_once() |
|
2133
|
|
|
2134
|
def test_namespace_recurse(self): |
|
2135
|
with _mock_ts("tree_sitter_cpp"): |
|
2136
|
from navegador.ingestion.cpp import CppParser |
|
2137
|
|
|
2138
|
parser = CppParser() |
|
2139
|
store = _make_store() |
|
2140
|
|
|
2141
|
# namespace body contains a function |
|
2142
|
fn_name = _text_node(b"inner_fn", "identifier") |
|
2143
|
fn_decl = MockNode("function_declarator", children=[fn_name]) |
|
2144
|
fn_decl.set_field("declarator", fn_name) |
|
2145
|
fn_body = MockNode("compound_statement") |
|
2146
|
fn_def = MockNode("function_definition", children=[fn_decl, fn_body]) |
|
2147
|
fn_def.set_field("declarator", fn_decl) |
|
2148
|
|
|
2149
|
decl_list = MockNode("declaration_list", children=[fn_def]) |
|
2150
|
ns_node = MockNode("namespace_definition", children=[decl_list]) |
|
2151
|
|
|
2152
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2153
|
parser._walk(ns_node, b"namespace ns { void inner_fn(){} }", "f.cpp", store, stats, class_name=None) |
|
2154
|
assert stats["functions"] == 1 |
|
2155
|
|
|
2156
|
|
|
2157
|
# =========================================================================== |
|
2158
|
# navegador.ingestion.csharp — extra branches (79% → target ~90%) |
|
2159
|
# =========================================================================== |
|
2160
|
|
|
2161
|
|
|
2162
|
class TestCSharpParserBranches: |
|
2163
|
def test_handle_class_with_bases(self): |
|
2164
|
with _mock_ts("tree_sitter_c_sharp"): |
|
2165
|
from navegador.ingestion.csharp import CSharpParser |
|
2166
|
|
|
2167
|
parser = CSharpParser() |
|
2168
|
store = _make_store() |
|
2169
|
|
|
2170
|
name_node = _text_node(b"MyService", "identifier") |
|
2171
|
base_id = _text_node(b"IService", "identifier") |
|
2172
|
bases_node = MockNode("base_list", children=[base_id]) |
|
2173
|
class_node = MockNode("class_declaration", children=[name_node, bases_node]) |
|
2174
|
class_node.set_field("name", name_node) |
|
2175
|
class_node.set_field("bases", bases_node) |
|
2176
|
|
|
2177
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2178
|
parser._handle_class(class_node, b"class MyService : IService {}", "f.cs", store, stats) |
|
2179
|
assert stats["classes"] == 1 |
|
2180
|
assert stats["edges"] >= 2 |
|
2181
|
|
|
2182
|
def test_handle_class_no_name(self): |
|
2183
|
with _mock_ts("tree_sitter_c_sharp"): |
|
2184
|
from navegador.ingestion.csharp import CSharpParser |
|
2185
|
|
|
2186
|
parser = CSharpParser() |
|
2187
|
store = _make_store() |
|
2188
|
anon = MockNode("class_declaration") |
|
2189
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2190
|
parser._handle_class(anon, b"", "f.cs", store, stats) |
|
2191
|
assert stats["classes"] == 0 |
|
2192
|
|
|
2193
|
def test_handle_method_standalone(self): |
|
2194
|
with _mock_ts("tree_sitter_c_sharp"): |
|
2195
|
from navegador.ingestion.csharp import CSharpParser |
|
2196
|
|
|
2197
|
parser = CSharpParser() |
|
2198
|
store = _make_store() |
|
2199
|
|
|
2200
|
name_node = _text_node(b"DoWork", "identifier") |
|
2201
|
fn_node = MockNode("method_declaration", children=[name_node]) |
|
2202
|
fn_node.set_field("name", name_node) |
|
2203
|
|
|
2204
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2205
|
parser._handle_method(fn_node, b"void DoWork() {}", "f.cs", store, stats, class_name=None) |
|
2206
|
assert stats["functions"] == 1 |
|
2207
|
|
|
2208
|
def test_handle_method_no_name(self): |
|
2209
|
with _mock_ts("tree_sitter_c_sharp"): |
|
2210
|
from navegador.ingestion.csharp import CSharpParser |
|
2211
|
|
|
2212
|
parser = CSharpParser() |
|
2213
|
store = _make_store() |
|
2214
|
anon = MockNode("method_declaration") |
|
2215
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2216
|
parser._handle_method(anon, b"", "f.cs", store, stats, class_name=None) |
|
2217
|
assert stats["functions"] == 0 |
|
2218
|
|
|
2219
|
def test_handle_using(self): |
|
2220
|
with _mock_ts("tree_sitter_c_sharp"): |
|
2221
|
from navegador.ingestion.csharp import CSharpParser |
|
2222
|
|
|
2223
|
parser = CSharpParser() |
|
2224
|
store = _make_store() |
|
2225
|
|
|
2226
|
src = b"using System.Collections.Generic;" |
|
2227
|
using_node = MockNode( |
|
2228
|
"using_directive", |
|
2229
|
start_byte=0, |
|
2230
|
end_byte=len(src), |
|
2231
|
start_point=(0, 0), |
|
2232
|
end_point=(0, len(src)), |
|
2233
|
) |
|
2234
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2235
|
parser._handle_using(using_node, src, "f.cs", store, stats) |
|
2236
|
store.create_node.assert_called_once() |
|
2237
|
|
|
2238
|
def test_extract_calls(self): |
|
2239
|
with _mock_ts("tree_sitter_c_sharp"): |
|
2240
|
from navegador.ingestion.csharp import CSharpParser |
|
2241
|
from navegador.graph.schema import NodeLabel |
|
2242
|
|
|
2243
|
parser = CSharpParser() |
|
2244
|
store = _make_store() |
|
2245
|
|
|
2246
|
callee_node = _text_node(b"DoWork", "identifier") |
|
2247
|
invoke_node = MockNode("invocation_expression", children=[callee_node]) |
|
2248
|
invoke_node.set_field("function", callee_node) |
|
2249
|
block_node = MockNode("block", children=[invoke_node]) |
|
2250
|
fn_node = MockNode("method_declaration", children=[block_node]) |
|
2251
|
fn_node.set_field("body", block_node) |
|
2252
|
|
|
2253
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2254
|
parser._extract_calls(fn_node, b"DoWork()", "f.cs", "Run", NodeLabel.Method, store, stats) |
|
2255
|
store.create_edge.assert_called() |
|
2256
|
|
|
2257
|
|
|
2258
|
# =========================================================================== |
|
2259
|
# navegador.ingestion.kotlin — extra branches (79% → target ~90%) |
|
2260
|
# =========================================================================== |
|
2261
|
|
|
2262
|
|
|
2263
|
class TestKotlinParserBranches: |
|
2264
|
def test_handle_class_no_name(self): |
|
2265
|
with _mock_ts("tree_sitter_kotlin"): |
|
2266
|
from navegador.ingestion.kotlin import KotlinParser |
|
2267
|
|
|
2268
|
parser = KotlinParser() |
|
2269
|
store = _make_store() |
|
2270
|
anon = MockNode("class_declaration") |
|
2271
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2272
|
parser._handle_class(anon, b"", "f.kt", store, stats) |
|
2273
|
assert stats["classes"] == 0 |
|
2274
|
|
|
2275
|
def test_handle_class_with_body(self): |
|
2276
|
with _mock_ts("tree_sitter_kotlin"): |
|
2277
|
from navegador.ingestion.kotlin import KotlinParser |
|
2278
|
|
|
2279
|
parser = KotlinParser() |
|
2280
|
store = _make_store() |
|
2281
|
|
|
2282
|
name_node = _text_node(b"MyClass", "simple_identifier") |
|
2283
|
fn_name = _text_node(b"doSomething", "simple_identifier") |
|
2284
|
fn_node = MockNode("function_declaration", children=[fn_name]) |
|
2285
|
fn_node.set_field("name", fn_name) |
|
2286
|
body_node = MockNode("class_body", children=[fn_node]) |
|
2287
|
class_node = MockNode("class_declaration", children=[name_node, body_node]) |
|
2288
|
class_node.set_field("name", name_node) |
|
2289
|
class_node.set_field("body", body_node) |
|
2290
|
|
|
2291
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2292
|
src = b"class MyClass { fun doSomething() {} }" |
|
2293
|
parser._handle_class(class_node, src, "f.kt", store, stats) |
|
2294
|
assert stats["classes"] == 1 |
|
2295
|
assert stats["functions"] == 1 |
|
2296
|
|
|
2297
|
def test_handle_function_no_name(self): |
|
2298
|
with _mock_ts("tree_sitter_kotlin"): |
|
2299
|
from navegador.ingestion.kotlin import KotlinParser |
|
2300
|
|
|
2301
|
parser = KotlinParser() |
|
2302
|
store = _make_store() |
|
2303
|
anon = MockNode("function_declaration") |
|
2304
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2305
|
parser._handle_function(anon, b"", "f.kt", store, stats, class_name=None) |
|
2306
|
assert stats["functions"] == 0 |
|
2307
|
|
|
2308
|
def test_handle_import(self): |
|
2309
|
with _mock_ts("tree_sitter_kotlin"): |
|
2310
|
from navegador.ingestion.kotlin import KotlinParser |
|
2311
|
|
|
2312
|
parser = KotlinParser() |
|
2313
|
store = _make_store() |
|
2314
|
|
|
2315
|
src = b"import kotlin.collections.List" |
|
2316
|
import_node = MockNode( |
|
2317
|
"import_header", |
|
2318
|
start_byte=0, |
|
2319
|
end_byte=len(src), |
|
2320
|
start_point=(0, 0), |
|
2321
|
end_point=(0, len(src)), |
|
2322
|
) |
|
2323
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2324
|
parser._handle_import(import_node, src, "f.kt", store, stats) |
|
2325
|
store.create_node.assert_called_once() |
|
2326
|
|
|
2327
|
|
|
2328
|
# =========================================================================== |
|
2329
|
# navegador.ingestion.php — extra branches (79% → target ~90%) |
|
2330
|
# =========================================================================== |
|
2331
|
|
|
2332
|
|
|
2333
|
class TestPHPParserBranches: |
|
2334
|
def test_handle_class_no_name(self): |
|
2335
|
with _mock_ts("tree_sitter_php"): |
|
2336
|
from navegador.ingestion.php import PHPParser |
|
2337
|
|
|
2338
|
parser = PHPParser() |
|
2339
|
store = _make_store() |
|
2340
|
anon = MockNode("class_declaration") |
|
2341
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2342
|
parser._handle_class(anon, b"", "f.php", store, stats) |
|
2343
|
assert stats["classes"] == 0 |
|
2344
|
|
|
2345
|
def test_handle_function_no_name(self): |
|
2346
|
with _mock_ts("tree_sitter_php"): |
|
2347
|
from navegador.ingestion.php import PHPParser |
|
2348
|
|
|
2349
|
parser = PHPParser() |
|
2350
|
store = _make_store() |
|
2351
|
anon = MockNode("function_definition") |
|
2352
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2353
|
parser._handle_function(anon, b"", "f.php", store, stats, class_name=None) |
|
2354
|
assert stats["functions"] == 0 |
|
2355
|
|
|
2356
|
def test_handle_class_with_body_methods(self): |
|
2357
|
with _mock_ts("tree_sitter_php"): |
|
2358
|
from navegador.ingestion.php import PHPParser |
|
2359
|
|
|
2360
|
parser = PHPParser() |
|
2361
|
store = _make_store() |
|
2362
|
|
|
2363
|
name_node = _text_node(b"MyController", "name") |
|
2364
|
fn_name = _text_node(b"index", "name") |
|
2365
|
method_node = MockNode("method_declaration", children=[fn_name]) |
|
2366
|
method_node.set_field("name", fn_name) |
|
2367
|
body_node = MockNode("declaration_list", children=[method_node]) |
|
2368
|
class_node = MockNode("class_declaration", children=[name_node, body_node]) |
|
2369
|
class_node.set_field("name", name_node) |
|
2370
|
class_node.set_field("body", body_node) |
|
2371
|
|
|
2372
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2373
|
src = b"class MyController { public function index() {} }" |
|
2374
|
parser._handle_class(class_node, src, "f.php", store, stats) |
|
2375
|
assert stats["classes"] == 1 |
|
2376
|
assert stats["functions"] >= 1 |
|
2377
|
|
|
2378
|
|
|
2379
|
# =========================================================================== |
|
2380
|
# navegador.ingestion.swift — extra branches (79% → target ~90%) |
|
2381
|
# =========================================================================== |
|
2382
|
|
|
2383
|
|
|
2384
|
class TestSwiftParserBranches: |
|
2385
|
def test_handle_class_no_name(self): |
|
2386
|
with _mock_ts("tree_sitter_swift"): |
|
2387
|
from navegador.ingestion.swift import SwiftParser |
|
2388
|
|
|
2389
|
parser = SwiftParser() |
|
2390
|
store = _make_store() |
|
2391
|
anon = MockNode("class_declaration") |
|
2392
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2393
|
parser._handle_class(anon, b"", "f.swift", store, stats) |
|
2394
|
assert stats["classes"] == 0 |
|
2395
|
|
|
2396
|
def test_handle_function_no_name(self): |
|
2397
|
with _mock_ts("tree_sitter_swift"): |
|
2398
|
from navegador.ingestion.swift import SwiftParser |
|
2399
|
|
|
2400
|
parser = SwiftParser() |
|
2401
|
store = _make_store() |
|
2402
|
anon = MockNode("function_declaration") |
|
2403
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2404
|
parser._handle_function(anon, b"", "f.swift", store, stats, class_name=None) |
|
2405
|
assert stats["functions"] == 0 |
|
2406
|
|
|
2407
|
def test_handle_import(self): |
|
2408
|
with _mock_ts("tree_sitter_swift"): |
|
2409
|
from navegador.ingestion.swift import SwiftParser |
|
2410
|
|
|
2411
|
parser = SwiftParser() |
|
2412
|
store = _make_store() |
|
2413
|
|
|
2414
|
src = b"import Foundation" |
|
2415
|
import_node = MockNode( |
|
2416
|
"import_declaration", |
|
2417
|
start_byte=0, |
|
2418
|
end_byte=len(src), |
|
2419
|
start_point=(0, 0), |
|
2420
|
end_point=(0, len(src)), |
|
2421
|
) |
|
2422
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2423
|
parser._handle_import(import_node, src, "f.swift", store, stats) |
|
2424
|
store.create_node.assert_called_once() |
|
2425
|
|
|
2426
|
def test_handle_class_with_body(self): |
|
2427
|
with _mock_ts("tree_sitter_swift"): |
|
2428
|
from navegador.ingestion.swift import SwiftParser |
|
2429
|
|
|
2430
|
parser = SwiftParser() |
|
2431
|
store = _make_store() |
|
2432
|
|
|
2433
|
name_node = _text_node(b"MyView", "type_identifier") |
|
2434
|
fn_name = _text_node(b"body", "simple_identifier") |
|
2435
|
fn_node = MockNode("function_declaration", children=[fn_name]) |
|
2436
|
fn_node.set_field("name", fn_name) |
|
2437
|
body_node = MockNode("class_body", children=[fn_node]) |
|
2438
|
class_node = MockNode("class_declaration", children=[name_node, body_node]) |
|
2439
|
class_node.set_field("name", name_node) |
|
2440
|
class_node.set_field("body", body_node) |
|
2441
|
|
|
2442
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2443
|
src = b"class MyView { func body() {} }" |
|
2444
|
parser._handle_class(class_node, src, "f.swift", store, stats) |
|
2445
|
assert stats["classes"] == 1 |
|
2446
|
assert stats["functions"] == 1 |
|
2447
|
|
|
2448
|
|
|
2449
|
# =========================================================================== |
|
2450
|
# navegador.ingestion.c — extra branches (76% → target ~90%) |
|
2451
|
# =========================================================================== |
|
2452
|
|
|
2453
|
|
|
2454
|
class TestCParserBranches: |
|
2455
|
def test_handle_function(self): |
|
2456
|
with _mock_ts("tree_sitter_c"): |
|
2457
|
from navegador.ingestion.c import CParser |
|
2458
|
|
|
2459
|
parser = CParser() |
|
2460
|
store = _make_store() |
|
2461
|
|
|
2462
|
fn_name_node = _text_node(b"myFunc", "identifier") |
|
2463
|
declarator = MockNode("function_declarator", children=[fn_name_node]) |
|
2464
|
declarator.set_field("declarator", fn_name_node) |
|
2465
|
body = MockNode("compound_statement") |
|
2466
|
fn_node = MockNode("function_definition", children=[declarator, body]) |
|
2467
|
fn_node.set_field("declarator", declarator) |
|
2468
|
fn_node.set_field("body", body) |
|
2469
|
|
|
2470
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2471
|
parser._handle_function(fn_node, b"void myFunc() {}", "f.c", store, stats) |
|
2472
|
assert stats["functions"] == 1 |
|
2473
|
|
|
2474
|
def test_handle_struct(self): |
|
2475
|
with _mock_ts("tree_sitter_c"): |
|
2476
|
from navegador.ingestion.c import CParser |
|
2477
|
|
|
2478
|
parser = CParser() |
|
2479
|
store = _make_store() |
|
2480
|
|
|
2481
|
name_node = _text_node(b"Point", "type_identifier") |
|
2482
|
struct_node = MockNode("struct_specifier", children=[name_node]) |
|
2483
|
struct_node.set_field("name", name_node) |
|
2484
|
|
|
2485
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2486
|
parser._handle_struct(struct_node, b"struct Point {};", "f.c", store, stats) |
|
2487
|
assert stats["classes"] == 1 |
|
2488
|
|
|
2489
|
def test_handle_struct_no_name(self): |
|
2490
|
with _mock_ts("tree_sitter_c"): |
|
2491
|
from navegador.ingestion.c import CParser |
|
2492
|
|
|
2493
|
parser = CParser() |
|
2494
|
store = _make_store() |
|
2495
|
anon = MockNode("struct_specifier") |
|
2496
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2497
|
parser._handle_struct(anon, b"", "f.c", store, stats) |
|
2498
|
assert stats["classes"] == 0 |
|
2499
|
|
|
2500
|
def test_handle_include(self): |
|
2501
|
with _mock_ts("tree_sitter_c"): |
|
2502
|
from navegador.ingestion.c import CParser |
|
2503
|
|
|
2504
|
parser = CParser() |
|
2505
|
store = _make_store() |
|
2506
|
|
|
2507
|
path_node = _text_node(b"<stdio.h>", "system_lib_string") |
|
2508
|
include_node = MockNode("preproc_include", children=[path_node]) |
|
2509
|
include_node.set_field("path", path_node) |
|
2510
|
|
|
2511
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2512
|
parser._handle_include(include_node, b"#include <stdio.h>", "f.c", store, stats) |
|
2513
|
store.create_node.assert_called_once() |
|
2514
|
|
|
2515
|
def test_handle_function_no_name(self): |
|
2516
|
with _mock_ts("tree_sitter_c"): |
|
2517
|
from navegador.ingestion.c import CParser |
|
2518
|
|
|
2519
|
parser = CParser() |
|
2520
|
store = _make_store() |
|
2521
|
|
|
2522
|
fn_node = MockNode("function_definition") |
|
2523
|
# No declarator field → _extract_function_name returns None |
|
2524
|
stats = {"functions": 0, "classes": 0, "edges": 0} |
|
2525
|
parser._handle_function(fn_node, b"", "f.c", store, stats) |
|
2526
|
assert stats["functions"] == 0 |
|
2527
|
|