Option C Tools
Loading...
Searching...
No Matches
test_lint_baseline.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_linter/test_lint_baseline.py
4
5"""
6Purpose
7-------
8FS-508 regression coverage for the lint baseline (snapshot + diff).
9
10Responsibilities
11----------------
12- Validate save_baseline creates a file and returns a count.
13- Validate load_baseline reads back saved data.
14- Validate diff_against_baseline detects new and resolved violations.
15- Validate --baseline and --against-baseline flag behaviour via run_linter.
16- Validate mutually exclusive flag error.
17
18Diagnostics
19-----------
20Domain: LINTER-TESTS
21 Levels:
22 L2 — test lifecycle
23 L3 — assertion details
24 L4 — deep tracing
25
26Contracts
27---------
28- All baseline files are written to ``tmp_path``; no real project
29 baselines are touched.
30"""
31
32from __future__ import annotations
33
34import json
35import textwrap
36from pathlib import Path
37
38import pytest
39
40from oct.linter.lint_baseline import (
41 BaselineEntry,
42 LintBaseline,
43 diff_against_baseline,
44 load_baseline,
45 save_baseline,
46)
47
48
49# -------------------------------------------------------------------
50# Helpers
51# -------------------------------------------------------------------
52
53def _make_results(violations: list[tuple[str, str, str]]) -> list[dict]:
54 """Build mock lint results with specified violations."""
55 by_path: dict[str, list[tuple[str, str]]] = {}
56 for path, rule, msg in violations:
57 by_path.setdefault(path, []).append((rule, msg))
58
59 results = []
60 for path, items in by_path.items():
61 checks = {}
62 all_rules = [
63 "header", "docstring", "diagnostics_profile", "dbg_usage",
64 "dbg_import", "func_pattern", "syntax_warnings", "type_hints",
65 "no_hardcoded_secrets", "dbg_assert_safety", "tests_exist",
66 ]
67 for rule in all_rules:
68 failing = [(r, m) for r, m in items if r == rule]
69 if failing:
70 checks[rule] = {"pass": False, "message": failing[0][1]}
71 else:
72 checks[rule] = {"pass": True, "message": "OK"}
73 results.append({
74 "header": checks["header"]["pass"],
75 "docstring": checks["docstring"]["pass"],
76 "diagnostics_profile": checks["diagnostics_profile"]["pass"],
77 "dbg_usage": checks["dbg_usage"]["pass"],
78 "dbg_import": checks["dbg_import"]["pass"],
79 "func_pattern": checks["func_pattern"]["pass"],
80 "syntax_warnings": checks["syntax_warnings"]["pass"],
81 "type_hints": checks["type_hints"]["pass"],
82 "no_hardcoded_secrets": checks["no_hardcoded_secrets"]["pass"],
83 "dbg_assert_safety": checks["dbg_assert_safety"]["pass"],
84 "tests_exist": checks["tests_exist"]["pass"],
85 "compliant": all(c["pass"] for c in checks.values()),
86 "fixed": False,
87 "rel": path,
88 "waivers": [],
89 "json_detail": {
90 "path": path,
91 "checks": checks,
92 "compliant": all(c["pass"] for c in checks.values()),
93 "waivers": [],
94 },
95 })
96 return results
97
98
99# -------------------------------------------------------------------
100# save_baseline
101# -------------------------------------------------------------------
102
104
105 def test_creates_file(self, tmp_path: Path):
106 results = _make_results([("foo.py", "header", "bad shebang")])
107 bp = tmp_path / "baseline.json"
108 count = save_baseline(results, bp, "strict")
109 assert bp.is_file()
110 assert count == 1
111
112 def test_returns_violation_count(self, tmp_path: Path):
113 results = _make_results([
114 ("a.py", "header", "bad"),
115 ("a.py", "docstring", "missing"),
116 ("b.py", "header", "wrong"),
117 ])
118 bp = tmp_path / "baseline.json"
119 count = save_baseline(results, bp, "strict")
120 assert count == 3
121
122 def test_creates_parent_dirs(self, tmp_path: Path):
123 bp = tmp_path / "deep" / "nested" / "baseline.json"
124 save_baseline([], bp, "strict")
125 assert bp.is_file()
126
127 def test_zero_violations(self, tmp_path: Path):
128 results = _make_results([])
129 bp = tmp_path / "baseline.json"
130 count = save_baseline(results, bp, "strict")
131 assert count == 0
132
133
134# -------------------------------------------------------------------
135# load_baseline
136# -------------------------------------------------------------------
137
139
140 def test_load_saved(self, tmp_path: Path):
141 results = _make_results([("foo.py", "header", "bad shebang")])
142 bp = tmp_path / "baseline.json"
143 save_baseline(results, bp, "strict")
144 bl = load_baseline(bp)
145 assert bl is not None
146 assert len(bl.entries) == 1
147 assert bl.entries[0].path == "foo.py"
148 assert bl.entries[0].rule == "header"
149 assert bl.profile == "strict"
150
151 def test_nonexistent_returns_none(self, tmp_path: Path):
152 assert load_baseline(tmp_path / "nope.json") is None
153
154 def test_corrupt_returns_none(self, tmp_path: Path):
155 bp = tmp_path / "baseline.json"
156 bp.write_text("not json!", encoding="utf-8")
157 assert load_baseline(bp) is None
158
159 def test_wrong_schema_returns_none(self, tmp_path: Path):
160 bp = tmp_path / "baseline.json"
161 bp.write_text(json.dumps({"_schema_version": "99.0"}), encoding="utf-8")
162 assert load_baseline(bp) is None
163
164
165# -------------------------------------------------------------------
166# diff_against_baseline
167# -------------------------------------------------------------------
168
170
172 baseline = LintBaseline(
173 entries=[BaselineEntry("a.py", "header", "old issue")],
174 oct_version="0.22.0", timestamp="", profile="strict",
175 )
176 current = _make_results([
177 ("a.py", "header", "old issue"),
178 ("b.py", "docstring", "new problem"),
179 ])
180 new, resolved = diff_against_baseline(current, baseline)
181 assert len(new) == 1
182 assert new[0][0] == "b.py"
183 assert new[0][1] == "docstring"
184
186 baseline = LintBaseline(
187 entries=[
188 BaselineEntry("a.py", "header", "bad"),
189 BaselineEntry("a.py", "docstring", "missing"),
190 ],
191 oct_version="0.22.0", timestamp="", profile="strict",
192 )
193 current = _make_results([("a.py", "header", "bad")])
194 new, resolved = diff_against_baseline(current, baseline)
195 assert len(new) == 0
196 assert len(resolved) == 1
197 assert resolved[0][1] == "docstring"
198
200 baseline = LintBaseline(
201 entries=[BaselineEntry("a.py", "header", "bad")],
202 oct_version="0.22.0", timestamp="", profile="strict",
203 )
204 current = _make_results([("a.py", "header", "bad")])
205 new, resolved = diff_against_baseline(current, baseline)
206 assert len(new) == 0
207 assert len(resolved) == 0
208
210 baseline = LintBaseline(
211 entries=[], oct_version="0.22.0", timestamp="", profile="strict",
212 )
213 current = _make_results([("a.py", "header", "bad")])
214 new, resolved = diff_against_baseline(current, baseline)
215 assert len(new) == 1
216 assert len(resolved) == 0
217
218
219# -------------------------------------------------------------------
220# Integration with run_linter flags
221# -------------------------------------------------------------------
222
224
225 def test_mutually_exclusive(self, tmp_path: Path, capsys):
226 from oct.linter.oct_lint import run_linter
227 result = run_linter(tmp_path, ["--baseline", "--against-baseline"])
228 assert result == 1
229 captured = capsys.readouterr()
230 assert "mutually exclusive" in captured.err
231
232 def test_against_baseline_without_file(self, tmp_path: Path, capsys):
233 from oct.linter.oct_lint import run_linter
234 result = run_linter(tmp_path, ["--against-baseline"])
235 assert result == 1
236 captured = capsys.readouterr()
237 assert "no baseline found" in captured.err
test_against_baseline_without_file(self, Path tmp_path, capsys)
list[dict] _make_results(list[tuple[str, str, str]] violations)