Skip to content

Commit d5e7965

Browse files
Lumi-nodeclaude
andauthored
test: guard loops that would assert nothing over an empty collection (#3)
Completes the vacuity pass. 24 tests looped over a computed collection with no check that it contained anything, so each would have reported success against an implementation that returned nothing at all. Mutation-verified: emptying the language registry now fails 17 of them, and suppressing orphan detection fails two more. Both previously passed. The remaining seven loop over locally-defined literals of four to six elements, which cannot be empty by accident, and are left alone -- a guard there would be noise rather than protection. Also restores a test that had quietly shrunk. `test_no_analyzer_declares_an_option_it_ignores` documents itself as checking "across all analyzers at once" and was iterating a hardcoded one-element list, so it verified a tenth of what it claimed while still passing. It reads the registry again, asserts the registry is fully populated, and now fails when a dead option is added to any analyzer. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0751ecc commit d5e7965

9 files changed

Lines changed: 56 additions & 1 deletion

tests/test_analyzers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def test_orphan_ids_have_extensions(
3434
result = boundaries.run(ctx)
3535

3636
orphans = result.get("orphans", [])
37+
# A loop over an empty collection asserts nothing.
38+
assert orphans, "nothing to check: orphans was empty"
3739
for orphan in orphans:
3840
# orphan should be a FileId (string with extension)
3941
assert isinstance(orphan, str)

tests/test_arch_analyzer.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,14 @@ def test_no_analyzer_declares_an_option_it_ignores() -> None:
528528
"""
529529
import inspect
530530

531-
for name, analyzer in [("architecture", ArchitectureAnalyzer())]:
531+
from drydock.core.registry import available
532+
533+
analyzers = available()
534+
assert len(analyzers) >= 8, (
535+
f"expected the full analyzer registry, got {sorted(analyzers)}"
536+
)
537+
538+
for name, analyzer in sorted(analyzers.items()):
532539
body = inspect.getsource(type(analyzer).__dict__["run"])
533540
dead = [
534541
o.name for o in analyzer.options

tests/test_arch_principles.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,5 +459,7 @@ def test_principle_registry_loads_all_builtins() -> None:
459459
"tests-use-public-surface"
460460
}
461461

462+
# A loop over an empty collection asserts nothing.
463+
assert builtin_ids, "nothing to check: builtin_ids was empty"
462464
for principle_id in builtin_ids:
463465
assert principle_id in all_principles, f"Built-in principle {principle_id} not loaded"

tests/test_cli.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class TestUniformCliVocabulary:
3030
def test_every_analyzer_accepts_markdown_flag(self) -> None:
3131
"""All analyzers accept --markdown flag."""
3232
analyzers = available()
33+
# A loop over an empty collection asserts nothing.
34+
assert analyzers.items(), "nothing to check: analyzers.items() was empty"
3335
for name, analyzer in analyzers.items():
3436
parser = build_parser(analyzer)
3537
# Should not raise
@@ -39,6 +41,8 @@ def test_every_analyzer_accepts_markdown_flag(self) -> None:
3941
def test_every_analyzer_accepts_compact_flag(self) -> None:
4042
"""All analyzers accept --compact flag."""
4143
analyzers = available()
44+
# A loop over an empty collection asserts nothing.
45+
assert analyzers.items(), "nothing to check: analyzers.items() was empty"
4246
for name, analyzer in analyzers.items():
4347
parser = build_parser(analyzer)
4448
args = parser.parse_args(["/tmp", "--compact"])
@@ -47,6 +51,8 @@ def test_every_analyzer_accepts_compact_flag(self) -> None:
4751
def test_every_analyzer_accepts_quiet_flag(self) -> None:
4852
"""All analyzers accept --quiet flag."""
4953
analyzers = available()
54+
# A loop over an empty collection asserts nothing.
55+
assert analyzers.items(), "nothing to check: analyzers.items() was empty"
5056
for name, analyzer in analyzers.items():
5157
parser = build_parser(analyzer)
5258
args = parser.parse_args(["/tmp", "--quiet"])
@@ -55,6 +61,8 @@ def test_every_analyzer_accepts_quiet_flag(self) -> None:
5561
def test_every_analyzer_accepts_no_tests_flag(self) -> None:
5662
"""All analyzers accept --no-tests flag."""
5763
analyzers = available()
64+
# A loop over an empty collection asserts nothing.
65+
assert analyzers.items(), "nothing to check: analyzers.items() was empty"
5866
for name, analyzer in analyzers.items():
5967
parser = build_parser(analyzer)
6068
args = parser.parse_args(["/tmp", "--no-tests"])
@@ -63,6 +71,8 @@ def test_every_analyzer_accepts_no_tests_flag(self) -> None:
6371
def test_every_analyzer_accepts_max_files_flag(self) -> None:
6472
"""All analyzers accept --max-files N flag."""
6573
analyzers = available()
74+
# A loop over an empty collection asserts nothing.
75+
assert analyzers.items(), "nothing to check: analyzers.items() was empty"
6676
for name, analyzer in analyzers.items():
6777
parser = build_parser(analyzer)
6878
args = parser.parse_args(["/tmp", "--max-files", "100"])
@@ -71,6 +81,8 @@ def test_every_analyzer_accepts_max_files_flag(self) -> None:
7181
def test_every_analyzer_accepts_output_flag(self) -> None:
7282
"""All analyzers accept --output/-o FILE flag."""
7383
analyzers = available()
84+
# A loop over an empty collection asserts nothing.
85+
assert analyzers.items(), "nothing to check: analyzers.items() was empty"
7486
for name, analyzer in analyzers.items():
7587
parser = build_parser(analyzer)
7688
args = parser.parse_args(["/tmp", "--output", "out.json"])

tests/test_diagram_mermaid.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,8 @@ def test_layer_diagram_edges_reference_nodes():
451451
edges = _extract_edges(output)
452452

453453
# All edge targets and sources must be in nodes (or be special nodes)
454+
# A loop over an empty collection asserts nothing.
455+
assert edges, "nothing to check: edges was empty"
454456
for src, tgt in edges:
455457
if src.startswith("layer") and src.endswith("_more"):
456458
continue # Special node
@@ -480,6 +482,8 @@ def test_package_diagram_edges_reference_nodes():
480482
nodes = _extract_nodes(output)
481483
edges = _extract_edges(output)
482484

485+
# A loop over an empty collection asserts nothing.
486+
assert edges, "nothing to check: edges was empty"
483487
for src, tgt in edges:
484488
if src == "note" or tgt == "note":
485489
continue # Special node

tests/test_langs.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,8 @@ def test_all_import_refs_no_spaces(self, realistic_rust_project: Path) -> None:
343343
ctx = AnalysisContext(realistic_rust_project)
344344

345345
# Walk all source files and parse them
346+
# A loop over an empty collection asserts nothing.
347+
assert ctx.walk.files, "nothing to check: ctx.walk.files was empty"
346348
for sf in ctx.walk.files:
347349
if sf.lang is None:
348350
continue
@@ -382,6 +384,8 @@ def test_language_support_matrix_complete(self) -> None:
382384
registry = language_registry()
383385
matrix = registry.support_matrix()
384386

387+
# A loop over an empty collection asserts nothing.
388+
assert matrix, "nothing to check: matrix was empty"
385389
for row in matrix:
386390
assert "language" in row
387391
assert "fidelity" in row
@@ -401,6 +405,8 @@ def test_each_language_has_extensions(self) -> None:
401405
registry = language_registry()
402406
matrix = registry.support_matrix()
403407

408+
# A loop over an empty collection asserts nothing.
409+
assert matrix, "nothing to check: matrix was empty"
404410
for row in matrix:
405411
assert len(row["extensions"]) > 0
406412
# Extensions should start with .
@@ -416,6 +422,8 @@ def test_parser_has_name(self, tmp_path: Path) -> None:
416422
"""Each parser has a name attribute."""
417423
(tmp_path / "__init__.py").touch() # Make it a valid project
418424
ctx = AnalysisContext(tmp_path)
425+
# A loop over an empty collection asserts nothing.
426+
assert ctx.languages.all().items(), "nothing to check: ctx.languages.all().items() was empty"
419427
for name, parser in ctx.languages.all().items():
420428
assert hasattr(parser, "name")
421429
assert isinstance(parser.name, str)
@@ -424,6 +432,8 @@ def test_parser_has_extensions(self, tmp_path: Path) -> None:
424432
"""Each parser declares extensions."""
425433
(tmp_path / "__init__.py").touch()
426434
ctx = AnalysisContext(tmp_path)
435+
# A loop over an empty collection asserts nothing.
436+
assert ctx.languages.all().items(), "nothing to check: ctx.languages.all().items() was empty"
427437
for name, parser in ctx.languages.all().items():
428438
assert hasattr(parser, "extensions")
429439
assert isinstance(parser.extensions, (tuple, list))
@@ -432,6 +442,8 @@ def test_parser_has_fidelity(self, tmp_path: Path) -> None:
432442
"""Each parser declares fidelity (ast or regex)."""
433443
(tmp_path / "__init__.py").touch()
434444
ctx = AnalysisContext(tmp_path)
445+
# A loop over an empty collection asserts nothing.
446+
assert ctx.languages.all().items(), "nothing to check: ctx.languages.all().items() was empty"
435447
for name, parser in ctx.languages.all().items():
436448
assert hasattr(parser, "fidelity")
437449
assert parser.fidelity in ("ast", "regex")
@@ -440,20 +452,26 @@ def test_parser_implements_symbols(self, tmp_path: Path) -> None:
440452
"""Each parser has symbols() method."""
441453
(tmp_path / "__init__.py").touch()
442454
ctx = AnalysisContext(tmp_path)
455+
# A loop over an empty collection asserts nothing.
456+
assert ctx.languages.all().items(), "nothing to check: ctx.languages.all().items() was empty"
443457
for name, parser in ctx.languages.all().items():
444458
assert callable(getattr(parser, "symbols", None))
445459

446460
def test_parser_implements_imports(self, tmp_path: Path) -> None:
447461
"""Each parser has imports() method."""
448462
(tmp_path / "__init__.py").touch()
449463
ctx = AnalysisContext(tmp_path)
464+
# A loop over an empty collection asserts nothing.
465+
assert ctx.languages.all().items(), "nothing to check: ctx.languages.all().items() was empty"
450466
for name, parser in ctx.languages.all().items():
451467
assert callable(getattr(parser, "imports", None))
452468

453469
def test_parser_implements_interface(self, tmp_path: Path) -> None:
454470
"""Each parser has interface() method."""
455471
(tmp_path / "__init__.py").touch()
456472
ctx = AnalysisContext(tmp_path)
473+
# A loop over an empty collection asserts nothing.
474+
assert ctx.languages.all().items(), "nothing to check: ctx.languages.all().items() was empty"
457475
for name, parser in ctx.languages.all().items():
458476
assert callable(getattr(parser, "interface", None))
459477

tests/test_paths.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,8 @@ def test_rust_mod_declaration_no_prefix(
386386
imports = parser.imports(source, "src/main.rs")
387387

388388
# All imports should have raw field without "mod " prefix
389+
# A loop over an empty collection asserts nothing.
390+
assert imports, "nothing to check: imports was empty"
389391
for imp in imports:
390392
assert not imp.raw.startswith("mod "), \
391393
f"Import raw field should not start with 'mod ': {imp.raw}"

tests/test_plugin_api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ def test_option_with_choices(self) -> None:
210210
def test_all_built_in_analyzers_have_options(self) -> None:
211211
"""Every analyzer declares its options (even if empty)."""
212212
analyzers = available()
213+
# A loop over an empty collection asserts nothing.
214+
assert analyzers.items(), "nothing to check: analyzers.items() was empty"
213215
for name, analyzer in analyzers.items():
214216
assert hasattr(analyzer, "options")
215217
assert isinstance(analyzer.options, (list, tuple))
@@ -265,6 +267,8 @@ def test_each_language_has_at_least_one_extension(self) -> None:
265267
registry = language_registry()
266268
matrix = registry.support_matrix()
267269

270+
# A loop over an empty collection asserts nothing.
271+
assert matrix, "nothing to check: matrix was empty"
268272
for row in matrix:
269273
exts = row["extensions"]
270274
# Should be a non-empty string with space-separated extensions
@@ -309,6 +313,8 @@ def test_context_all_parsers_implement_protocol(
309313
(tmp_path / "__init__.py").touch()
310314
ctx = AnalysisContext(tmp_path)
311315

316+
# A loop over an empty collection asserts nothing.
317+
assert ctx.languages.all().items(), "nothing to check: ctx.languages.all().items() was empty"
312318
for lang_name, parser in ctx.languages.all().items():
313319
assert hasattr(parser, "symbols")
314320
assert hasattr(parser, "imports")

tests/test_security.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,8 @@ def test_security_analyzer_max_paths_option() -> None:
733733
findings = result.get("findings", [])
734734
path_findings = [f for f in findings if "path" in f.get("id", "")]
735735

736+
# A loop over an empty collection asserts nothing.
737+
assert path_findings, "nothing to check: path_findings was empty"
736738
for finding in path_findings:
737739
paths = finding.get("paths", [])
738740
assert len(paths) <= 2

0 commit comments

Comments
 (0)