forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_desktop_beta_qualification_retry.py
More file actions
executable file
·328 lines (268 loc) · 12.9 KB
/
Copy pathtest_desktop_beta_qualification_retry.py
File metadata and controls
executable file
·328 lines (268 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#!/usr/bin/env python3
"""Fault tests for the bounded desktop Beta qualification retry decision.
These exercise every safety property of the retry decision against fabricated
server JSON: invalid candidate, age bound, no prior attempt, in-progress and
successful runs, transient retry under the attempt bound, the deterministic
loop guard at the bound, exact-SHA/tag binding, and non-retryable conclusions.
"""
# omi-test-quality: source-inspection -- the decision script is pure JSON logic.
from __future__ import annotations
import copy
import json
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path
import desktop_beta_qualification_retry as retry
TAG = "v1.2.3+1234-macos"
SHA = "a" * 40
def _utc_iso(dt: datetime) -> str:
return dt.replace(microsecond=0).astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
_NOW = datetime.now(timezone.utc)
NOW_ISO = _utc_iso(_NOW)
RECENT_ISO = _utc_iso(_NOW - timedelta(hours=1))
def _release(*, tag: str = TAG, channel: str = "candidate", is_live: str = "false",
draft: bool = False, prerelease: bool = False, published: str = RECENT_ISO,
assets=retry.CANONICAL_ASSETS) -> dict:
body = (
"<!-- KEY_VALUE_START\n"
f"channel: {channel}\nisLive: {is_live}\n"
"<!-- KEY_VALUE_END -->"
)
return {
"tagName": tag,
"body": body,
"isDraft": draft,
"isPrerelease": prerelease,
"publishedAt": published,
"assets": [{"name": name} for name in assets],
}
def _run(*, status="completed", conclusion="failure", branch=TAG, sha=SHA,
updated=NOW_ISO, run_id=1) -> dict:
return {
"id": run_id,
"status": status,
"conclusion": conclusion,
"event": "workflow_dispatch",
"path": retry.QUALIFICATION_WORKFLOW,
"head_branch": branch,
"head_sha": sha,
"name": "Qualify Desktop Beta Candidate",
"updated_at": updated,
"created_at": updated,
}
def _decide(release, runs, **overrides):
kwargs = dict(release_tag=TAG, tag_sha=SHA, max_attempts=3, max_age_hours=24)
kwargs.update(overrides)
return retry.decide(release, runs, **kwargs)
class RetryDecisionTests(unittest.TestCase):
# --- candidate validity (no retry) ---
def test_missing_release_denies(self):
d = _decide({}, [])
self.assertFalse(d["should_retry"])
self.assertIn("no published GitHub release", d["reason"])
def test_release_tag_mismatch_denies(self):
release = _release(tag="v9.9.9+9999-macos")
d = _decide(release, [])
self.assertFalse(d["should_retry"])
self.assertIn("does not match", d["reason"])
def test_draft_or_prerelease_denies(self):
for kw in ({"draft": True}, {"prerelease": True}):
with self.subTest(**kw):
d = _decide(_release(**kw), [_run(conclusion="failure")])
self.assertFalse(d["should_retry"])
def test_missing_canonical_asset_denies(self):
release = _release(assets=("Omi.zip", "omi.dmg")) # no smoke result
d = _decide(release, [_run(conclusion="failure")])
self.assertFalse(d["should_retry"])
self.assertIn("missing canonical assets", d["reason"])
def test_promoted_candidate_denies(self):
# isLive:true means the candidate already advanced; never re-qualify.
d = _decide(_release(is_live="true"), [_run(conclusion="failure")])
self.assertFalse(d["should_retry"])
self.assertIn("isLive", d["reason"])
def test_wrong_channel_denies(self):
d = _decide(_release(channel="stable"), [_run(conclusion="failure")])
self.assertFalse(d["should_retry"])
self.assertIn("channel", d["reason"])
# --- age bound ---
def test_candidate_too_old_denies(self):
old = _utc_iso(_NOW - timedelta(hours=48))
d = _decide(_release(published=old), [_run(conclusion="failure")], max_age_hours=24)
self.assertFalse(d["should_retry"])
self.assertIn("exceeds", d["reason"])
def test_candidate_24_hours_30_minutes_old_denies(self):
old = _utc_iso(_NOW - timedelta(hours=24, minutes=30))
d = _decide(_release(published=old), [_run(conclusion="failure")], max_age_hours=24)
self.assertFalse(d["should_retry"])
self.assertIn("exceeds", d["reason"])
def test_malformed_publication_timestamp_denies(self):
d = _decide(_release(published="not-a-timestamp"), [_run(conclusion="failure")])
self.assertFalse(d["should_retry"])
self.assertIn("malformed", d["reason"])
def test_future_publication_timestamp_denies(self):
future = _utc_iso(_NOW + timedelta(minutes=30))
d = _decide(_release(published=future), [_run(conclusion="failure")])
self.assertFalse(d["should_retry"])
self.assertIn("future", d["reason"])
# --- run state (never retry in-progress / successful / unattempted) ---
def test_no_prior_attempt_denies(self):
# The retry never owns the initial dispatch; Codemagic does.
d = _decide(_release(), [])
self.assertFalse(d["should_retry"])
self.assertIn("unattempted", d["reason"])
def test_in_progress_denies(self):
for status in ("in_progress", "queued", "waiting"):
with self.subTest(status=status):
d = _decide(_release(), [_run(status=status)])
self.assertFalse(d["should_retry"])
self.assertIn("in-progress", d["reason"])
def test_successful_run_denies(self):
d = _decide(_release(), [_run(conclusion="success")])
self.assertFalse(d["should_retry"])
self.assertIn("already succeeded", d["reason"])
def test_non_retryable_conclusion_denies(self):
for conclusion in ("neutral", "skipped", "action_required"):
with self.subTest(conclusion=conclusion):
d = _decide(_release(), [_run(conclusion=conclusion)])
self.assertFalse(d["should_retry"])
# --- transient retry under the bound ---
def test_failed_run_under_bound_retries(self):
d = _decide(_release(), [_run(conclusion="failure")])
self.assertTrue(d["should_retry"])
self.assertEqual(d["attempts_so_far"], 1)
self.assertIn("attempt 2 of 3", d["reason"])
def test_cancelled_run_retries(self):
d = _decide(_release(), [_run(conclusion="cancelled")])
self.assertTrue(d["should_retry"])
def test_second_failure_still_retries(self):
runs = [
_run(conclusion="failure", run_id=1),
_run(conclusion="failure", run_id=2, updated=_utc_iso(_NOW + timedelta(minutes=5))),
]
d = _decide(_release(), runs)
self.assertTrue(d["should_retry"])
self.assertEqual(d["attempts_so_far"], 2)
self.assertIn("attempt 3 of 3", d["reason"])
# --- deterministic / persistent loop guard ---
def test_at_max_attempts_denies(self):
runs = [
_run(conclusion="failure", run_id=i, updated=_utc_iso(_NOW + timedelta(minutes=i)))
for i in (1, 2, 3)
]
d = _decide(_release(), runs)
self.assertFalse(d["should_retry"])
self.assertEqual(d["attempts_so_far"], 3)
self.assertIn("bound", d["reason"])
def test_custom_lower_bound_caps_earlier(self):
runs = [_run(conclusion="failure", run_id=1), _run(conclusion="failure", run_id=2)]
d = _decide(_release(), runs, max_attempts=2)
self.assertFalse(d["should_retry"])
self.assertIn("bound", d["reason"])
def test_neutral_plus_two_failures_reaches_total_attempt_bound(self):
runs = [
_run(conclusion="neutral", run_id=1, updated=_utc_iso(_NOW - timedelta(minutes=2))),
_run(conclusion="failure", run_id=2, updated=_utc_iso(_NOW - timedelta(minutes=1))),
_run(conclusion="failure", run_id=3, updated=NOW_ISO),
]
d = _decide(_release(), runs)
self.assertFalse(d["should_retry"])
self.assertEqual(d["attempts_so_far"], 3)
self.assertIn("bound", d["reason"])
def test_duplicate_run_records_count_as_one_attempt(self):
run = _run(conclusion="failure", run_id=7)
d = _decide(_release(), [run, copy.deepcopy(run)])
self.assertTrue(d["should_retry"])
self.assertEqual(d["attempts_so_far"], 1)
# --- exact tag + SHA binding (ignore other candidates' runs) ---
def test_runs_for_other_tags_are_ignored(self):
# A failed run for a different tag must not count toward this tag.
runs = [
_run(branch="v9.9.9+9999-macos", conclusion="failure", run_id=1),
_run(branch="v8.8.8+8888-macos", sha="b" * 40, conclusion="failure", run_id=2),
]
d = _decide(_release(), runs)
self.assertFalse(d["should_retry"])
self.assertIn("unattempted", d["reason"])
def test_newest_run_governs_in_progress_check(self):
# Older failure present, but newest run is in_progress -> do not retry.
runs = [
_run(conclusion="failure", run_id=1, updated=_utc_iso(_NOW - timedelta(hours=1))),
_run(status="in_progress", conclusion=None, run_id=2, updated=NOW_ISO),
]
d = _decide(_release(), runs)
self.assertFalse(d["should_retry"])
self.assertIn("in-progress", d["reason"])
def test_success_after_failure_denies(self):
# A later successful run supersedes an earlier failure.
runs = [
_run(conclusion="failure", run_id=1, updated=_utc_iso(_NOW - timedelta(hours=1))),
_run(conclusion="success", run_id=2, updated=NOW_ISO),
]
d = _decide(_release(), runs)
self.assertFalse(d["should_retry"])
self.assertIn("already succeeded", d["reason"])
def test_older_success_with_newer_failure_denies(self):
runs = [
_run(conclusion="success", run_id=1, updated=_utc_iso(_NOW - timedelta(hours=1))),
_run(conclusion="failure", run_id=2, updated=NOW_ISO),
]
d = _decide(_release(), runs)
self.assertFalse(d["should_retry"])
self.assertIn("already succeeded", d["reason"])
def test_older_in_progress_with_newer_cancelled_denies(self):
runs = [
_run(status="in_progress", conclusion=None, run_id=1,
updated=_utc_iso(_NOW - timedelta(hours=1))),
_run(conclusion="cancelled", run_id=2, updated=NOW_ISO),
]
d = _decide(_release(), runs)
self.assertFalse(d["should_retry"])
self.assertIn("in-progress", d["reason"])
def test_non_workflow_dispatch_runs_ignored(self):
# Only workflow_dispatch qualification runs are authoritative.
run = _run()
run["event"] = "workflow_run"
d = _decide(_release(), [run])
self.assertFalse(d["should_retry"])
# --- never reaches promotion / stable ---
def test_decision_never_carries_promotion_authority(self):
d = _decide(_release(), [_run(conclusion="failure")])
self.assertTrue(d["should_retry"])
self.assertNotIn("promote-qualified", json.dumps(d))
self.assertNotIn("stable", json.dumps(d).lower())
class CliOutputContractTests(unittest.TestCase):
"""The CLI emits the decision file the workflow reads into GITHUB_OUTPUT."""
def test_decide_writes_json_with_should_retry(self):
import tempfile
script = Path(retry.__file__).resolve()
root = script.parent
release_path = root / "_tmp_retry_release.json"
runs_path = root / "_tmp_retry_runs.json"
out_path = root / "_tmp_retry_decision.json"
try:
release_path.write_text(json.dumps(_release()), encoding="utf-8")
runs_path.write_text(json.dumps([_run(conclusion="failure")]), encoding="utf-8")
import subprocess
result = subprocess.run(
["python3", str(script), "decide",
"--release-json", str(release_path),
"--runs-json", str(runs_path),
"--release-tag", TAG, "--tag-sha", SHA,
"--output", str(out_path)],
check=True, text=True, capture_output=True,
)
decision = json.loads(out_path.read_text(encoding="utf-8"))
self.assertTrue(decision["should_retry"])
self.assertEqual(decision["release_tag"], TAG)
self.assertIn("retrying", result.stdout)
finally:
for p in (release_path, runs_path, out_path):
p.unlink(missing_ok=True)
class WorkflowContractTests(unittest.TestCase):
def test_app_token_requests_only_required_permissions(self):
workflow = (Path(__file__).resolve().parents[1] / "workflows" /
"desktop_retry_beta_qualification.yml").read_text(encoding="utf-8")
self.assertIn(" permission-actions: write\n", workflow)
self.assertIn(" permission-contents: read\n", workflow)
if __name__ == "__main__":
unittest.main()