From 41f1ffc1dda345c8e8489613fdb0d05f9b3c4fd6 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 17 Dec 2025 15:34:53 -0500 Subject: [PATCH] AAP-45541 Add test to recreate jobs/4075584/job_events/children_summary/ error (#16163) * Add test to recreate the error * Also begin to add detection for empty event * Remove breakpoint * fix: ignore events with missing event types * run linter and apply changes --------- Co-authored-by: AlanCoding Co-authored-by: Peter Braun --- awx/api/views/__init__.py | 8 +++- awx/main/tests/functional/api/test_events.py | 44 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 8393bac5c2..6e91c9ba61 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -3943,6 +3943,10 @@ class JobJobEventsChildrenSummary(APIView): prev_non_meta_event = events[0] for i, e in enumerate(events): + if not e['event']: + logging.warning(f'event type missing for event {e}') + continue + if not e['event'] in JobJobEventsChildrenSummary.meta_events: prev_non_meta_event = e if not e['uuid']: @@ -3980,9 +3984,11 @@ class JobJobEventsChildrenSummary(APIView): z = i next_non_meta_event = events[-1] while z < len(events): - if events[z]['event'] not in JobJobEventsChildrenSummary.meta_events: + if events[z]['event'] not in JobJobEventsChildrenSummary.meta_events + ('',): next_non_meta_event = events[z] break + elif not events[z]['event']: + logging.warning(f"JobEventChildrenSummary: job event 'event' field is unexpectedly empty for job {job.id}") z += 1 event_level_after = models.JobEvent.LEVEL_FOR_EVENT[next_non_meta_event['event']] if event_level_after and event_level_after > event_level_before: diff --git a/awx/main/tests/functional/api/test_events.py b/awx/main/tests/functional/api/test_events.py index 34ecf4d691..ebc8900cae 100644 --- a/awx/main/tests/functional/api/test_events.py +++ b/awx/main/tests/functional/api/test_events.py @@ -73,6 +73,7 @@ def test_job_job_events_children_summary(get, organization_factory, job_template job_id=job.pk, uuid='uuid3', parent_uuid='uuid2', event="playbook_on_task_start", counter=3, stdout='a' * 1024, job_created=job.created ).save() JobEvent.create_from_data(job_id=job.pk, uuid='uuid4', parent_uuid='', event='verbose', counter=4, stdout='a' * 1024, job_created=job.created).save() + JobEvent.create_from_data( job_id=job.pk, uuid='uuid5', parent_uuid='uuid1', event="playbook_on_play_start", counter=5, stdout='a' * 1024, job_created=job.created ).save() @@ -131,3 +132,46 @@ def test_job_job_events_children_summary_is_tree(get, organization_factory, job_ assert response.data["meta_event_nested_uuid"] == {} assert response.data["event_processing_finished"] == True assert response.data["is_tree"] == False + + +@pytest.mark.django_db +def test_job_job_events_children_summary_empty_event(get, organization_factory, job_template_factory): + objs = organization_factory("org", superusers=['admin']) + jt = job_template_factory("jt", organization=objs.organization, inventory='test_inv', project='test_proj').job_template + job = jt.create_unified_job() + url = reverse('api:job_job_events_children_summary', kwargs={'pk': job.pk}) + response = get(url, user=objs.superusers.admin, expect=200) + assert response.data["event_processing_finished"] == False + ''' + E1 + E2 + E3 + E4 (verbose) + E5 + ''' + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid1', parent_uuid='', event="playbook_on_start", counter=1, stdout='a' * 1024, job_created=job.created + ).save() + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid2', parent_uuid='uuid1', event="playbook_on_play_start", counter=2, stdout='a' * 1024, job_created=job.created + ).save() + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid3', parent_uuid='uuid2', event="playbook_on_task_start", counter=3, stdout='a' * 1024, job_created=job.created + ).save() + JobEvent.create_from_data(job_id=job.pk, uuid='uuid4', parent_uuid='', event='verbose', counter=4, stdout='a' * 1024, job_created=job.created).save() + + JobEvent.create_from_data(job_id=job.pk, uuid='uuid4', parent_uuid='', event='', counter=5, stdout='a' * 1024, job_created=job.created).save() + + JobEvent.create_from_data( + job_id=job.pk, uuid='uuid5', parent_uuid='uuid1', event="playbook_on_play_start", counter=6, stdout='a' * 1024, job_created=job.created + ).save() + + job.emitted_events = job.get_event_queryset().count() + job.status = "successful" + job.save() + url = reverse('api:job_job_events_children_summary', kwargs={'pk': job.pk}) + response = get(url, user=objs.superusers.admin, expect=200) + assert response.data["children_summary"] == {1: {"rowNumber": 0, "numChildren": 4}, 2: {"rowNumber": 1, "numChildren": 2}} + assert response.data["meta_event_nested_uuid"] == {4: "uuid2"} + assert response.data["event_processing_finished"] == True + assert response.data["is_tree"] == True