Skip to content

Commit 71b18e7

Browse files
committed
githyb_sync: refactor and test sync_relevant_subject
Factor out sync_relevant_subject() routine from sync_patches() Add test cases verifying base branch selection logic, when applying the series. Signed-off-by: Ihor Solodrai <[email protected]>
1 parent f1159bb commit 71b18e7

File tree

2 files changed

+151
-46
lines changed

2 files changed

+151
-46
lines changed

kernel_patches_daemon/github_sync.py

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,55 @@ async def checkout_and_patch_safe(
187187
)
188188
return None
189189

190+
async def sync_relevant_subject(self, subject: Subject) -> None:
191+
"""
192+
1. Get Subject's latest series
193+
2. Get series tags
194+
3. Map tags to a branches
195+
4. Start from first branch, try to apply and generate PR,
196+
if fails continue to next branch, if no more branches, generate a merge-conflict PR
197+
"""
198+
series = none_throws(await subject.latest_series)
199+
tags = await series.all_tags()
200+
logging.info(f"Processing {series.id}: {subject.subject} (tags: {tags})")
201+
202+
mapped_branches = await self.get_mapped_branches(series)
203+
if len(mapped_branches) == 0:
204+
logging.info(
205+
f"Skipping {series.id}: {subject.subject} for no mapped branches."
206+
)
207+
return
208+
209+
last_branch = mapped_branches[-1]
210+
for branch in mapped_branches:
211+
worker = self.workers[branch]
212+
# PR branch name == sid of the first known series
213+
pr_branch_name = await worker.subject_to_branch(subject)
214+
(applied, _, _) = await worker.try_apply_mailbox_series(
215+
pr_branch_name, series
216+
)
217+
if not applied:
218+
msg = f"Failed to apply series to {branch}, "
219+
if branch != last_branch:
220+
logging.info(msg + "moving to next.")
221+
continue
222+
else:
223+
logging.info(msg + "no more next, staying.")
224+
logging.info(f"Choosing branch {branch} to create/update PR.")
225+
pr = await self.checkout_and_patch_safe(worker, pr_branch_name, series)
226+
if pr is None:
227+
continue
228+
229+
logging.info(
230+
f"Created/updated {pr} ({pr.head.ref}): {pr.url} for series {series.id}"
231+
)
232+
await worker.sync_checks(pr, series)
233+
# Close out other PRs if exists
234+
self.close_existing_prs_with_same_base(list(self.workers.values()), pr)
235+
236+
break
237+
pass
238+
190239
async def sync_patches(self) -> None:
191240
"""
192241
One subject = one branch
@@ -225,52 +274,8 @@ async def sync_patches(self) -> None:
225274

226275
pw_done = time.time()
227276

228-
# 1. Get Subject's latest series
229-
# 2. Get series tags
230-
# 3. Map tags to a branches
231-
# 4. Start from first branch, try to apply and generate PR,
232-
# if fails continue to next branch, if no more branches, generate a merge-conflict PR
233277
for subject in self.subjects:
234-
series = none_throws(await subject.latest_series)
235-
logging.info(
236-
f"Processing {series.id}: {subject.subject} (tags: {await series.all_tags()})"
237-
)
238-
239-
mapped_branches = await self.get_mapped_branches(series)
240-
# series to apply - last known series
241-
if len(mapped_branches) == 0:
242-
logging.info(
243-
f"Skipping {series.id}: {subject.subject} for no mapped branches."
244-
)
245-
continue
246-
last_branch = mapped_branches[-1]
247-
for branch in mapped_branches:
248-
worker = self.workers[branch]
249-
# PR branch name == sid of the first known series
250-
pr_branch_name = await worker.subject_to_branch(subject)
251-
apply_mbox = await worker.try_apply_mailbox_series(
252-
pr_branch_name, series
253-
)
254-
if not apply_mbox[0]:
255-
msg = f"Failed to apply series to {branch}, "
256-
if branch != last_branch:
257-
logging.info(msg + "moving to next.")
258-
continue
259-
else:
260-
logging.info(msg + "no more next, staying.")
261-
logging.info(f"Choosing branch {branch} to create/update PR.")
262-
pr = await self.checkout_and_patch_safe(worker, pr_branch_name, series)
263-
if pr is None:
264-
continue
265-
266-
logging.info(
267-
f"Created/updated {pr} ({pr.head.ref}): {pr.url} for series {series.id}"
268-
)
269-
await worker.sync_checks(pr, series)
270-
# Close out other PRs if exists
271-
self.close_existing_prs_with_same_base(list(self.workers.values()), pr)
272-
273-
break
278+
await self.sync_relevant_subject(subject)
274279

275280
# sync old subjects
276281
subject_names = {x.subject for x in self.subjects}

tests/test_github_sync.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from kernel_patches_daemon.branch_worker import NewPRWithNoChangeException
1818
from kernel_patches_daemon.config import KPDConfig
19-
from kernel_patches_daemon.github_sync import GithubSync
19+
from kernel_patches_daemon.github_sync import GithubSync, HEAD_BASE_SEPARATOR
2020
from tests.common.patchwork_mock import init_pw_responses, load_test_data, PatchworkMock
2121

2222
TEST_BRANCH = "test-branch"
@@ -47,6 +47,7 @@
4747
},
4848
"tag_to_branch_mapping": {
4949
"bpf-next": [TEST_BPF_NEXT_BRANCH],
50+
"multibranch-tag": [TEST_BRANCH, TEST_BPF_NEXT_BRANCH],
5051
"__DEFAULT__": [TEST_BRANCH],
5152
},
5253
"base_directory": "/tmp",
@@ -178,6 +179,105 @@ async def test_checkout_and_patch_safe(self) -> None:
178179
)
179180
)
180181

182+
def _setup_sync_relevant_subject_mocks(self):
183+
"""Helper method to set up common mocks for sync_relevant_subject tests."""
184+
series_mock = MagicMock()
185+
series_mock.id = 12345
186+
series_mock.all_tags = AsyncMock(return_value=["tag"])
187+
subject_mock = MagicMock()
188+
subject_mock.subject = "Test subject"
189+
subject_mock.latest_series = AsyncMock(return_value=series_mock)()
190+
191+
return subject_mock, series_mock
192+
193+
async def test_sync_relevant_subject_no_mapped_branches(self) -> None:
194+
subject_mock, series_mock = self._setup_sync_relevant_subject_mocks()
195+
self._gh.get_mapped_branches = AsyncMock(return_value=[])
196+
self._gh.checkout_and_patch_safe = AsyncMock()
197+
198+
await self._gh.sync_relevant_subject(subject_mock)
199+
200+
self._gh.get_mapped_branches.assert_called_once_with(series_mock)
201+
self._gh.checkout_and_patch_safe.assert_not_called()
202+
203+
async def test_sync_relevant_subject_success_first_branch(self) -> None:
204+
series_prefix = "series/987654"
205+
series_branch_name = f"{series_prefix}{HEAD_BASE_SEPARATOR}{TEST_BRANCH}"
206+
207+
subject_mock, series_mock = self._setup_sync_relevant_subject_mocks()
208+
series_mock.all_tags = AsyncMock(return_value=["multibranch-tag"])
209+
subject_mock.branch = AsyncMock(return_value=series_prefix)()
210+
211+
pr_mock = MagicMock()
212+
pr_mock.head.ref = series_branch_name
213+
214+
self._gh.checkout_and_patch_safe = AsyncMock(return_value=pr_mock)
215+
self._gh.close_existing_prs_with_same_base = MagicMock()
216+
217+
worker_mock = self._gh.workers[TEST_BRANCH]
218+
worker_mock.sync_checks = AsyncMock()
219+
worker_mock.try_apply_mailbox_series = AsyncMock(
220+
return_value=(True, None, None)
221+
)
222+
223+
await self._gh.sync_relevant_subject(subject_mock)
224+
225+
worker_mock.try_apply_mailbox_series.assert_called_once_with(
226+
series_branch_name, series_mock
227+
)
228+
self._gh.checkout_and_patch_safe.assert_called_once_with(
229+
worker_mock, series_branch_name, series_mock
230+
)
231+
worker_mock.sync_checks.assert_called_once_with(pr_mock, series_mock)
232+
self._gh.close_existing_prs_with_same_base.assert_called_once_with(
233+
list(self._gh.workers.values()), pr_mock
234+
)
235+
236+
async def test_sync_relevant_subject_success_second_branch(self) -> None:
237+
"""Test sync_relevant_subject when series fails on first branch but succeeds on second."""
238+
series_prefix = "series/333333"
239+
bad_branch_name = f"{series_prefix}{HEAD_BASE_SEPARATOR}{TEST_BRANCH}"
240+
good_branch_name = f"{series_prefix}{HEAD_BASE_SEPARATOR}{TEST_BPF_NEXT_BRANCH}"
241+
242+
subject_mock, series_mock = self._setup_sync_relevant_subject_mocks()
243+
series_mock.all_tags = AsyncMock(return_value=["multibranch-tag"])
244+
245+
pr_mock = MagicMock()
246+
pr_mock.head.ref = good_branch_name
247+
248+
self._gh.checkout_and_patch_safe = AsyncMock(return_value=pr_mock)
249+
self._gh.close_existing_prs_with_same_base = MagicMock()
250+
251+
bad_worker_mock = self._gh.workers[TEST_BRANCH]
252+
bad_worker_mock.sync_checks = AsyncMock()
253+
bad_worker_mock.subject_to_branch = AsyncMock(return_value=bad_branch_name)
254+
bad_worker_mock.try_apply_mailbox_series = AsyncMock(
255+
return_value=(False, None, None)
256+
)
257+
258+
good_worker_mock = self._gh.workers[TEST_BPF_NEXT_BRANCH]
259+
good_worker_mock.sync_checks = AsyncMock()
260+
good_worker_mock.subject_to_branch = AsyncMock(return_value=good_branch_name)
261+
good_worker_mock.try_apply_mailbox_series = AsyncMock(
262+
return_value=(True, None, None)
263+
)
264+
265+
await self._gh.sync_relevant_subject(subject_mock)
266+
267+
bad_worker_mock.try_apply_mailbox_series.assert_called_once_with(
268+
bad_branch_name, series_mock
269+
)
270+
good_worker_mock.try_apply_mailbox_series.assert_called_once_with(
271+
good_branch_name, series_mock
272+
)
273+
self._gh.checkout_and_patch_safe.assert_called_once_with(
274+
good_worker_mock, good_branch_name, series_mock
275+
)
276+
good_worker_mock.sync_checks.assert_called_once_with(pr_mock, series_mock)
277+
self._gh.close_existing_prs_with_same_base.assert_called_once_with(
278+
list(self._gh.workers.values()), pr_mock
279+
)
280+
181281
@aioresponses()
182282
async def test_sync_patches_pr_summary_success(self, m: aioresponses) -> None:
183283
"""

0 commit comments

Comments
 (0)