Skip to content

Commit 2d1c900

Browse files
test(redis_admin): cover repo_id/commit_sha aliases + ComputeComparison hydration
Pins the celery_broker contract added in this stack: * Parser: \`repo_id\` / \`commit_sha\` aliases work on UploadBreadcrumb- shaped envelopes; canonical \`repoid\` / \`commitid\` win when both are present; \`comparison_id\` extraction handles int + string-int. * \`_materialise\` calls the resolver once per page, applies it before \`_matches_filters\` (so a \`?repoid=\` filter narrows ComputeComparisonTask rows correctly), skips the resolver entirely when no \`comparison_id\`s are present, and falls back cleanly when the resolver returns \`{}\`. * \`_stream_frequency_aggregate\` resolves comparison buckets into the main \`(task, repoid, commitid)\` counter so the chart matches the changelist.
1 parent b0a8579 commit 2d1c900

1 file changed

Lines changed: 368 additions & 0 deletions

File tree

apps/codecov-api/redis_admin/tests/test_celery_broker_queue.py

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from django.test import TestCase
3737

3838
from redis_admin import conn as redis_admin_conn
39+
from redis_admin import queryset as redis_admin_queryset
3940
from redis_admin import settings as redis_admin_settings
4041
from redis_admin.admin import CeleryBrokerQueueAdmin, _resolve_repo_displays
4142
from redis_admin.families import parse_celery_envelope
@@ -157,6 +158,373 @@ def test_parse_envelope_handles_string_int_repoid():
157158
assert meta.repoid == 1234
158159

159160

161+
def test_parse_envelope_aliases_repo_id_and_commit_sha():
162+
"""`UploadBreadcrumbTask` carries snake_case `repo_id`/`commit_sha`
163+
rather than the canonical `repoid`/`commitid`. Surface them so the
164+
admin renders structured columns and accepts repoid/commitid
165+
filters on those messages.
166+
"""
167+
168+
envelope = _build_envelope(
169+
task="app.tasks.upload_breadcrumb.UploadBreadcrumbTask",
170+
kwargs={
171+
"repo_id": 18802842,
172+
"commit_sha": "af73fb73310312d4d2633449e50e6e141191c28c",
173+
"upload_ids": [],
174+
},
175+
)
176+
177+
meta = parse_celery_envelope(envelope)
178+
179+
assert meta.repoid == 18802842
180+
assert meta.commitid == "af73fb73310312d4d2633449e50e6e141191c28c"
181+
182+
183+
def test_parse_envelope_repoid_canonical_wins_over_alias():
184+
"""When both `repoid` and `repo_id` are present (defensive belt-and-
185+
braces), prefer the canonical name so a task that sets both stays
186+
consistent with every other call site.
187+
"""
188+
189+
envelope = _build_envelope(
190+
kwargs={"repoid": 1, "repo_id": 2, "commitid": "abc", "commit_sha": "xyz"},
191+
)
192+
193+
meta = parse_celery_envelope(envelope)
194+
195+
assert meta.repoid == 1
196+
assert meta.commitid == "abc"
197+
198+
199+
def test_parse_envelope_extracts_comparison_id():
200+
"""`ComputeComparisonTask` only carries `comparison_id` in kwargs;
201+
the queryset's hydrate pass turns it into `(repoid, commitid)`,
202+
but the parser surface keeps it on `CeleryEnvelopeMeta` so the
203+
queryset has something to hydrate from.
204+
"""
205+
206+
envelope = _build_envelope(
207+
task="app.tasks.compute_comparison.ComputeComparison",
208+
kwargs={"comparison_id": 7777},
209+
)
210+
211+
meta = parse_celery_envelope(envelope)
212+
213+
assert meta.comparison_id == 7777
214+
assert meta.repoid is None
215+
assert meta.commitid is None
216+
217+
218+
def test_parse_envelope_comparison_id_handles_string_int():
219+
"""JSON ints sometimes round-trip as strings; the comparison_id
220+
coercion mirrors `repoid` so a stringified value is still picked
221+
up by the chart hydration pass.
222+
"""
223+
224+
envelope = _build_envelope(kwargs={"comparison_id": "42"})
225+
226+
meta = parse_celery_envelope(envelope)
227+
228+
assert meta.comparison_id == 42
229+
230+
231+
# ---- ComputeComparison hydration ------------------------------------------
232+
233+
234+
def test_materialise_hydrates_comparison_rows_from_db(patched_broker, monkeypatch):
235+
"""A `ComputeComparisonTask` envelope only carries `comparison_id`,
236+
but after `_materialise` the row should expose the resolved
237+
`(repoid, commitid)` pair so the changelist can render and filter
238+
on those columns.
239+
"""
240+
241+
monkeypatch.setattr(
242+
redis_admin_queryset,
243+
"_resolve_comparison_repo_commits",
244+
lambda ids: {7777: (4242, "deadbeef")},
245+
)
246+
247+
_push(
248+
patched_broker,
249+
"notify",
250+
task="app.tasks.compute_comparison.ComputeComparison",
251+
kwargs={"comparison_id": 7777},
252+
)
253+
254+
rows = list(CeleryBrokerQueueQuerySet(CeleryBrokerQueue, queue_name="notify"))
255+
256+
assert len(rows) == 1
257+
assert rows[0].repoid == 4242
258+
assert rows[0].commitid == "deadbeef"
259+
260+
261+
def test_materialise_comparison_filter_by_repoid_after_hydration(
262+
patched_broker, monkeypatch
263+
):
264+
"""Hydration must run before `_matches_filters`, so a
265+
`repoid__exact` filter pushed down from the changelist still
266+
narrows ComputeComparisonTask rows whose envelope only carries
267+
`comparison_id`.
268+
"""
269+
270+
monkeypatch.setattr(
271+
redis_admin_queryset,
272+
"_resolve_comparison_repo_commits",
273+
lambda ids: {1: (10, "aaa"), 2: (20, "bbb")},
274+
)
275+
276+
_push(
277+
patched_broker,
278+
"notify",
279+
task="app.tasks.compute_comparison.ComputeComparison",
280+
kwargs={"comparison_id": 1},
281+
)
282+
_push(
283+
patched_broker,
284+
"notify",
285+
task="app.tasks.compute_comparison.ComputeComparison",
286+
kwargs={"comparison_id": 2},
287+
)
288+
289+
rows = list(
290+
CeleryBrokerQueueQuerySet(CeleryBrokerQueue, queue_name="notify").filter(
291+
repoid__exact=20
292+
)
293+
)
294+
295+
assert {r.repoid for r in rows} == {20}
296+
assert {r.commitid for r in rows} == {"bbb"}
297+
298+
299+
def test_materialise_uses_one_orm_call_for_full_window(patched_broker, monkeypatch):
300+
"""The hydrate helper must batch every comparison_id in the
301+
materialised window into a single ORM lookup so a 100-deep
302+
queue of ComputeComparison messages doesn't issue 100 queries.
303+
"""
304+
305+
call_count = {"n": 0}
306+
307+
def _fake_resolver(ids):
308+
call_count["n"] += 1
309+
return {cid: (100 + cid, f"sha-{cid}") for cid in ids}
310+
311+
monkeypatch.setattr(
312+
redis_admin_queryset,
313+
"_resolve_comparison_repo_commits",
314+
_fake_resolver,
315+
)
316+
317+
for cid in range(1, 11):
318+
_push(
319+
patched_broker,
320+
"notify",
321+
task="app.tasks.compute_comparison.ComputeComparison",
322+
kwargs={"comparison_id": cid},
323+
)
324+
325+
rows = list(CeleryBrokerQueueQuerySet(CeleryBrokerQueue, queue_name="notify"))
326+
327+
assert len(rows) == 10
328+
assert call_count["n"] == 1
329+
330+
331+
def test_materialise_skips_resolver_when_no_comparison_ids(patched_broker, monkeypatch):
332+
"""A queue carrying only ordinary repoid/commitid envelopes (i.e.
333+
the hot path) must not pay the ORM round-trip — operators run
334+
this admin without a CommitComparison-app deployment in some
335+
tests.
336+
"""
337+
338+
call_count = {"n": 0}
339+
340+
def _fake_resolver(ids):
341+
call_count["n"] += 1
342+
return {}
343+
344+
monkeypatch.setattr(
345+
redis_admin_queryset,
346+
"_resolve_comparison_repo_commits",
347+
_fake_resolver,
348+
)
349+
350+
_push(
351+
patched_broker,
352+
"notify",
353+
task="app.tasks.notify.NotifyTask",
354+
kwargs={"repoid": 1, "commitid": "a"},
355+
)
356+
357+
rows = list(CeleryBrokerQueueQuerySet(CeleryBrokerQueue, queue_name="notify"))
358+
359+
assert len(rows) == 1
360+
assert call_count["n"] == 0
361+
362+
363+
def test_materialise_keeps_comparison_id_when_resolution_returns_empty(
364+
patched_broker, monkeypatch
365+
):
366+
"""If the lookup misses (id deleted, ORM unavailable), the row
367+
falls back to bare `(None, None)` rather than crashing the
368+
changelist render.
369+
"""
370+
371+
monkeypatch.setattr(
372+
redis_admin_queryset,
373+
"_resolve_comparison_repo_commits",
374+
lambda ids: {},
375+
)
376+
377+
_push(
378+
patched_broker,
379+
"notify",
380+
task="app.tasks.compute_comparison.ComputeComparison",
381+
kwargs={"comparison_id": 9999},
382+
)
383+
384+
rows = list(CeleryBrokerQueueQuerySet(CeleryBrokerQueue, queue_name="notify"))
385+
386+
assert len(rows) == 1
387+
assert rows[0].repoid is None
388+
assert rows[0].commitid is None
389+
assert rows[0]._comparison_id == 9999
390+
391+
392+
def test_stream_frequency_aggregate_resolves_comparison_buckets(
393+
patched_broker, monkeypatch
394+
):
395+
"""The chart aggregator must surface ComputeComparison messages
396+
under their resolved `(task, repoid, commitid)` bucket rather
397+
than collapsing every one into the all-None row.
398+
"""
399+
400+
monkeypatch.setattr(
401+
redis_admin_queryset,
402+
"_resolve_comparison_repo_commits",
403+
lambda ids: {1: (10, "aaa"), 2: (20, "bbb")},
404+
)
405+
406+
_push(
407+
patched_broker,
408+
"notify",
409+
task="app.tasks.compute_comparison.ComputeComparison",
410+
kwargs={"comparison_id": 1},
411+
)
412+
_push(
413+
patched_broker,
414+
"notify",
415+
task="app.tasks.compute_comparison.ComputeComparison",
416+
kwargs={"comparison_id": 1},
417+
)
418+
_push(
419+
patched_broker,
420+
"notify",
421+
task="app.tasks.compute_comparison.ComputeComparison",
422+
kwargs={"comparison_id": 2},
423+
)
424+
425+
buckets, total = _stream_frequency_aggregate(patched_broker, "notify")
426+
427+
assert total == 3
428+
keys = {(b.task_name, b.repoid, b.commitid) for b in buckets}
429+
assert (
430+
"app.tasks.compute_comparison.ComputeComparison",
431+
10,
432+
"aaa",
433+
) in keys
434+
assert (
435+
"app.tasks.compute_comparison.ComputeComparison",
436+
20,
437+
"bbb",
438+
) in keys
439+
440+
441+
def test_resolve_comparison_repo_commits_returns_empty_for_falsy_ids():
442+
"""Falsy ids (0 / None) must skip the ORM round-trip entirely so
443+
`comparison_id=0` payloads don't pull a doomed empty IN()."""
444+
445+
from redis_admin.queryset import _resolve_comparison_repo_commits # noqa: PLC0415
446+
447+
assert _resolve_comparison_repo_commits([]) == {}
448+
assert _resolve_comparison_repo_commits([0, None]) == {}
449+
450+
451+
def test_resolve_comparison_repo_commits_batches_real_orm_call(monkeypatch):
452+
"""Happy path: the resolver calls `CommitComparison.objects.filter`
453+
once and shapes `(id, repoid, commitid)` tuples into the
454+
`{cid: (repoid, commitid)}` mapping the hydrate helpers consume.
455+
"""
456+
457+
from redis_admin.queryset import _resolve_comparison_repo_commits # noqa: PLC0415
458+
from shared.django_apps.compare import models as compare_models # noqa: PLC0415
459+
460+
captured: dict[str, Any] = {}
461+
462+
class _FakeQuerySet:
463+
def values_list(self, *fields):
464+
captured["fields"] = fields
465+
return [(1, 10, "aaa"), (2, 20, "bbb")]
466+
467+
class _FakeManager:
468+
def filter(self, **kwargs):
469+
captured["filter_kwargs"] = kwargs
470+
return _FakeQuerySet()
471+
472+
monkeypatch.setattr(compare_models.CommitComparison, "objects", _FakeManager())
473+
474+
result = _resolve_comparison_repo_commits([1, 2, 0])
475+
476+
assert result == {1: (10, "aaa"), 2: (20, "bbb")}
477+
assert captured["filter_kwargs"] == {"id__in": {1, 2}}
478+
assert captured["fields"] == (
479+
"id",
480+
"compare_commit__repository_id",
481+
"compare_commit__commitid",
482+
)
483+
484+
485+
def test_hydrate_comparison_rows_skips_rows_with_repoid_already_set(
486+
patched_broker, monkeypatch
487+
):
488+
"""Mixed page: ordinary `repoid`/`commitid` rows are left alone
489+
(no resolver call for them) and a row whose `comparison_id` isn't
490+
in the resolver mapping keeps its bare comparison_id rather than
491+
spuriously getting nulled.
492+
"""
493+
494+
monkeypatch.setattr(
495+
redis_admin_queryset,
496+
"_resolve_comparison_repo_commits",
497+
lambda ids: {1: (10, "aaa")}, # 9999 deliberately missing
498+
)
499+
500+
_push(
501+
patched_broker,
502+
"notify",
503+
task="app.tasks.notify.NotifyTask",
504+
kwargs={"repoid": 7, "commitid": "preset"},
505+
)
506+
_push(
507+
patched_broker,
508+
"notify",
509+
task="app.tasks.compute_comparison.ComputeComparison",
510+
kwargs={"comparison_id": 1},
511+
)
512+
_push(
513+
patched_broker,
514+
"notify",
515+
task="app.tasks.compute_comparison.ComputeComparison",
516+
kwargs={"comparison_id": 9999},
517+
)
518+
519+
rows = list(CeleryBrokerQueueQuerySet(CeleryBrokerQueue, queue_name="notify"))
520+
by_idx = {r.index_in_queue: r for r in rows}
521+
522+
assert by_idx[0].repoid == 7 and by_idx[0].commitid == "preset"
523+
assert by_idx[1].repoid == 10 and by_idx[1].commitid == "aaa"
524+
assert by_idx[2].repoid is None and by_idx[2].commitid is None
525+
assert by_idx[2]._comparison_id == 9999
526+
527+
160528
def test_parse_envelope_rejects_bool_in_int_fields():
161529
"""`bool` subclasses `int`; refuse to coerce True/False to 1/0."""
162530

0 commit comments

Comments
 (0)