From 7b6ffed4ae3102b7c90592eeff8e28855cc25c11 Mon Sep 17 00:00:00 2001 From: Gavin Mak Date: Fri, 13 Jun 2025 17:53:38 -0700 Subject: sync: Implement --interleaved sync worker For each assigned project, the worker sequentially calls Sync_NetworkHalf and Sync_LocalHalf, respecting --local-only and --network-only flags. To prevent scrambled progress bars, all stderr output from the checkout phase is captured (shown with --verbose). Result objects now carry status and timing information from the worker for state updates. Bug: 421935613 Change-Id: I398602e08a375e974a8914e5fa48ffae673dda9b Reviewed-on: https://gerrit-review.googlesource.com/c/git-repo/+/483301 Commit-Queue: Gavin Mak Reviewed-by: Scott Lee Tested-by: Gavin Mak --- tests/test_subcmds_sync.py | 181 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) (limited to 'tests') diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index 60f283af..e7213ed9 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py @@ -310,6 +310,16 @@ class FakeProject: self.name = name or relpath self.objdir = objdir or relpath + self.use_git_worktrees = False + self.UseAlternates = False + self.manifest = mock.MagicMock() + self.manifest.GetProjectsWithName.return_value = [self] + self.config = mock.MagicMock() + self.EnableRepositoryExtension = mock.MagicMock() + + def RelPath(self, local=None): + return self.relpath + def __str__(self): return f"project: {self.relpath}" @@ -531,7 +541,11 @@ class InterleavedSyncTest(unittest.TestCase): self.manifest.CloneBundle = False self.manifest.default.sync_j = 1 - self.cmd = sync.Sync(manifest=self.manifest) + self.outer_client = mock.MagicMock() + self.outer_client.manifest.IsArchive = False + self.cmd = sync.Sync( + manifest=self.manifest, outer_client=self.outer_client + ) self.cmd.outer_manifest = self.manifest # Mock projects. @@ -549,6 +563,21 @@ class InterleavedSyncTest(unittest.TestCase): mock.patch.object(sync, "_PostRepoUpgrade").start() mock.patch.object(sync, "_PostRepoFetch").start() + # Mock parallel context for worker tests. + self.parallel_context_patcher = mock.patch( + "subcmds.sync.Sync.get_parallel_context" + ) + self.mock_get_parallel_context = self.parallel_context_patcher.start() + self.sync_dict = {} + self.mock_context = { + "projects": [], + "sync_dict": self.sync_dict, + } + self.mock_get_parallel_context.return_value = self.mock_context + + # Mock _GetCurrentBranchOnly for worker tests. + mock.patch.object(sync.Sync, "_GetCurrentBranchOnly").start() + def tearDown(self): """Clean up resources.""" shutil.rmtree(self.repodir) @@ -635,3 +664,153 @@ class InterleavedSyncTest(unittest.TestCase): work_items_sets = {frozenset(item) for item in work_items} expected_sets = {frozenset([0, 2]), frozenset([1])} self.assertEqual(work_items_sets, expected_sets) + + def _get_opts(self, args=None): + """Helper to get default options for worker tests.""" + if args is None: + args = ["--interleaved"] + opt, _ = self.cmd.OptionParser.parse_args(args) + # Set defaults for options used by the worker. + opt.quiet = True + opt.verbose = False + opt.force_sync = False + opt.clone_bundle = False + opt.tags = False + opt.optimized_fetch = False + opt.retry_fetches = 0 + opt.prune = False + opt.detach_head = False + opt.force_checkout = False + opt.rebase = False + return opt + + def test_worker_successful_sync(self): + """Test _SyncProjectList with a successful fetch and checkout.""" + opt = self._get_opts() + project = self.projA + project.Sync_NetworkHalf = mock.Mock( + return_value=SyncNetworkHalfResult(error=None, remote_fetched=True) + ) + project.Sync_LocalHalf = mock.Mock() + project.manifest.manifestProject.config = mock.MagicMock() + self.mock_context["projects"] = [project] + + with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer: + mock_sync_buf_instance = mock.MagicMock() + mock_sync_buf_instance.Finish.return_value = True + mock_sync_buffer.return_value = mock_sync_buf_instance + + result_obj = self.cmd._SyncProjectList(opt, [0]) + + self.assertEqual(len(result_obj.results), 1) + result = result_obj.results[0] + self.assertTrue(result.fetch_success) + self.assertTrue(result.checkout_success) + self.assertIsNone(result.fetch_error) + self.assertIsNone(result.checkout_error) + project.Sync_NetworkHalf.assert_called_once() + project.Sync_LocalHalf.assert_called_once() + + def test_worker_fetch_fails(self): + """Test _SyncProjectList with a failed fetch.""" + opt = self._get_opts() + project = self.projA + fetch_error = GitError("Fetch failed") + project.Sync_NetworkHalf = mock.Mock( + return_value=SyncNetworkHalfResult( + error=fetch_error, remote_fetched=False + ) + ) + project.Sync_LocalHalf = mock.Mock() + self.mock_context["projects"] = [project] + + result_obj = self.cmd._SyncProjectList(opt, [0]) + result = result_obj.results[0] + + self.assertFalse(result.fetch_success) + self.assertFalse(result.checkout_success) + self.assertEqual(result.fetch_error, fetch_error) + self.assertIsNone(result.checkout_error) + project.Sync_NetworkHalf.assert_called_once() + project.Sync_LocalHalf.assert_not_called() + + def test_worker_fetch_fails_exception(self): + """Test _SyncProjectList with an exception during fetch.""" + opt = self._get_opts() + project = self.projA + fetch_error = GitError("Fetch failed") + project.Sync_NetworkHalf = mock.Mock(side_effect=fetch_error) + project.Sync_LocalHalf = mock.Mock() + self.mock_context["projects"] = [project] + + result_obj = self.cmd._SyncProjectList(opt, [0]) + result = result_obj.results[0] + + self.assertFalse(result.fetch_success) + self.assertFalse(result.checkout_success) + self.assertEqual(result.fetch_error, fetch_error) + project.Sync_NetworkHalf.assert_called_once() + project.Sync_LocalHalf.assert_not_called() + + def test_worker_checkout_fails(self): + """Test _SyncProjectList with an exception during checkout.""" + opt = self._get_opts() + project = self.projA + project.Sync_NetworkHalf = mock.Mock( + return_value=SyncNetworkHalfResult(error=None, remote_fetched=True) + ) + checkout_error = GitError("Checkout failed") + project.Sync_LocalHalf = mock.Mock(side_effect=checkout_error) + project.manifest.manifestProject.config = mock.MagicMock() + self.mock_context["projects"] = [project] + + with mock.patch("subcmds.sync.SyncBuffer"): + result_obj = self.cmd._SyncProjectList(opt, [0]) + result = result_obj.results[0] + + self.assertTrue(result.fetch_success) + self.assertFalse(result.checkout_success) + self.assertIsNone(result.fetch_error) + self.assertEqual(result.checkout_error, checkout_error) + project.Sync_NetworkHalf.assert_called_once() + project.Sync_LocalHalf.assert_called_once() + + def test_worker_local_only(self): + """Test _SyncProjectList with --local-only.""" + opt = self._get_opts(["--interleaved", "--local-only"]) + project = self.projA + project.Sync_NetworkHalf = mock.Mock() + project.Sync_LocalHalf = mock.Mock() + project.manifest.manifestProject.config = mock.MagicMock() + self.mock_context["projects"] = [project] + + with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer: + mock_sync_buf_instance = mock.MagicMock() + mock_sync_buf_instance.Finish.return_value = True + mock_sync_buffer.return_value = mock_sync_buf_instance + + result_obj = self.cmd._SyncProjectList(opt, [0]) + result = result_obj.results[0] + + self.assertTrue(result.fetch_success) + self.assertTrue(result.checkout_success) + project.Sync_NetworkHalf.assert_not_called() + project.Sync_LocalHalf.assert_called_once() + + def test_worker_network_only(self): + """Test _SyncProjectList with --network-only.""" + opt = self._get_opts(["--interleaved", "--network-only"]) + project = self.projA + project.Sync_NetworkHalf = mock.Mock( + return_value=SyncNetworkHalfResult(error=None, remote_fetched=True) + ) + project.Sync_LocalHalf = mock.Mock() + self.mock_context["projects"] = [project] + + result_obj = self.cmd._SyncProjectList(opt, [0]) + result = result_obj.results[0] + + self.assertTrue(result.fetch_success) + self.assertTrue(result.checkout_success) + project.Sync_NetworkHalf.assert_called_once() + project.Sync_LocalHalf.assert_not_called() -- cgit v1.2.3-54-g00ecf