1from contextlib import suppress 2from datetime import datetime, timedelta 3from unittest import mock 4from unittest.mock import MagicMock, patch 5 6import ci_post_gantt 7import pytest 8from ci_gantt_chart import generate_gantt_chart 9from ci_post_gantt import Gitlab, MockGanttExit 10 11 12def create_mock_job( 13 name, id, status, created_at, queued_duration, started_at, finished_at=None 14): 15 mock_job = MagicMock() 16 mock_job.name = name 17 mock_job.status = status 18 mock_job.id = id 19 mock_job.created_at = created_at 20 mock_job.queued_duration = queued_duration 21 mock_job.started_at = started_at 22 mock_job.finished_at = finished_at 23 return mock_job 24 25 26@pytest.fixture 27def fake_pipeline(): 28 current_time = datetime.fromisoformat("2024-12-17 23:54:13.940091+00:00") 29 created_at = current_time - timedelta(minutes=10) 30 31 job1 = create_mock_job( 32 name="job1", 33 id="1", 34 status="success", 35 created_at=created_at.isoformat(), 36 queued_duration=1, # seconds 37 started_at=(created_at + timedelta(seconds=2)).isoformat(), 38 finished_at=(created_at + timedelta(minutes=1)).isoformat(), 39 ) 40 41 mock_pipeline = MagicMock() 42 mock_pipeline.web_url = "https://gitlab.freedesktop.org/mesa/mesa/-/pipelines/9999" 43 mock_pipeline.duration = 600 # Total pipeline duration in seconds 44 mock_pipeline.created_at = created_at.isoformat() 45 mock_pipeline.yaml_errors = False 46 mock_pipeline.jobs.list.return_value = [job1] 47 return mock_pipeline 48 49 50def test_generate_gantt_chart(fake_pipeline): 51 fig = generate_gantt_chart(fake_pipeline) 52 53 fig_dict = fig.to_dict() 54 assert "data" in fig_dict 55 56 # Extract all job names from the "y" axis in the Gantt chart data 57 all_job_names = set() 58 for trace in fig_dict["data"]: 59 if "y" in trace: 60 all_job_names.update(trace["y"]) 61 62 assert any( 63 "job1" in job for job in all_job_names 64 ), "job1 should be present in the Gantt chart" 65 66 67def test_ci_timeout(fake_pipeline): 68 fig = generate_gantt_chart(fake_pipeline, ci_timeout=1) 69 70 fig_dict = fig.to_dict() 71 72 timeout_line = None 73 for shape in fig_dict.get("layout", {}).get("shapes", []): 74 if shape.get("line", {}).get("dash") == "dash": 75 timeout_line = shape 76 break 77 78 assert timeout_line is not None, "Timeout line should exist in the Gantt chart" 79 timeout_x = timeout_line["x0"] 80 81 # Check that the timeout line is 1 minute after the pipeline creation time 82 pipeline_created_at = datetime.fromisoformat(fake_pipeline.created_at) 83 expected_timeout = pipeline_created_at + timedelta(minutes=1) 84 assert ( 85 timeout_x == expected_timeout 86 ), f"Timeout should be at {expected_timeout}, got {timeout_x}" 87 88 89def test_marge_bot_user_id(): 90 with patch("ci_post_gantt.Gitlab") as MockGitlab: 91 mock_gitlab_instance = MagicMock(spec=Gitlab) 92 mock_gitlab_instance.users = MagicMock() 93 MockGitlab.return_value = mock_gitlab_instance 94 95 marge_bot_user_id = 12345 96 ci_post_gantt.main("fake_token", None, marge_bot_user_id) 97 mock_gitlab_instance.users.get.assert_called_once_with(marge_bot_user_id) 98 99 100def test_project_ids(): 101 current_time = datetime.now() 102 project_id_1 = 176 103 event_1 = MagicMock() 104 event_1.project_id = project_id_1 105 event_1.created_at = (current_time - timedelta(days=1)).isoformat() 106 event_1.note = {"body": f"Event for project {project_id_1}"} 107 108 project_id_2 = 166 109 event_2 = MagicMock() 110 event_2.project_id = project_id_2 111 event_2.created_at = (current_time - timedelta(days=2)).isoformat() 112 event_2.note = {"body": f"Event for project {project_id_2}"} 113 114 with patch("ci_post_gantt.Gitlab") as MockGitlab: 115 mock_user = MagicMock() 116 mock_user.events = MagicMock() 117 mock_user.events.list.return_value = [event_1, event_2] 118 119 mock_gitlab_instance = MagicMock(spec=Gitlab) 120 mock_gitlab_instance.users = MagicMock() 121 mock_gitlab_instance.users.get.return_value = mock_user 122 MockGitlab.return_value = mock_gitlab_instance 123 124 last_event_date = (current_time - timedelta(days=3)).isoformat() 125 126 # Test a single project id 127 ci_post_gantt.main("fake_token", last_event_date) 128 marge_bot_single_project_scope = [ 129 event.note["body"] 130 for event in mock_user.events.list.return_value 131 if event.project_id == project_id_1 132 ] 133 assert f"Event for project {project_id_1}" in marge_bot_single_project_scope 134 assert f"Event for project {project_id_2}" not in marge_bot_single_project_scope 135 136 # Test multiple project ids 137 ci_post_gantt.main( 138 "fake_token", last_event_date, 9716, [project_id_1, project_id_2] 139 ) 140 141 marge_bot_multiple_project_scope = [ 142 event.note["body"] for event in mock_user.events.list.return_value 143 ] 144 assert f"Event for project {project_id_1}" in marge_bot_multiple_project_scope 145 assert f"Event for project {project_id_2}" in marge_bot_multiple_project_scope 146 147 148def test_add_gantt_after_pipeline_message(): 149 current_time = datetime.now() 150 151 plain_url = "https://gitlab.freedesktop.org/mesa/mesa/-/pipelines/12345" 152 plain_message = ( 153 f"I couldn't merge this branch: CI failed! See pipeline {plain_url}." 154 ) 155 event_plain = MagicMock() 156 event_plain.project_id = 176 157 event_plain.created_at = (current_time - timedelta(days=1)).isoformat() 158 event_plain.note = {"body": plain_message} 159 160 summary_url = "https://gitlab.freedesktop.org/mesa/mesa/-/pipelines/99999" 161 summary_message = ( 162 "I couldn't merge this branch: " 163 f"CI failed! See pipeline {summary_url}.<br>There were problems with job:" 164 "[lavapipe](https://gitlab.freedesktop.org/mesa/mesa/-/jobs/68141218)<details><summary> " 165 "3 crashed tests</summary>dEQP-VK.ray_query.builtin.instancecustomindex.frag.aabbs,Crash<br>dEQP" 166 "-VK.ray_query.builtin.objecttoworld.frag.aabbs,Crash<br>dEQP-VK.sparse_resources.shader_intrinsics." 167 "2d_array_sparse_fetch.g16_b16r16_2plane_444_unorm.11_37_3_nontemporal,Crash<br></details>" 168 ) 169 event_with_summary = MagicMock() 170 event_with_summary.project_id = 176 171 event_with_summary.created_at = (current_time - timedelta(days=1)).isoformat() 172 event_with_summary.note = {"body": summary_message} 173 174 with patch("ci_post_gantt.Gitlab") as MockGitlab, patch( 175 "ci_post_gantt.get_gitlab_pipeline_from_url", return_value=None 176 ) as mock_get_gitlab_pipeline_from_url: 177 178 def safe_mock(*args, **kwargs): 179 with suppress(TypeError): 180 raise MockGanttExit("Exiting for test purposes") 181 182 mock_get_gitlab_pipeline_from_url.side_effect = safe_mock 183 184 mock_user = MagicMock() 185 mock_user.events = MagicMock() 186 mock_user.events.list.return_value = [event_plain, event_with_summary] 187 188 mock_gitlab_instance = MagicMock(spec=Gitlab) 189 mock_gitlab_instance.users = MagicMock() 190 mock_gitlab_instance.users.get.return_value = mock_user 191 MockGitlab.return_value = mock_gitlab_instance 192 193 last_event_date = (current_time - timedelta(days=3)).isoformat() 194 ci_post_gantt.main("fake_token", last_event_date, 12345) 195 mock_get_gitlab_pipeline_from_url.assert_has_calls( 196 [ 197 mock.call(mock_gitlab_instance, plain_url), 198 mock.call(mock_gitlab_instance, summary_url), 199 ], 200 any_order=True, 201 ) 202