~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/per_intertree/test_compare.py

  • Committer: Robert Collins
  • Date: 2009-07-29 04:57:50 UTC
  • mto: This revision was merged to the branch mainline in revision 4649.
  • Revision ID: robertc@robertcollins.net-20090729045750-qp1iw48rp7jyrfh2
Change the way iter_changes treats specific files to prevent InconsistentDeltas.

This is the internal core code that handles specific file operations like ``bzr st
FILENAME`` or ``bzr commit FILENAME``, and has been changed to include the
parent directories if they have altered, and when a directory stops being
a directory its children are always included.  This fixes a number of
causes for ``InconsistentDelta`` errors, and permits faster commit of
specific paths. (Robert Collins, #347649)

Show diffs side-by-side

added added

removed removed

Lines of Context:
217
217
        d = self.intertree_class(tree1, tree2).compare(
218
218
            specific_files=['a', 'b/c'])
219
219
        self.assertEqual(
220
 
            [('a', 'a-id', 'file'), ('b/c', 'c-id', 'file')],
 
220
            [('a', 'a-id', 'file'),  (u'b', 'b-id', 'directory'),
 
221
             ('b/c', 'c-id', 'file')],
221
222
            d.added)
222
223
        self.assertEqual([], d.modified)
223
224
        self.assertEqual([], d.removed)
224
225
        self.assertEqual([], d.renamed)
225
226
        self.assertEqual([], d.unchanged)
226
227
 
 
228
    def test_empty_to_abc_content_c_only(self):
 
229
        tree1 = self.make_branch_and_tree('1')
 
230
        tree2 = self.make_to_branch_and_tree('2')
 
231
        tree1 = self.get_tree_no_parents_no_content(tree1)
 
232
        tree2 = self.get_tree_no_parents_abc_content(tree2)
 
233
        tree1, tree2 = self.mutable_trees_to_test_trees(self, tree1, tree2)
 
234
        d = self.intertree_class(tree1, tree2).compare(
 
235
            specific_files=['b/c'])
 
236
        self.assertEqual(
 
237
            [(u'b', 'b-id', 'directory'), ('b/c', 'c-id', 'file')], d.added)
 
238
        self.assertEqual([], d.modified)
 
239
        self.assertEqual([], d.removed)
 
240
        self.assertEqual([], d.renamed)
 
241
        self.assertEqual([], d.unchanged)
 
242
 
227
243
    def test_empty_to_abc_content_b_only(self):
228
244
        """Restricting to a dir matches the children of the dir."""
229
245
        tree1 = self.make_branch_and_tree('1')
233
249
        tree1, tree2 = self.mutable_trees_to_test_trees(self, tree1, tree2)
234
250
        d = self.intertree_class(tree1, tree2).compare(specific_files=['b'])
235
251
        self.assertEqual(
236
 
            [('b', 'b-id', 'directory'),('b/c', 'c-id', 'file')],
 
252
            [('b', 'b-id', 'directory'), ('b/c', 'c-id', 'file')],
237
253
            d.added)
238
254
        self.assertEqual([], d.modified)
239
255
        self.assertEqual([], d.removed)
350
366
class TestIterChanges(TestCaseWithTwoTrees):
351
367
    """Test the comparison iterator"""
352
368
 
 
369
    def assertEqualIterChanges(self, left_changes, right_changes):
 
370
        """Assert that left_changes == right_changes.
 
371
 
 
372
        :param left_changes: A list of the output from iter_changes.
 
373
        :param right_changes: A list of the output from iter_changes.
 
374
        """
 
375
        left_changes = sorted(left_changes)
 
376
        right_changes = sorted(right_changes)
 
377
        if left_changes == right_changes:
 
378
            return
 
379
        # setify to get item by item differences, but we can only do this
 
380
        # when all the ids are unique on both sides.
 
381
        left_dict = dict((item[0], item) for item in left_changes)
 
382
        right_dict = dict((item[0], item) for item in right_changes)
 
383
        if (len(left_dict) != len(left_changes) or
 
384
            len(right_dict) != len(right_changes)):
 
385
            # Can't do a direct comparison. We could do a sequence diff, but
 
386
            # for now just do a regular assertEqual for now.
 
387
            self.assertEqual(left_changes, right_changes)
 
388
        keys = set(left_dict).union(set(right_dict))
 
389
        different = []
 
390
        same = []
 
391
        for key in keys:
 
392
            left_item = left_dict.get(key)
 
393
            right_item = right_dict.get(key)
 
394
            if left_item == right_item:
 
395
                same.append(str(left_item))
 
396
            else:
 
397
                different.append(" %s\n %s" % (left_item, right_item))
 
398
        self.fail("iter_changes output different. Unchanged items:\n" +
 
399
            "\n".join(same) + "\nChanged items:\n" + "\n".join(different))
 
400
 
353
401
    def do_iter_changes(self, tree1, tree2, **extra_args):
354
402
        """Helper to run iter_changes from tree1 to tree2.
355
403
 
379
427
                # Neither tree can be used
380
428
                return
381
429
        tree1.lock_read()
382
 
        tree2.lock_read()
383
430
        try:
384
 
            return tree2.has_changes(tree1)
 
431
            tree2.lock_read()
 
432
            try:
 
433
                return tree2.has_changes(tree1)
 
434
            finally:
 
435
                tree2.unlock()
385
436
        finally:
386
437
            tree1.unlock()
387
 
            tree2.unlock()
388
438
 
389
439
    def mutable_trees_to_locked_test_trees(self, tree1, tree2):
390
440
        """Convert the working trees into test trees.
572
622
        tree2 = self.get_tree_no_parents_abc_content(tree2)
573
623
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
574
624
        self.assertEqual(
575
 
            [self.added(tree2, 'a-id')],
 
625
            sorted([self.added(tree2, 'root-id'),
 
626
             self.added(tree2, 'a-id'),
 
627
             self.deleted(tree1, 'empty-root-id')]),
576
628
            self.do_iter_changes(tree1, tree2, specific_files=['a']))
577
629
 
578
 
    def test_abc_content_to_empty_to_abc_content_a_only(self):
 
630
    def test_abc_content_to_empty_a_only(self):
 
631
        # For deletes we don't need to pickup parents.
579
632
        tree1 = self.make_branch_and_tree('1')
580
633
        tree2 = self.make_to_branch_and_tree('2')
581
634
        tree1 = self.get_tree_no_parents_abc_content(tree1)
585
638
            [self.deleted(tree1, 'a-id')],
586
639
            self.do_iter_changes(tree1, tree2, specific_files=['a']))
587
640
 
 
641
    def test_abc_content_to_empty_b_only(self):
 
642
        # When b stops being a directory we have to pick up b/c as well.
 
643
        tree1 = self.make_branch_and_tree('1')
 
644
        tree2 = self.make_to_branch_and_tree('2')
 
645
        tree1 = self.get_tree_no_parents_abc_content(tree1)
 
646
        tree2 = self.get_tree_no_parents_no_content(tree2)
 
647
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
 
648
        self.assertEqual(
 
649
            [self.deleted(tree1, 'b-id'), self.deleted(tree1, 'c-id')],
 
650
            self.do_iter_changes(tree1, tree2, specific_files=['b']))
 
651
 
588
652
    def test_empty_to_abc_content_a_and_c_only(self):
589
653
        tree1 = self.make_branch_and_tree('1')
590
654
        tree2 = self.make_to_branch_and_tree('2')
591
655
        tree1 = self.get_tree_no_parents_no_content(tree1)
592
656
        tree2 = self.get_tree_no_parents_abc_content(tree2)
593
657
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
594
 
        expected_result = [self.added(tree2, 'a-id'), self.added(tree2, 'c-id')]
 
658
        expected_result = sorted([self.added(tree2, 'root-id'),
 
659
            self.added(tree2, 'a-id'), self.added(tree2, 'b-id'),
 
660
            self.added(tree2, 'c-id'), self.deleted(tree1, 'empty-root-id')])
595
661
        self.assertEqual(expected_result,
596
662
            self.do_iter_changes(tree1, tree2, specific_files=['a', 'b/c']))
597
663
 
601
667
        tree1 = self.get_tree_no_parents_abc_content(tree1)
602
668
        tree2 = self.get_tree_no_parents_no_content(tree2)
603
669
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
604
 
        def deleted(file_id):
605
 
            entry = tree1.inventory[file_id]
606
 
            path = tree1.id2path(file_id)
607
 
            return (file_id, (path, None), True, (True, False),
608
 
                    (entry.parent_id, None),
609
 
                    (entry.name, None), (entry.kind, None),
610
 
                    (entry.executable, None))
611
670
        expected_results = sorted([
612
671
            self.added(tree2, 'empty-root-id'),
613
 
            deleted('root-id'), deleted('a-id'),
614
 
            deleted('b-id'), deleted('c-id')])
 
672
            self.deleted(tree1, 'root-id'), self.deleted(tree1, 'a-id'),
 
673
            self.deleted(tree1, 'b-id'), self.deleted(tree1, 'c-id')])
615
674
        self.assertEqual(
616
675
            expected_results,
617
676
            self.do_iter_changes(tree1, tree2))
679
738
                           (False, False))],
680
739
                         self.do_iter_changes(tree1, tree2))
681
740
 
 
741
    def test_specific_with_rename_under_new_dir_reports_new_dir(self):
 
742
        tree1 = self.make_branch_and_tree('1')
 
743
        tree2 = self.make_to_branch_and_tree('2')
 
744
        tree1 = self.get_tree_no_parents_abc_content(tree1)
 
745
        tree2 = self.get_tree_no_parents_abc_content_7(tree2)
 
746
        tree1, tree2 = self.mutable_trees_to_test_trees(self, tree1, tree2)
 
747
        # d(d-id) is new, e is b-id renamed. 
 
748
        root_id = tree1.path2id('')
 
749
        self.assertEqualIterChanges(
 
750
            [self.renamed(tree1, tree2, 'b-id', False),
 
751
             self.added(tree2, 'd-id')],
 
752
             self.do_iter_changes(tree1, tree2, specific_files=['d/e']))
 
753
 
 
754
    def test_specific_with_rename_under_dir_under_new_dir_reports_new_dir(self):
 
755
        tree1 = self.make_branch_and_tree('1')
 
756
        tree2 = self.make_to_branch_and_tree('2')
 
757
        tree1 = self.get_tree_no_parents_abc_content(tree1)
 
758
        tree2 = self.get_tree_no_parents_abc_content_7(tree2)
 
759
        tree2.rename_one('a', 'd/e/a')
 
760
        tree1, tree2 = self.mutable_trees_to_test_trees(self, tree1, tree2)
 
761
        # d is new, d/e is b-id renamed, d/e/a is a-id renamed 
 
762
        root_id = tree1.path2id('')
 
763
        self.assertEqualIterChanges(
 
764
            [self.renamed(tree1, tree2, 'b-id', False),
 
765
             self.added(tree2, 'd-id'),
 
766
             self.renamed(tree1, tree2, 'a-id', False)],
 
767
             self.do_iter_changes(tree1, tree2, specific_files=['d/e/a']))
 
768
 
 
769
    def test_specific_old_parent_same_path_new_parent(self):
 
770
        # when a parent is new at its path, if the path was used in the source
 
771
        # it must be emitted as a change.
 
772
        tree1 = self.make_branch_and_tree('1')
 
773
        tree1.add(['a'], ['a-id'], ['file'])
 
774
        tree1.put_file_bytes_non_atomic('a-id', 'a file')
 
775
        tree2 = self.make_to_branch_and_tree('2')
 
776
        tree2.set_root_id(tree1.get_root_id())
 
777
        tree2.mkdir('a', 'b-id')
 
778
        tree2.add(['a/c'], ['c-id'], ['file'])
 
779
        tree2.put_file_bytes_non_atomic('c-id', 'another file')
 
780
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
 
781
        # a-id is gone, b-id and c-id are added.
 
782
        self.assertEqualIterChanges(
 
783
            [self.deleted(tree1, 'a-id'),
 
784
             self.added(tree2, 'b-id'),
 
785
             self.added(tree2, 'c-id')],
 
786
             self.do_iter_changes(tree1, tree2, specific_files=['a/c']))
 
787
 
 
788
    def test_specific_old_parent_becomes_file(self):
 
789
        # When an old parent included because of a path conflict becomes a
 
790
        # non-directory, its children have to be all included in the delta.
 
791
        tree1 = self.make_branch_and_tree('1')
 
792
        tree1.mkdir('a', 'a-old-id')
 
793
        tree1.mkdir('a/reparented', 'reparented-id')
 
794
        tree1.mkdir('a/deleted', 'deleted-id')
 
795
        tree2 = self.make_to_branch_and_tree('2')
 
796
        tree2.set_root_id(tree1.get_root_id())
 
797
        tree2.mkdir('a', 'a-new-id')
 
798
        tree2.mkdir('a/reparented', 'reparented-id')
 
799
        tree2.add(['b'], ['a-old-id'], ['file'])
 
800
        tree2.put_file_bytes_non_atomic('a-old-id', '')
 
801
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
 
802
        # a-old-id is kind-changed, a-new-id is added, reparented-id is renamed,
 
803
        # deleted-id is gone
 
804
        self.assertEqualIterChanges(
 
805
            [self.kind_changed(tree1, tree2, 'a-old-id'),
 
806
             self.added(tree2, 'a-new-id'),
 
807
             self.renamed(tree1, tree2, 'reparented-id', False),
 
808
             self.deleted(tree1, 'deleted-id')],
 
809
             self.do_iter_changes(tree1, tree2,
 
810
                specific_files=['a/reparented']))
 
811
 
 
812
    def test_specific_old_parent_is_deleted(self):
 
813
        # When an old parent included because of a path conflict is removed,
 
814
        # its children have to be all included in the delta.
 
815
        tree1 = self.make_branch_and_tree('1')
 
816
        tree1.mkdir('a', 'a-old-id')
 
817
        tree1.mkdir('a/reparented', 'reparented-id')
 
818
        tree1.mkdir('a/deleted', 'deleted-id')
 
819
        tree2 = self.make_to_branch_and_tree('2')
 
820
        tree2.set_root_id(tree1.get_root_id())
 
821
        tree2.mkdir('a', 'a-new-id')
 
822
        tree2.mkdir('a/reparented', 'reparented-id')
 
823
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
 
824
        # a-old-id is gone, a-new-id is added, reparented-id is renamed,
 
825
        # deleted-id is gone
 
826
        self.assertEqualIterChanges(
 
827
            [self.deleted(tree1, 'a-old-id'),
 
828
             self.added(tree2, 'a-new-id'),
 
829
             self.renamed(tree1, tree2, 'reparented-id', False),
 
830
             self.deleted(tree1, 'deleted-id')],
 
831
             self.do_iter_changes(tree1, tree2,
 
832
                specific_files=['a/reparented']))
 
833
 
 
834
    def test_specific_old_parent_child_collides_with_unselected_new(self):
 
835
        # When the child of an old parent because of a path conflict becomes a
 
836
        # path conflict with some unselected item in the source, that item also
 
837
        # needs to be included (because otherwise the output of applying the
 
838
        # delta to the source would have two items at that path).
 
839
        tree1 = self.make_branch_and_tree('1')
 
840
        tree1.mkdir('a', 'a-old-id')
 
841
        tree1.mkdir('a/reparented', 'reparented-id')
 
842
        tree1.mkdir('collides', 'collides-id')
 
843
        tree2 = self.make_to_branch_and_tree('2')
 
844
        tree2.set_root_id(tree1.get_root_id())
 
845
        tree2.mkdir('a', 'a-new-id')
 
846
        tree2.mkdir('a/selected', 'selected-id')
 
847
        tree2.mkdir('collides', 'reparented-id')
 
848
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
 
849
        # a-old-id is one, a-new-id is added, reparented-id is renamed,
 
850
        # collides-id is gone, selected-id is new.
 
851
        self.assertEqualIterChanges(
 
852
            [self.deleted(tree1, 'a-old-id'),
 
853
             self.added(tree2, 'a-new-id'),
 
854
             self.renamed(tree1, tree2, 'reparented-id', False),
 
855
             self.deleted(tree1, 'collides-id'),
 
856
             self.added(tree2, 'selected-id')],
 
857
             self.do_iter_changes(tree1, tree2,
 
858
                specific_files=['a/selected']))
 
859
 
 
860
    def test_specific_old_parent_child_dir_stops_being_dir(self):
 
861
        # When the child of an old parent also stops being a directory, its
 
862
        # children must also be included. This test checks that downward
 
863
        # recursion is done appropriately by starting at a child of the root of
 
864
        # a deleted subtree (a/reparented), and checking that a sibling
 
865
        # directory (a/deleted) has its children included in the delta.
 
866
        tree1 = self.make_branch_and_tree('1')
 
867
        tree1.mkdir('a', 'a-old-id')
 
868
        tree1.mkdir('a/reparented', 'reparented-id-1')
 
869
        tree1.mkdir('a/deleted', 'deleted-id-1')
 
870
        tree1.mkdir('a/deleted/reparented', 'reparented-id-2')
 
871
        tree1.mkdir('a/deleted/deleted', 'deleted-id-2')
 
872
        tree2 = self.make_to_branch_and_tree('2')
 
873
        tree2.set_root_id(tree1.get_root_id())
 
874
        tree2.mkdir('a', 'a-new-id')
 
875
        tree2.mkdir('a/reparented', 'reparented-id-1')
 
876
        tree2.mkdir('reparented', 'reparented-id-2')
 
877
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
 
878
        # a-old-id is gone, a-new-id is added, reparented-id-1, -2 are renamed,
 
879
        # deleted-id-1 and -2 are gone.
 
880
        self.assertEqualIterChanges(
 
881
            [self.deleted(tree1, 'a-old-id'),
 
882
             self.added(tree2, 'a-new-id'),
 
883
             self.renamed(tree1, tree2, 'reparented-id-1', False),
 
884
             self.renamed(tree1, tree2, 'reparented-id-2', False),
 
885
             self.deleted(tree1, 'deleted-id-1'),
 
886
             self.deleted(tree1, 'deleted-id-2')],
 
887
             self.do_iter_changes(tree1, tree2,
 
888
                specific_files=['a/reparented']))
 
889
 
682
890
    def test_file_rename_and_meta_modification(self):
683
891
        tree1 = self.make_branch_and_tree('1')
684
892
        tree2 = self.make_to_branch_and_tree('2')
1148
1356
 
1149
1357
        tree1, tree2 = self.mutable_trees_to_locked_test_trees(tree1, tree2)
1150
1358
        # We should notice that 'b' and all its children are deleted
1151
 
        expected = sorted([
 
1359
        expected = [
1152
1360
            self.content_changed(tree2, 'a-id'),
1153
1361
            self.content_changed(tree2, 'g-id'),
1154
1362
            self.deleted(tree1, 'b-id'),
1155
1363
            self.deleted(tree1, 'c-id'),
1156
1364
            self.deleted(tree1, 'd-id'),
1157
1365
            self.deleted(tree1, 'e-id'),
1158
 
            ])
1159
 
        self.assertEqual(expected, self.do_iter_changes(tree1, tree2))
 
1366
            ]
 
1367
        self.assertEqualIterChanges(expected,
 
1368
            self.do_iter_changes(tree1, tree2))
1160
1369
        self.check_has_changes(True, tree1, tree2)
1161
1370
 
1162
1371
    def test_added_unicode(self):
1273
1482
 
1274
1483
        self.assertEqual([self.renamed(tree1, tree2, rename_id, False)],
1275
1484
                         self.do_iter_changes(tree1, tree2))
1276
 
        self.assertEqual([self.renamed(tree1, tree2, rename_id, False)],
1277
 
                         self.do_iter_changes(tree1, tree2,
1278
 
                                              specific_files=[u'\u03b1']))
 
1485
        self.assertEqualIterChanges(
 
1486
            [self.renamed(tree1, tree2, rename_id, False)],
 
1487
            self.do_iter_changes(tree1, tree2, specific_files=[u'\u03b1']))
1279
1488
        self.check_has_changes(True, tree1, tree2)
1280
1489
 
1281
1490
    def test_unchanged_unicode(self):
1323
1532
            self.unchanged(tree1, subfile_id),
1324
1533
            ])
1325
1534
        self.assertEqual(expected,
1326
 
                         self.do_iter_changes(tree1, tree2,
1327
 
                                              specific_files=[u'\u03b1'],
1328
 
                                              include_unchanged=True))
 
1535
            self.do_iter_changes(tree1, tree2, specific_files=[u'\u03b1'],
 
1536
                include_unchanged=True))
1329
1537
 
1330
1538
    def test_unknown_unicode(self):
1331
1539
        tree1 = self.make_branch_and_tree('tree1')