|
| 1 | +"""Tests for argus.viewers.diagnose — missing-results-JSON remediation hints. |
| 2 | +
|
| 3 | +Three branches matter: |
| 4 | +- argus.yml exists and `reporting.formats` omits `json` → targeted hint |
| 5 | + identifying the config root cause. |
| 6 | +- argus.yml exists and lists `json` → fall back to the generic hint |
| 7 | + (config isn't the cause). |
| 8 | +- no argus.yml found anywhere up the tree → generic hint. |
| 9 | +
|
| 10 | +Plus defensive paths: malformed YAML, missing reporting key, etc., all |
| 11 | +fall back to the generic hint without raising. |
| 12 | +""" |
| 13 | + |
| 14 | +from __future__ import annotations |
| 15 | + |
| 16 | +from pathlib import Path |
| 17 | + |
| 18 | +from argus.viewers.diagnose import ( |
| 19 | + _find_nearby_argus_config, |
| 20 | + _read_reporting_formats, |
| 21 | + diagnose_missing_results, |
| 22 | +) |
| 23 | + |
| 24 | + |
| 25 | +# ─────────────────────────────────────────────── |
| 26 | +# diagnose_missing_results — public surface |
| 27 | +# ─────────────────────────────────────────────── |
| 28 | + |
| 29 | + |
| 30 | +class TestDiagnoseMissingResults: |
| 31 | + def test_targeted_hint_when_config_omits_json(self, tmp_path, monkeypatch): |
| 32 | + """argus.yml present, json NOT in reporting.formats → message names |
| 33 | + the file, the actual formats value, and offers a one-shot CLI fix |
| 34 | + plus the config-edit fix.""" |
| 35 | + config = tmp_path / "argus.yml" |
| 36 | + config.write_text( |
| 37 | + "reporting:\n formats:\n - terminal\n - sarif\n" |
| 38 | + ) |
| 39 | + monkeypatch.chdir(tmp_path) |
| 40 | + |
| 41 | + msg = diagnose_missing_results(tmp_path / "argus-results.json") |
| 42 | + |
| 43 | + # Required: identifies the file. |
| 44 | + assert "argus-results.json" in msg |
| 45 | + # Required: message names config root cause. |
| 46 | + assert "argus.yml" in msg |
| 47 | + assert "reporting.formats" in msg |
| 48 | + assert "'terminal'" in msg and "'sarif'" in msg |
| 49 | + # Required: at least one config fix and one CLI fix. |
| 50 | + assert "Add 'json' to reporting.formats" in msg |
| 51 | + assert "argus scan --format json" in msg |
| 52 | + # Required: retry instruction echoes the searched path. |
| 53 | + assert "argus view" in msg |
| 54 | + |
| 55 | + def test_generic_hint_when_no_argus_yml(self, tmp_path, monkeypatch): |
| 56 | + """No argus.yml anywhere up the tree → generic hint, no false claim |
| 57 | + about config being the cause.""" |
| 58 | + # Use an isolated tmp dir as cwd; walk-up shouldn't find any |
| 59 | + # config because tmp dirs are well outside the repo root. |
| 60 | + sub = tmp_path / "isolated" |
| 61 | + sub.mkdir() |
| 62 | + monkeypatch.chdir(sub) |
| 63 | + |
| 64 | + msg = diagnose_missing_results(sub / "argus-results.json") |
| 65 | + |
| 66 | + # Still identifies the file + still gives both fix paths. |
| 67 | + assert "argus-results.json" in msg |
| 68 | + assert "Add 'json' to reporting.formats in argus.yml" in msg |
| 69 | + assert "argus scan --format json" in msg |
| 70 | + # Doesn't make a confident claim about a config we never found. |
| 71 | + assert "Detected" not in msg |
| 72 | + |
| 73 | + def test_generic_hint_when_config_includes_json(self, tmp_path, monkeypatch): |
| 74 | + """Config has json — bug is somewhere else (wrong path, scan never |
| 75 | + ran, output dir mismatch). Don't blame the config.""" |
| 76 | + config = tmp_path / "argus.yml" |
| 77 | + config.write_text( |
| 78 | + "reporting:\n formats:\n - terminal\n - json\n" |
| 79 | + ) |
| 80 | + monkeypatch.chdir(tmp_path) |
| 81 | + |
| 82 | + msg = diagnose_missing_results(tmp_path / "argus-results.json") |
| 83 | + |
| 84 | + # Generic hint path — not the targeted one. |
| 85 | + assert "Detected" not in msg |
| 86 | + # Both fixes still listed (user might still need to run the scan). |
| 87 | + assert "argus scan --format json" in msg |
| 88 | + |
| 89 | + def test_unparseable_argus_yml_falls_back_to_generic(self, tmp_path, monkeypatch): |
| 90 | + """Broken YAML must NOT mask the original missing-file diagnostic |
| 91 | + with a parse-error traceback. Falls back to the generic hint.""" |
| 92 | + config = tmp_path / "argus.yml" |
| 93 | + config.write_text("reporting:\n formats: [terminal,, : :") |
| 94 | + monkeypatch.chdir(tmp_path) |
| 95 | + |
| 96 | + msg = diagnose_missing_results(tmp_path / "argus-results.json") |
| 97 | + |
| 98 | + # No traceback / exception leakage. |
| 99 | + assert "Traceback" not in msg |
| 100 | + assert "yaml" not in msg.lower() |
| 101 | + # Falls back to generic path. |
| 102 | + assert "Detected" not in msg |
| 103 | + assert "argus-results.json" in msg |
| 104 | + |
| 105 | + def test_argus_yml_without_reporting_key_falls_back(self, tmp_path, monkeypatch): |
| 106 | + """Config exists but doesn't define reporting.formats → can't make |
| 107 | + a confident 'json missing from formats' claim, generic hint fires.""" |
| 108 | + config = tmp_path / "argus.yml" |
| 109 | + config.write_text("scanners:\n bandit:\n enabled: true\n") |
| 110 | + monkeypatch.chdir(tmp_path) |
| 111 | + |
| 112 | + msg = diagnose_missing_results(tmp_path / "argus-results.json") |
| 113 | + assert "Detected" not in msg |
| 114 | + |
| 115 | + def test_retry_arg_uses_parent_for_results_filename(self, tmp_path, monkeypatch): |
| 116 | + """When the searched path ends in argus-results.json, the retry |
| 117 | + hint should point at its parent dir (what argus view actually |
| 118 | + accepts), not the JSON file itself.""" |
| 119 | + config = tmp_path / "argus.yml" |
| 120 | + config.write_text("reporting:\n formats: [terminal]\n") |
| 121 | + monkeypatch.chdir(tmp_path) |
| 122 | + |
| 123 | + run_dir = tmp_path / "argus-results" / "2026-05-05T10-00-00Z" |
| 124 | + msg = diagnose_missing_results(run_dir / "argus-results.json") |
| 125 | + |
| 126 | + assert f"argus view {run_dir}" in msg |
| 127 | + |
| 128 | + def test_retry_arg_falls_back_to_placeholder_for_empty_path(self, tmp_path, monkeypatch): |
| 129 | + """A relative '.' or empty path renders as a literal placeholder |
| 130 | + rather than a useless 'argus view .' line.""" |
| 131 | + monkeypatch.chdir(tmp_path) |
| 132 | + msg = diagnose_missing_results(Path(".")) |
| 133 | + assert "argus view <results-dir>" in msg |
| 134 | + |
| 135 | + |
| 136 | +# ─────────────────────────────────────────────── |
| 137 | +# Helpers exercised separately for confidence at boundaries |
| 138 | +# ─────────────────────────────────────────────── |
| 139 | + |
| 140 | + |
| 141 | +class TestFindNearbyArgusConfig: |
| 142 | + def test_finds_in_starting_dir(self, tmp_path): |
| 143 | + config = tmp_path / "argus.yml" |
| 144 | + config.write_text("# empty\n") |
| 145 | + assert _find_nearby_argus_config(tmp_path) == config |
| 146 | + |
| 147 | + def test_walks_up_to_find_config(self, tmp_path): |
| 148 | + nested = tmp_path / "a" / "b" / "c" |
| 149 | + nested.mkdir(parents=True) |
| 150 | + config = tmp_path / "argus.yml" |
| 151 | + config.write_text("# empty\n") |
| 152 | + assert _find_nearby_argus_config(nested) == config |
| 153 | + |
| 154 | + def test_returns_none_when_no_config_in_tree(self, tmp_path): |
| 155 | + # tmp_path's tree is well-isolated from any repo with argus.yml. |
| 156 | + assert _find_nearby_argus_config(tmp_path) is None |
| 157 | + |
| 158 | + def test_recognises_alternate_config_names(self, tmp_path): |
| 159 | + # argus.yaml and the dotted variants are all valid filenames. |
| 160 | + for name in ("argus.yaml", ".argus.yml", ".argus.yaml"): |
| 161 | + sub = tmp_path / name.replace(".", "_") |
| 162 | + sub.mkdir() |
| 163 | + config = sub / name |
| 164 | + config.write_text("# empty\n") |
| 165 | + assert _find_nearby_argus_config(sub) == config |
| 166 | + |
| 167 | + |
| 168 | +class TestReadReportingFormats: |
| 169 | + def test_reads_formats_list(self, tmp_path): |
| 170 | + config = tmp_path / "argus.yml" |
| 171 | + config.write_text( |
| 172 | + "reporting:\n formats:\n - terminal\n - json\n" |
| 173 | + ) |
| 174 | + assert _read_reporting_formats(config) == ["terminal", "json"] |
| 175 | + |
| 176 | + def test_returns_none_for_missing_reporting_key(self, tmp_path): |
| 177 | + config = tmp_path / "argus.yml" |
| 178 | + config.write_text("scanners:\n bandit:\n enabled: true\n") |
| 179 | + assert _read_reporting_formats(config) is None |
| 180 | + |
| 181 | + def test_returns_none_for_unparseable_yaml(self, tmp_path): |
| 182 | + config = tmp_path / "argus.yml" |
| 183 | + config.write_text("reporting: [{") |
| 184 | + assert _read_reporting_formats(config) is None |
| 185 | + |
| 186 | + def test_returns_none_when_formats_isnt_a_list(self, tmp_path): |
| 187 | + config = tmp_path / "argus.yml" |
| 188 | + config.write_text("reporting:\n formats: terminal\n") |
| 189 | + assert _read_reporting_formats(config) is None |
0 commit comments