diff options
author | Gavin Mak <gavinmak@google.com> | 2025-06-13 17:53:38 -0700 |
---|---|---|
committer | LUCI <gerrit-scoped@luci-project-accounts.iam.gserviceaccount.com> | 2025-06-18 10:26:27 -0700 |
commit | 7b6ffed4ae3102b7c90592eeff8e28855cc25c11 (patch) | |
tree | 13be6b1470be5f3a2fad04797813936c034bb024 /tests/test_subcmds_sync.py | |
parent | b4b323a8bd02d52d060f7f6fa15ba045df5af5b2 (diff) | |
download | git-repo-7b6ffed4ae3102b7c90592eeff8e28855cc25c11.tar.gz |
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 <gavinmak@google.com>
Reviewed-by: Scott Lee <ddoman@google.com>
Tested-by: Gavin Mak <gavinmak@google.com>
Diffstat (limited to 'tests/test_subcmds_sync.py')
-rw-r--r-- | tests/test_subcmds_sync.py | 181 |
1 files changed, 180 insertions, 1 deletions
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: | |||
310 | self.name = name or relpath | 310 | self.name = name or relpath |
311 | self.objdir = objdir or relpath | 311 | self.objdir = objdir or relpath |
312 | 312 | ||
313 | self.use_git_worktrees = False | ||
314 | self.UseAlternates = False | ||
315 | self.manifest = mock.MagicMock() | ||
316 | self.manifest.GetProjectsWithName.return_value = [self] | ||
317 | self.config = mock.MagicMock() | ||
318 | self.EnableRepositoryExtension = mock.MagicMock() | ||
319 | |||
320 | def RelPath(self, local=None): | ||
321 | return self.relpath | ||
322 | |||
313 | def __str__(self): | 323 | def __str__(self): |
314 | return f"project: {self.relpath}" | 324 | return f"project: {self.relpath}" |
315 | 325 | ||
@@ -531,7 +541,11 @@ class InterleavedSyncTest(unittest.TestCase): | |||
531 | self.manifest.CloneBundle = False | 541 | self.manifest.CloneBundle = False |
532 | self.manifest.default.sync_j = 1 | 542 | self.manifest.default.sync_j = 1 |
533 | 543 | ||
534 | self.cmd = sync.Sync(manifest=self.manifest) | 544 | self.outer_client = mock.MagicMock() |
545 | self.outer_client.manifest.IsArchive = False | ||
546 | self.cmd = sync.Sync( | ||
547 | manifest=self.manifest, outer_client=self.outer_client | ||
548 | ) | ||
535 | self.cmd.outer_manifest = self.manifest | 549 | self.cmd.outer_manifest = self.manifest |
536 | 550 | ||
537 | # Mock projects. | 551 | # Mock projects. |
@@ -549,6 +563,21 @@ class InterleavedSyncTest(unittest.TestCase): | |||
549 | mock.patch.object(sync, "_PostRepoUpgrade").start() | 563 | mock.patch.object(sync, "_PostRepoUpgrade").start() |
550 | mock.patch.object(sync, "_PostRepoFetch").start() | 564 | mock.patch.object(sync, "_PostRepoFetch").start() |
551 | 565 | ||
566 | # Mock parallel context for worker tests. | ||
567 | self.parallel_context_patcher = mock.patch( | ||
568 | "subcmds.sync.Sync.get_parallel_context" | ||
569 | ) | ||
570 | self.mock_get_parallel_context = self.parallel_context_patcher.start() | ||
571 | self.sync_dict = {} | ||
572 | self.mock_context = { | ||
573 | "projects": [], | ||
574 | "sync_dict": self.sync_dict, | ||
575 | } | ||
576 | self.mock_get_parallel_context.return_value = self.mock_context | ||
577 | |||
578 | # Mock _GetCurrentBranchOnly for worker tests. | ||
579 | mock.patch.object(sync.Sync, "_GetCurrentBranchOnly").start() | ||
580 | |||
552 | def tearDown(self): | 581 | def tearDown(self): |
553 | """Clean up resources.""" | 582 | """Clean up resources.""" |
554 | shutil.rmtree(self.repodir) | 583 | shutil.rmtree(self.repodir) |
@@ -635,3 +664,153 @@ class InterleavedSyncTest(unittest.TestCase): | |||
635 | work_items_sets = {frozenset(item) for item in work_items} | 664 | work_items_sets = {frozenset(item) for item in work_items} |
636 | expected_sets = {frozenset([0, 2]), frozenset([1])} | 665 | expected_sets = {frozenset([0, 2]), frozenset([1])} |
637 | self.assertEqual(work_items_sets, expected_sets) | 666 | self.assertEqual(work_items_sets, expected_sets) |
667 | |||
668 | def _get_opts(self, args=None): | ||
669 | """Helper to get default options for worker tests.""" | ||
670 | if args is None: | ||
671 | args = ["--interleaved"] | ||
672 | opt, _ = self.cmd.OptionParser.parse_args(args) | ||
673 | # Set defaults for options used by the worker. | ||
674 | opt.quiet = True | ||
675 | opt.verbose = False | ||
676 | opt.force_sync = False | ||
677 | opt.clone_bundle = False | ||
678 | opt.tags = False | ||
679 | opt.optimized_fetch = False | ||
680 | opt.retry_fetches = 0 | ||
681 | opt.prune = False | ||
682 | opt.detach_head = False | ||
683 | opt.force_checkout = False | ||
684 | opt.rebase = False | ||
685 | return opt | ||
686 | |||
687 | def test_worker_successful_sync(self): | ||
688 | """Test _SyncProjectList with a successful fetch and checkout.""" | ||
689 | opt = self._get_opts() | ||
690 | project = self.projA | ||
691 | project.Sync_NetworkHalf = mock.Mock( | ||
692 | return_value=SyncNetworkHalfResult(error=None, remote_fetched=True) | ||
693 | ) | ||
694 | project.Sync_LocalHalf = mock.Mock() | ||
695 | project.manifest.manifestProject.config = mock.MagicMock() | ||
696 | self.mock_context["projects"] = [project] | ||
697 | |||
698 | with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer: | ||
699 | mock_sync_buf_instance = mock.MagicMock() | ||
700 | mock_sync_buf_instance.Finish.return_value = True | ||
701 | mock_sync_buffer.return_value = mock_sync_buf_instance | ||
702 | |||
703 | result_obj = self.cmd._SyncProjectList(opt, [0]) | ||
704 | |||
705 | self.assertEqual(len(result_obj.results), 1) | ||
706 | result = result_obj.results[0] | ||
707 | self.assertTrue(result.fetch_success) | ||
708 | self.assertTrue(result.checkout_success) | ||
709 | self.assertIsNone(result.fetch_error) | ||
710 | self.assertIsNone(result.checkout_error) | ||
711 | project.Sync_NetworkHalf.assert_called_once() | ||
712 | project.Sync_LocalHalf.assert_called_once() | ||
713 | |||
714 | def test_worker_fetch_fails(self): | ||
715 | """Test _SyncProjectList with a failed fetch.""" | ||
716 | opt = self._get_opts() | ||
717 | project = self.projA | ||
718 | fetch_error = GitError("Fetch failed") | ||
719 | project.Sync_NetworkHalf = mock.Mock( | ||
720 | return_value=SyncNetworkHalfResult( | ||
721 | error=fetch_error, remote_fetched=False | ||
722 | ) | ||
723 | ) | ||
724 | project.Sync_LocalHalf = mock.Mock() | ||
725 | self.mock_context["projects"] = [project] | ||
726 | |||
727 | result_obj = self.cmd._SyncProjectList(opt, [0]) | ||
728 | result = result_obj.results[0] | ||
729 | |||
730 | self.assertFalse(result.fetch_success) | ||
731 | self.assertFalse(result.checkout_success) | ||
732 | self.assertEqual(result.fetch_error, fetch_error) | ||
733 | self.assertIsNone(result.checkout_error) | ||
734 | project.Sync_NetworkHalf.assert_called_once() | ||
735 | project.Sync_LocalHalf.assert_not_called() | ||
736 | |||
737 | def test_worker_fetch_fails_exception(self): | ||
738 | """Test _SyncProjectList with an exception during fetch.""" | ||
739 | opt = self._get_opts() | ||
740 | project = self.projA | ||
741 | fetch_error = GitError("Fetch failed") | ||
742 | project.Sync_NetworkHalf = mock.Mock(side_effect=fetch_error) | ||
743 | project.Sync_LocalHalf = mock.Mock() | ||
744 | self.mock_context["projects"] = [project] | ||
745 | |||
746 | result_obj = self.cmd._SyncProjectList(opt, [0]) | ||
747 | result = result_obj.results[0] | ||
748 | |||
749 | self.assertFalse(result.fetch_success) | ||
750 | self.assertFalse(result.checkout_success) | ||
751 | self.assertEqual(result.fetch_error, fetch_error) | ||
752 | project.Sync_NetworkHalf.assert_called_once() | ||
753 | project.Sync_LocalHalf.assert_not_called() | ||
754 | |||
755 | def test_worker_checkout_fails(self): | ||
756 | """Test _SyncProjectList with an exception during checkout.""" | ||
757 | opt = self._get_opts() | ||
758 | project = self.projA | ||
759 | project.Sync_NetworkHalf = mock.Mock( | ||
760 | return_value=SyncNetworkHalfResult(error=None, remote_fetched=True) | ||
761 | ) | ||
762 | checkout_error = GitError("Checkout failed") | ||
763 | project.Sync_LocalHalf = mock.Mock(side_effect=checkout_error) | ||
764 | project.manifest.manifestProject.config = mock.MagicMock() | ||
765 | self.mock_context["projects"] = [project] | ||
766 | |||
767 | with mock.patch("subcmds.sync.SyncBuffer"): | ||
768 | result_obj = self.cmd._SyncProjectList(opt, [0]) | ||
769 | result = result_obj.results[0] | ||
770 | |||
771 | self.assertTrue(result.fetch_success) | ||
772 | self.assertFalse(result.checkout_success) | ||
773 | self.assertIsNone(result.fetch_error) | ||
774 | self.assertEqual(result.checkout_error, checkout_error) | ||
775 | project.Sync_NetworkHalf.assert_called_once() | ||
776 | project.Sync_LocalHalf.assert_called_once() | ||
777 | |||
778 | def test_worker_local_only(self): | ||
779 | """Test _SyncProjectList with --local-only.""" | ||
780 | opt = self._get_opts(["--interleaved", "--local-only"]) | ||
781 | project = self.projA | ||
782 | project.Sync_NetworkHalf = mock.Mock() | ||
783 | project.Sync_LocalHalf = mock.Mock() | ||
784 | project.manifest.manifestProject.config = mock.MagicMock() | ||
785 | self.mock_context["projects"] = [project] | ||
786 | |||
787 | with mock.patch("subcmds.sync.SyncBuffer") as mock_sync_buffer: | ||
788 | mock_sync_buf_instance = mock.MagicMock() | ||
789 | mock_sync_buf_instance.Finish.return_value = True | ||
790 | mock_sync_buffer.return_value = mock_sync_buf_instance | ||
791 | |||
792 | result_obj = self.cmd._SyncProjectList(opt, [0]) | ||
793 | result = result_obj.results[0] | ||
794 | |||
795 | self.assertTrue(result.fetch_success) | ||
796 | self.assertTrue(result.checkout_success) | ||
797 | project.Sync_NetworkHalf.assert_not_called() | ||
798 | project.Sync_LocalHalf.assert_called_once() | ||
799 | |||
800 | def test_worker_network_only(self): | ||
801 | """Test _SyncProjectList with --network-only.""" | ||
802 | opt = self._get_opts(["--interleaved", "--network-only"]) | ||
803 | project = self.projA | ||
804 | project.Sync_NetworkHalf = mock.Mock( | ||
805 | return_value=SyncNetworkHalfResult(error=None, remote_fetched=True) | ||
806 | ) | ||
807 | project.Sync_LocalHalf = mock.Mock() | ||
808 | self.mock_context["projects"] = [project] | ||
809 | |||
810 | result_obj = self.cmd._SyncProjectList(opt, [0]) | ||
811 | result = result_obj.results[0] | ||
812 | |||
813 | self.assertTrue(result.fetch_success) | ||
814 | self.assertTrue(result.checkout_success) | ||
815 | project.Sync_NetworkHalf.assert_called_once() | ||
816 | project.Sync_LocalHalf.assert_not_called() | ||