diff options
Diffstat (limited to 'tests')
-rw-r--r-- | tests/test_subcmds_sync.py | 124 |
1 files changed, 123 insertions, 1 deletions
diff --git a/tests/test_subcmds_sync.py b/tests/test_subcmds_sync.py index b871317c..60f283af 100644 --- a/tests/test_subcmds_sync.py +++ b/tests/test_subcmds_sync.py | |||
@@ -305,8 +305,10 @@ class LocalSyncState(unittest.TestCase): | |||
305 | 305 | ||
306 | 306 | ||
307 | class FakeProject: | 307 | class FakeProject: |
308 | def __init__(self, relpath): | 308 | def __init__(self, relpath, name=None, objdir=None): |
309 | self.relpath = relpath | 309 | self.relpath = relpath |
310 | self.name = name or relpath | ||
311 | self.objdir = objdir or relpath | ||
310 | 312 | ||
311 | def __str__(self): | 313 | def __str__(self): |
312 | return f"project: {self.relpath}" | 314 | return f"project: {self.relpath}" |
@@ -513,3 +515,123 @@ class SyncCommand(unittest.TestCase): | |||
513 | self.cmd.Execute(self.opt, []) | 515 | self.cmd.Execute(self.opt, []) |
514 | self.assertIn(self.sync_local_half_error, e.aggregate_errors) | 516 | self.assertIn(self.sync_local_half_error, e.aggregate_errors) |
515 | self.assertIn(self.sync_network_half_error, e.aggregate_errors) | 517 | self.assertIn(self.sync_network_half_error, e.aggregate_errors) |
518 | |||
519 | |||
520 | class InterleavedSyncTest(unittest.TestCase): | ||
521 | """Tests for interleaved sync.""" | ||
522 | |||
523 | def setUp(self): | ||
524 | """Set up a sync command with mocks.""" | ||
525 | self.repodir = tempfile.mkdtemp(".repo") | ||
526 | self.manifest = mock.MagicMock(repodir=self.repodir) | ||
527 | self.manifest.repoProject.LastFetch = time.time() | ||
528 | self.manifest.repoProject.worktree = self.repodir | ||
529 | self.manifest.manifestProject.worktree = self.repodir | ||
530 | self.manifest.IsArchive = False | ||
531 | self.manifest.CloneBundle = False | ||
532 | self.manifest.default.sync_j = 1 | ||
533 | |||
534 | self.cmd = sync.Sync(manifest=self.manifest) | ||
535 | self.cmd.outer_manifest = self.manifest | ||
536 | |||
537 | # Mock projects. | ||
538 | self.projA = FakeProject("projA", objdir="objA") | ||
539 | self.projB = FakeProject("projB", objdir="objB") | ||
540 | self.projA_sub = FakeProject( | ||
541 | "projA/sub", name="projA_sub", objdir="objA_sub" | ||
542 | ) | ||
543 | self.projC = FakeProject("projC", objdir="objC") | ||
544 | |||
545 | # Mock methods that are not part of the core interleaved sync logic. | ||
546 | mock.patch.object(self.cmd, "_UpdateAllManifestProjects").start() | ||
547 | mock.patch.object(self.cmd, "_UpdateProjectsRevisionId").start() | ||
548 | mock.patch.object(self.cmd, "_ValidateOptionsWithManifest").start() | ||
549 | mock.patch.object(sync, "_PostRepoUpgrade").start() | ||
550 | mock.patch.object(sync, "_PostRepoFetch").start() | ||
551 | |||
552 | def tearDown(self): | ||
553 | """Clean up resources.""" | ||
554 | shutil.rmtree(self.repodir) | ||
555 | mock.patch.stopall() | ||
556 | |||
557 | def test_interleaved_fail_fast(self): | ||
558 | """Test that --fail-fast is respected in interleaved mode.""" | ||
559 | opt, args = self.cmd.OptionParser.parse_args( | ||
560 | ["--interleaved", "--fail-fast", "-j2"] | ||
561 | ) | ||
562 | opt.quiet = True | ||
563 | |||
564 | # With projA/sub, _SafeCheckoutOrder creates two batches: | ||
565 | # 1. [projA, projB] | ||
566 | # 2. [projA/sub] | ||
567 | # We want to fail on the first batch and ensure the second isn't run. | ||
568 | all_projects = [self.projA, self.projB, self.projA_sub] | ||
569 | mock.patch.object( | ||
570 | self.cmd, "GetProjects", return_value=all_projects | ||
571 | ).start() | ||
572 | |||
573 | # Mock ExecuteInParallel to simulate a failed run on the first batch of | ||
574 | # projects. | ||
575 | execute_mock = mock.patch.object( | ||
576 | self.cmd, "ExecuteInParallel", return_value=False | ||
577 | ).start() | ||
578 | |||
579 | with self.assertRaises(sync.SyncFailFastError): | ||
580 | self.cmd._SyncInterleaved( | ||
581 | opt, | ||
582 | args, | ||
583 | [], | ||
584 | self.manifest, | ||
585 | self.manifest.manifestProject, | ||
586 | all_projects, | ||
587 | {}, | ||
588 | ) | ||
589 | |||
590 | execute_mock.assert_called_once() | ||
591 | |||
592 | def test_interleaved_shared_objdir_serial(self): | ||
593 | """Test that projects with shared objdir are processed serially.""" | ||
594 | opt, args = self.cmd.OptionParser.parse_args(["--interleaved", "-j4"]) | ||
595 | opt.quiet = True | ||
596 | |||
597 | # Setup projects with a shared objdir. | ||
598 | self.projA.objdir = "common_objdir" | ||
599 | self.projC.objdir = "common_objdir" | ||
600 | |||
601 | all_projects = [self.projA, self.projB, self.projC] | ||
602 | mock.patch.object( | ||
603 | self.cmd, "GetProjects", return_value=all_projects | ||
604 | ).start() | ||
605 | |||
606 | def execute_side_effect(jobs, target, work_items, **kwargs): | ||
607 | # The callback is a partial object. The first arg is the set we | ||
608 | # need to update to avoid the stall detection. | ||
609 | synced_relpaths_set = kwargs["callback"].args[0] | ||
610 | projects_in_pass = self.cmd.get_parallel_context()["projects"] | ||
611 | for item in work_items: | ||
612 | for project_idx in item: | ||
613 | synced_relpaths_set.add( | ||
614 | projects_in_pass[project_idx].relpath | ||
615 | ) | ||
616 | return True | ||
617 | |||
618 | execute_mock = mock.patch.object( | ||
619 | self.cmd, "ExecuteInParallel", side_effect=execute_side_effect | ||
620 | ).start() | ||
621 | |||
622 | self.cmd._SyncInterleaved( | ||
623 | opt, | ||
624 | args, | ||
625 | [], | ||
626 | self.manifest, | ||
627 | self.manifest.manifestProject, | ||
628 | all_projects, | ||
629 | {}, | ||
630 | ) | ||
631 | |||
632 | execute_mock.assert_called_once() | ||
633 | jobs_arg, _, work_items = execute_mock.call_args.args | ||
634 | self.assertEqual(jobs_arg, 2) | ||
635 | work_items_sets = {frozenset(item) for item in work_items} | ||
636 | expected_sets = {frozenset([0, 2]), frozenset([1])} | ||
637 | self.assertEqual(work_items_sets, expected_sets) | ||