main
py 215 lines 7.68 KB
Raw
1 """Tests for scripts/quality_gate.py"""
2
3 from __future__ import annotations
4
5 import json
6
7 import scripts.quality_gate as quality_gate
8
9
10 def make_scored_repos(count: int, score: float = 65.0) -> list[dict]:
11 """Generate a list of scored repo dicts."""
12 return [{"name": f"org/repo-{i}", "relevance_score": score} for i in range(count)]
13
14
15 class TestGetQualityConfig:
16 def test_defaults_when_empty(self):
17 config = quality_gate.get_quality_config({})
18 assert config["min_repos_per_week"] == 5
19 assert config["max_repos_per_week"] == 30
20 assert config["min_quality_score"] == 60
21
22 def test_overrides(self):
23 config = quality_gate.get_quality_config({"quality": {"min_repos_per_week": 10}})
24 assert config["min_repos_per_week"] == 10
25 assert config["max_repos_per_week"] == 30
26
27
28 class TestCheckQuality:
29 def test_ok_status(self):
30 repos = make_scored_repos(10, score=50.0)
31 result = quality_gate.check_quality(
32 repos,
33 {"min_repos_per_week": 5, "max_repos_per_week": 30},
34 {"min_relevance_score": 40},
35 )
36 assert result["status"] == "ok"
37 assert result["repos_passing"] == 10
38 assert result["repos_scored"] == 10
39 assert result["warnings"] == []
40
41 def test_below_threshold(self):
42 repos = make_scored_repos(3, score=50.0)
43 result = quality_gate.check_quality(
44 repos,
45 {"min_repos_per_week": 5, "max_repos_per_week": 30},
46 {"min_relevance_score": 40},
47 )
48 assert result["status"] == "below_threshold"
49 assert result["repos_passing"] == 3
50 assert len(result["warnings"]) == 1
51 assert "threshold is 5" in result["warnings"][0]
52
53 def test_above_maximum(self):
54 repos = make_scored_repos(35, score=50.0)
55 result = quality_gate.check_quality(
56 repos,
57 {"min_repos_per_week": 5, "max_repos_per_week": 30},
58 {"min_relevance_score": 40},
59 )
60 assert result["status"] == "above_maximum"
61 assert result["repos_passing"] == 35
62 assert len(result["warnings"]) == 1
63 assert "noise" in result["warnings"][0].lower()
64
65 def test_repos_below_score_not_counted(self):
66 repos = make_scored_repos(10, score=30.0)
67 result = quality_gate.check_quality(
68 repos,
69 {"min_repos_per_week": 5, "max_repos_per_week": 30},
70 {"min_relevance_score": 40},
71 )
72 assert result["repos_passing"] == 0
73 assert result["status"] == "below_threshold"
74
75 def test_empty_repos(self):
76 result = quality_gate.check_quality(
77 [],
78 {"min_repos_per_week": 5, "max_repos_per_week": 30},
79 {"min_relevance_score": 40},
80 )
81 assert result["status"] == "below_threshold"
82 assert result["repos_passing"] == 0
83 assert result["repos_scored"] == 0
84
85
86 class TestEmitWarnings:
87 def test_prints_annotations(self, capsys):
88 quality_gate.emit_warnings(["Something is wrong", "Another issue"])
89 captured = capsys.readouterr()
90 assert "::warning::Something is wrong" in captured.out
91 assert "::warning::Another issue" in captured.out
92
93 def test_no_output_when_empty(self, capsys):
94 quality_gate.emit_warnings([])
95 captured = capsys.readouterr()
96 assert captured.out == ""
97
98
99 class TestWriteMetric:
100 def test_writes_json(self, tmp_path, monkeypatch):
101 monkeypatch.setattr(quality_gate, "metrics_dir", lambda t: tmp_path / "metrics" / t)
102 metric = {
103 "repos_scored": 10,
104 "repos_passing": 8,
105 "threshold": 5,
106 "status": "ok",
107 "warnings": [],
108 }
109 path = quality_gate.write_metric("ai-ml", metric, "2026-W21")
110 assert path.exists()
111 data = json.loads(path.read_text())
112 assert data["week"] == "2026-W21"
113 assert data["topic"] == "ai-ml"
114 assert data["repos_passing"] == 8
115 assert data["status"] == "ok"
116 assert "warnings" not in data
117
118
119 class TestLoadScoredRepos:
120 def test_loads_valid_json(self, tmp_path):
121 path = tmp_path / "scored.json"
122 repos = make_scored_repos(5)
123 path.write_text(json.dumps(repos))
124 result = quality_gate.load_scored_repos(path)
125 assert len(result) == 5
126
127 def test_missing_file(self, tmp_path):
128 result = quality_gate.load_scored_repos(tmp_path / "missing.json")
129 assert result == []
130
131 def test_invalid_json(self, tmp_path):
132 path = tmp_path / "bad.json"
133 path.write_text("not json")
134 result = quality_gate.load_scored_repos(path)
135 assert result == []
136
137 def test_non_list_json(self, tmp_path):
138 path = tmp_path / "obj.json"
139 path.write_text(json.dumps({"repos": []}))
140 result = quality_gate.load_scored_repos(path)
141 assert result == []
142
143
144 class TestMain:
145 def test_with_input_file(self, tmp_path, monkeypatch):
146 monkeypatch.setattr(quality_gate, "metrics_dir", lambda t: tmp_path / "metrics" / t)
147 scored_path = tmp_path / "scored.json"
148 scored_path.write_text(json.dumps(make_scored_repos(10, score=50.0)))
149
150 config_path = tmp_path / "config.yml"
151 config_path.write_text(
152 "topic:\n id: ai-ml\nscoring:\n min_relevance_score: 40\n"
153 "quality:\n min_repos_per_week: 5\n max_repos_per_week: 30\n"
154 )
155
156 result = quality_gate.main(["--input", str(scored_path), "--config", str(config_path)])
157 assert result == 0
158
159 def test_missing_input_no_crash(self, tmp_path, monkeypatch):
160 monkeypatch.setattr(quality_gate, "metrics_dir", lambda t: tmp_path / "metrics" / t)
161 config_path = tmp_path / "config.yml"
162 config_path.write_text("topic:\n id: test\n")
163
164 result = quality_gate.main(
165 ["--input", str(tmp_path / "nope.json"), "--config", str(config_path)]
166 )
167 assert result == 0
168
169 def test_always_exits_zero(self, tmp_path, monkeypatch):
170 monkeypatch.setattr(quality_gate, "metrics_dir", lambda t: tmp_path / "metrics" / t)
171 scored_path = tmp_path / "scored.json"
172 scored_path.write_text(json.dumps(make_scored_repos(2, score=50.0)))
173
174 config_path = tmp_path / "config.yml"
175 config_path.write_text(
176 "topic:\n id: ai-ml\nscoring:\n min_relevance_score: 40\n"
177 "quality:\n min_repos_per_week: 10\n max_repos_per_week: 30\n"
178 )
179
180 result = quality_gate.main(["--input", str(scored_path), "--config", str(config_path)])
181 assert result == 0
182
183 def test_topic_override(self, tmp_path, monkeypatch):
184 monkeypatch.setattr(quality_gate, "metrics_dir", lambda t: tmp_path / "metrics" / t)
185 scored_path = tmp_path / "scored.json"
186 scored_path.write_text(json.dumps(make_scored_repos(10, score=50.0)))
187
188 config_path = tmp_path / "config.yml"
189 config_path.write_text("topic:\n id: ai-ml\nscoring:\n min_relevance_score: 40\n")
190
191 result = quality_gate.main(
192 [
193 "--input",
194 str(scored_path),
195 "--config",
196 str(config_path),
197 "--topic",
198 "custom-topic",
199 ]
200 )
201 assert result == 0
202 # Verify metric written with correct topic
203 files = list((tmp_path / "metrics" / "custom-topic").glob("quality-*.json"))
204 assert len(files) == 1
205 data = json.loads(files[0].read_text())
206 assert data["topic"] == "custom-topic"
207
208
209 class TestWeekSlug:
210 def test_format(self):
211 from datetime import datetime, timezone
212
213 dt = datetime(2026, 5, 18, tzinfo=timezone.utc)
214 result = quality_gate.week_slug(dt)
215 assert result == "2026-W21"