Merge lp:~jelmer/bzr/per-repository-vf-reconcile into lp:bzr

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 5710
Proposed branch: lp:~jelmer/bzr/per-repository-vf-reconcile
Merge into: lp:bzr
Prerequisite: lp:~jelmer/bzr/per-repository-vf
Diff against target: 1571 lines (+764/-749)
3 files modified
bzrlib/tests/per_repository/__init__.py (+1/-745)
bzrlib/tests/per_repository_vf/__init__.py (+1/-0)
bzrlib/tests/per_repository_vf/test_check_reconcile.py (+762/-4)
To merge this branch: bzr merge lp:~jelmer/bzr/per-repository-vf-reconcile
Reviewer Review Type Date Requested Status
Vincent Ladeuil Needs Fixing
Review via email: mp+52283@code.launchpad.net

Commit message

Move VersionedFile-specific reconcile tests to bzrlib.tests.per_repository_vf.

Description of the change

This moves test_check_reconcile into bzrlib.tests.per_repository_vf from bzrlib.tests.per_repository.

These tests only make sense for repository formats that implement the full VersionedFiles interface; e.g. foreign formats don't implement VersionedFiles.add_lines (and aren't able to).

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

Hmm, kind of the same feeling here, why not put all the broken scenarios where they are used (i.e. in per_repository_vf/test_check_reconcile.py instead of per_repository_vf/__init__.py) ?

review: Needs Fixing
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/tests/per_repository/__init__.py'
2--- bzrlib/tests/per_repository/__init__.py 2011-02-08 15:25:49 +0000
3+++ bzrlib/tests/per_repository/__init__.py 2011-03-08 20:43:14 +0000
4@@ -27,11 +27,9 @@
5 from bzrlib import (
6 repository,
7 )
8-from bzrlib.revision import NULL_REVISION
9 from bzrlib.remote import RemoteRepositoryFormat
10 from bzrlib.tests import (
11 default_transport,
12- multiply_scenarios,
13 multiply_tests,
14 test_server,
15 )
16@@ -108,737 +106,6 @@
17 relpath, shared=shared, format=format)
18
19
20-class BrokenRepoScenario(object):
21- """Base class for defining scenarios for testing check and reconcile.
22-
23- A subclass needs to define the following methods:
24- :populate_repository: a method to use to populate a repository with
25- sample revisions, inventories and file versions.
26- :all_versions_after_reconcile: all the versions in repository after
27- reconcile. run_test verifies that the text of each of these
28- versions of the file is unchanged by the reconcile.
29- :populated_parents: a list of (parents list, revision). Each version
30- of the file is verified to have the given parents before running
31- the reconcile. i.e. this is used to assert that the repo from the
32- factory is what we expect.
33- :corrected_parents: a list of (parents list, revision). Each version
34- of the file is verified to have the given parents after the
35- reconcile. i.e. this is used to assert that reconcile made the
36- changes we expect it to make.
37-
38- A subclass may define the following optional method as well:
39- :corrected_fulltexts: a list of file versions that should be stored as
40- fulltexts (not deltas) after reconcile. run_test will verify that
41- this occurs.
42- """
43-
44- def __init__(self, test_case):
45- self.test_case = test_case
46-
47- def make_one_file_inventory(self, repo, revision, parents,
48- inv_revision=None, root_revision=None,
49- file_contents=None, make_file_version=True):
50- return self.test_case.make_one_file_inventory(
51- repo, revision, parents, inv_revision=inv_revision,
52- root_revision=root_revision, file_contents=file_contents,
53- make_file_version=make_file_version)
54-
55- def add_revision(self, repo, revision_id, inv, parent_ids):
56- return self.test_case.add_revision(repo, revision_id, inv, parent_ids)
57-
58- def corrected_fulltexts(self):
59- return []
60-
61- def repository_text_key_index(self):
62- result = {}
63- if self.versioned_root:
64- result.update(self.versioned_repository_text_keys())
65- result.update(self.repository_text_keys())
66- return result
67-
68-
69-class UndamagedRepositoryScenario(BrokenRepoScenario):
70- """A scenario where the repository has no damage.
71-
72- It has a single revision, 'rev1a', with a single file.
73- """
74-
75- def all_versions_after_reconcile(self):
76- return ('rev1a', )
77-
78- def populated_parents(self):
79- return (((), 'rev1a'), )
80-
81- def corrected_parents(self):
82- # Same as the populated parents, because there was nothing wrong.
83- return self.populated_parents()
84-
85- def check_regexes(self, repo):
86- return ["0 unreferenced text versions"]
87-
88- def populate_repository(self, repo):
89- # make rev1a: A well-formed revision, containing 'a-file'
90- inv = self.make_one_file_inventory(
91- repo, 'rev1a', [], root_revision='rev1a')
92- self.add_revision(repo, 'rev1a', inv, [])
93- self.versioned_root = repo.supports_rich_root()
94-
95- def repository_text_key_references(self):
96- result = {}
97- if self.versioned_root:
98- result.update({('TREE_ROOT', 'rev1a'): True})
99- result.update({('a-file-id', 'rev1a'): True})
100- return result
101-
102- def repository_text_keys(self):
103- return {('a-file-id', 'rev1a'):[NULL_REVISION]}
104-
105- def versioned_repository_text_keys(self):
106- return {('TREE_ROOT', 'rev1a'):[NULL_REVISION]}
107-
108-
109-class FileParentIsNotInRevisionAncestryScenario(BrokenRepoScenario):
110- """A scenario where a revision 'rev2' has 'a-file' with a
111- parent 'rev1b' that is not in the revision ancestry.
112-
113- Reconcile should remove 'rev1b' from the parents list of 'a-file' in
114- 'rev2', preserving 'rev1a' as a parent.
115- """
116-
117- def all_versions_after_reconcile(self):
118- return ('rev1a', 'rev2')
119-
120- def populated_parents(self):
121- return (
122- ((), 'rev1a'),
123- ((), 'rev1b'), # Will be gc'd
124- (('rev1a', 'rev1b'), 'rev2')) # Will have parents trimmed
125-
126- def corrected_parents(self):
127- return (
128- ((), 'rev1a'),
129- (None, 'rev1b'),
130- (('rev1a',), 'rev2'))
131-
132- def check_regexes(self, repo):
133- return [r"\* a-file-id version rev2 has parents \('rev1a', 'rev1b'\) "
134- r"but should have \('rev1a',\)",
135- "1 unreferenced text versions",
136- ]
137-
138- def populate_repository(self, repo):
139- # make rev1a: A well-formed revision, containing 'a-file'
140- inv = self.make_one_file_inventory(
141- repo, 'rev1a', [], root_revision='rev1a')
142- self.add_revision(repo, 'rev1a', inv, [])
143-
144- # make rev1b, which has no Revision, but has an Inventory, and
145- # a-file
146- inv = self.make_one_file_inventory(
147- repo, 'rev1b', [], root_revision='rev1b')
148- repo.add_inventory('rev1b', inv, [])
149-
150- # make rev2, with a-file.
151- # a-file has 'rev1b' as an ancestor, even though this is not
152- # mentioned by 'rev1a', making it an unreferenced ancestor
153- inv = self.make_one_file_inventory(
154- repo, 'rev2', ['rev1a', 'rev1b'])
155- self.add_revision(repo, 'rev2', inv, ['rev1a'])
156- self.versioned_root = repo.supports_rich_root()
157-
158- def repository_text_key_references(self):
159- result = {}
160- if self.versioned_root:
161- result.update({('TREE_ROOT', 'rev1a'): True,
162- ('TREE_ROOT', 'rev2'): True})
163- result.update({('a-file-id', 'rev1a'): True,
164- ('a-file-id', 'rev2'): True})
165- return result
166-
167- def repository_text_keys(self):
168- return {('a-file-id', 'rev1a'):[NULL_REVISION],
169- ('a-file-id', 'rev2'):[('a-file-id', 'rev1a')]}
170-
171- def versioned_repository_text_keys(self):
172- return {('TREE_ROOT', 'rev1a'):[NULL_REVISION],
173- ('TREE_ROOT', 'rev2'):[('TREE_ROOT', 'rev1a')]}
174-
175-
176-class FileParentHasInaccessibleInventoryScenario(BrokenRepoScenario):
177- """A scenario where a revision 'rev3' containing 'a-file' modified in
178- 'rev3', and with a parent which is in the revision ancestory, but whose
179- inventory cannot be accessed at all.
180-
181- Reconcile should remove the file version parent whose inventory is
182- inaccessbile (i.e. remove 'rev1c' from the parents of a-file's rev3).
183- """
184-
185- def all_versions_after_reconcile(self):
186- return ('rev2', 'rev3')
187-
188- def populated_parents(self):
189- return (
190- ((), 'rev2'),
191- (('rev1c',), 'rev3'))
192-
193- def corrected_parents(self):
194- return (
195- ((), 'rev2'),
196- ((), 'rev3'))
197-
198- def check_regexes(self, repo):
199- return [r"\* a-file-id version rev3 has parents "
200- r"\('rev1c',\) but should have \(\)",
201- ]
202-
203- def populate_repository(self, repo):
204- # make rev2, with a-file
205- # a-file is sane
206- inv = self.make_one_file_inventory(repo, 'rev2', [])
207- self.add_revision(repo, 'rev2', inv, [])
208-
209- # make ghost revision rev1c, with a version of a-file present so
210- # that we generate a knit delta against this version. In real life
211- # the ghost might never have been present or rev3 might have been
212- # generated against a revision that was present at the time. So
213- # currently we have the full history of a-file present even though
214- # the inventory and revision objects are not.
215- self.make_one_file_inventory(repo, 'rev1c', [])
216-
217- # make rev3 with a-file
218- # a-file refers to 'rev1c', which is a ghost in this repository, so
219- # a-file cannot have rev1c as its ancestor.
220- inv = self.make_one_file_inventory(repo, 'rev3', ['rev1c'])
221- self.add_revision(repo, 'rev3', inv, ['rev1c', 'rev1a'])
222- self.versioned_root = repo.supports_rich_root()
223-
224- def repository_text_key_references(self):
225- result = {}
226- if self.versioned_root:
227- result.update({('TREE_ROOT', 'rev2'): True,
228- ('TREE_ROOT', 'rev3'): True})
229- result.update({('a-file-id', 'rev2'): True,
230- ('a-file-id', 'rev3'): True})
231- return result
232-
233- def repository_text_keys(self):
234- return {('a-file-id', 'rev2'):[NULL_REVISION],
235- ('a-file-id', 'rev3'):[NULL_REVISION]}
236-
237- def versioned_repository_text_keys(self):
238- return {('TREE_ROOT', 'rev2'):[NULL_REVISION],
239- ('TREE_ROOT', 'rev3'):[NULL_REVISION]}
240-
241-
242-class FileParentsNotReferencedByAnyInventoryScenario(BrokenRepoScenario):
243- """A scenario where a repository with file 'a-file' which has extra
244- per-file versions that are not referenced by any inventory (even though
245- they have the same ID as actual revisions). The inventory of 'rev2'
246- references 'rev1a' of 'a-file', but there is a 'rev2' of 'some-file' stored
247- and erroneously referenced by later per-file versions (revisions 'rev4' and
248- 'rev5').
249-
250- Reconcile should remove the file parents that are not referenced by any
251- inventory.
252- """
253-
254- def all_versions_after_reconcile(self):
255- return ('rev1a', 'rev2c', 'rev4', 'rev5')
256-
257- def populated_parents(self):
258- return [
259- (('rev1a',), 'rev2'),
260- (('rev1a',), 'rev2b'),
261- (('rev2',), 'rev3'),
262- (('rev2',), 'rev4'),
263- (('rev2', 'rev2c'), 'rev5')]
264-
265- def corrected_parents(self):
266- return (
267- # rev2 and rev2b have been removed.
268- (None, 'rev2'),
269- (None, 'rev2b'),
270- # rev3's accessible parent inventories all have rev1a as the last
271- # modifier.
272- (('rev1a',), 'rev3'),
273- # rev1a features in both rev4's parents but should only appear once
274- # in the result
275- (('rev1a',), 'rev4'),
276- # rev2c is the head of rev1a and rev2c, the inventory provided
277- # per-file last-modified revisions.
278- (('rev2c',), 'rev5'))
279-
280- def check_regexes(self, repo):
281- if repo.supports_rich_root():
282- # TREE_ROOT will be wrong; but we're not testing it. so just adjust
283- # the expected count of errors.
284- count = 9
285- else:
286- count = 3
287- return [
288- # will be gc'd
289- r"unreferenced version: {rev2} in a-file-id",
290- r"unreferenced version: {rev2b} in a-file-id",
291- # will be corrected
292- r"a-file-id version rev3 has parents \('rev2',\) "
293- r"but should have \('rev1a',\)",
294- r"a-file-id version rev5 has parents \('rev2', 'rev2c'\) "
295- r"but should have \('rev2c',\)",
296- r"a-file-id version rev4 has parents \('rev2',\) "
297- r"but should have \('rev1a',\)",
298- "%d inconsistent parents" % count,
299- ]
300-
301- def populate_repository(self, repo):
302- # make rev1a: A well-formed revision, containing 'a-file'
303- inv = self.make_one_file_inventory(
304- repo, 'rev1a', [], root_revision='rev1a')
305- self.add_revision(repo, 'rev1a', inv, [])
306-
307- # make rev2, with a-file.
308- # a-file is unmodified from rev1a, and an unreferenced rev2 file
309- # version is present in the repository.
310- self.make_one_file_inventory(
311- repo, 'rev2', ['rev1a'], inv_revision='rev1a')
312- self.add_revision(repo, 'rev2', inv, ['rev1a'])
313-
314- # make rev3 with a-file
315- # a-file has 'rev2' as its ancestor, but the revision in 'rev2' was
316- # rev1a so this is inconsistent with rev2's inventory - it should
317- # be rev1a, and at the revision level 1c is not present - it is a
318- # ghost, so only the details from rev1a are available for
319- # determining whether a delta is acceptable, or a full is needed,
320- # and what the correct parents are.
321- inv = self.make_one_file_inventory(repo, 'rev3', ['rev2'])
322- self.add_revision(repo, 'rev3', inv, ['rev1c', 'rev1a'])
323-
324- # In rev2b, the true last-modifying-revision of a-file is rev1a,
325- # inherited from rev2, but there is a version rev2b of the file, which
326- # reconcile could remove, leaving no rev2b. Most importantly,
327- # revisions descending from rev2b should not have per-file parents of
328- # a-file-rev2b.
329- # ??? This is to test deduplication in fixing rev4
330- inv = self.make_one_file_inventory(
331- repo, 'rev2b', ['rev1a'], inv_revision='rev1a')
332- self.add_revision(repo, 'rev2b', inv, ['rev1a'])
333-
334- # rev4 is for testing that when the last modified of a file in
335- # multiple parent revisions is the same, that it only appears once
336- # in the generated per file parents list: rev2 and rev2b both
337- # descend from 1a and do not change the file a-file, so there should
338- # be no version of a-file 'rev2' or 'rev2b', but rev4 does change
339- # a-file, and is a merge of rev2 and rev2b, so it should end up with
340- # a parent of just rev1a - the starting file parents list is simply
341- # completely wrong.
342- inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
343- self.add_revision(repo, 'rev4', inv, ['rev2', 'rev2b'])
344-
345- # rev2c changes a-file from rev1a, so the version it of a-file it
346- # introduces is a head revision when rev5 is checked.
347- inv = self.make_one_file_inventory(repo, 'rev2c', ['rev1a'])
348- self.add_revision(repo, 'rev2c', inv, ['rev1a'])
349-
350- # rev5 descends from rev2 and rev2c; as rev2 does not alter a-file,
351- # but rev2c does, this should use rev2c as the parent for the per
352- # file history, even though more than one per-file parent is
353- # available, because we use the heads of the revision parents for
354- # the inventory modification revisions of the file to determine the
355- # parents for the per file graph.
356- inv = self.make_one_file_inventory(repo, 'rev5', ['rev2', 'rev2c'])
357- self.add_revision(repo, 'rev5', inv, ['rev2', 'rev2c'])
358- self.versioned_root = repo.supports_rich_root()
359-
360- def repository_text_key_references(self):
361- result = {}
362- if self.versioned_root:
363- result.update({('TREE_ROOT', 'rev1a'): True,
364- ('TREE_ROOT', 'rev2'): True,
365- ('TREE_ROOT', 'rev2b'): True,
366- ('TREE_ROOT', 'rev2c'): True,
367- ('TREE_ROOT', 'rev3'): True,
368- ('TREE_ROOT', 'rev4'): True,
369- ('TREE_ROOT', 'rev5'): True})
370- result.update({('a-file-id', 'rev1a'): True,
371- ('a-file-id', 'rev2c'): True,
372- ('a-file-id', 'rev3'): True,
373- ('a-file-id', 'rev4'): True,
374- ('a-file-id', 'rev5'): True})
375- return result
376-
377- def repository_text_keys(self):
378- return {('a-file-id', 'rev1a'): [NULL_REVISION],
379- ('a-file-id', 'rev2c'): [('a-file-id', 'rev1a')],
380- ('a-file-id', 'rev3'): [('a-file-id', 'rev1a')],
381- ('a-file-id', 'rev4'): [('a-file-id', 'rev1a')],
382- ('a-file-id', 'rev5'): [('a-file-id', 'rev2c')]}
383-
384- def versioned_repository_text_keys(self):
385- return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
386- ('TREE_ROOT', 'rev2'): [('TREE_ROOT', 'rev1a')],
387- ('TREE_ROOT', 'rev2b'): [('TREE_ROOT', 'rev1a')],
388- ('TREE_ROOT', 'rev2c'): [('TREE_ROOT', 'rev1a')],
389- ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev1a')],
390- ('TREE_ROOT', 'rev4'):
391- [('TREE_ROOT', 'rev2'), ('TREE_ROOT', 'rev2b')],
392- ('TREE_ROOT', 'rev5'):
393- [('TREE_ROOT', 'rev2'), ('TREE_ROOT', 'rev2c')]}
394-
395-
396-class UnreferencedFileParentsFromNoOpMergeScenario(BrokenRepoScenario):
397- """
398- rev1a and rev1b with identical contents
399- rev2 revision has parents of [rev1a, rev1b]
400- There is a a-file:rev2 file version, not referenced by the inventory.
401- """
402-
403- def all_versions_after_reconcile(self):
404- return ('rev1a', 'rev1b', 'rev2', 'rev4')
405-
406- def populated_parents(self):
407- return (
408- ((), 'rev1a'),
409- ((), 'rev1b'),
410- (('rev1a', 'rev1b'), 'rev2'),
411- (None, 'rev3'),
412- (('rev2',), 'rev4'),
413- )
414-
415- def corrected_parents(self):
416- return (
417- ((), 'rev1a'),
418- ((), 'rev1b'),
419- ((), 'rev2'),
420- (None, 'rev3'),
421- (('rev2',), 'rev4'),
422- )
423-
424- def corrected_fulltexts(self):
425- return ['rev2']
426-
427- def check_regexes(self, repo):
428- return []
429-
430- def populate_repository(self, repo):
431- # make rev1a: A well-formed revision, containing 'a-file'
432- inv1a = self.make_one_file_inventory(
433- repo, 'rev1a', [], root_revision='rev1a')
434- self.add_revision(repo, 'rev1a', inv1a, [])
435-
436- # make rev1b: A well-formed revision, containing 'a-file'
437- # rev1b of a-file has the exact same contents as rev1a.
438- file_contents = repo.revision_tree('rev1a').get_file_text('a-file-id')
439- inv = self.make_one_file_inventory(
440- repo, 'rev1b', [], root_revision='rev1b',
441- file_contents=file_contents)
442- self.add_revision(repo, 'rev1b', inv, [])
443-
444- # make rev2, a merge of rev1a and rev1b, with a-file.
445- # a-file is unmodified from rev1a and rev1b, but a new version is
446- # wrongly present anyway.
447- inv = self.make_one_file_inventory(
448- repo, 'rev2', ['rev1a', 'rev1b'], inv_revision='rev1a',
449- file_contents=file_contents)
450- self.add_revision(repo, 'rev2', inv, ['rev1a', 'rev1b'])
451-
452- # rev3: a-file unchanged from rev2, but wrongly referencing rev2 of the
453- # file in its inventory.
454- inv = self.make_one_file_inventory(
455- repo, 'rev3', ['rev2'], inv_revision='rev2',
456- file_contents=file_contents, make_file_version=False)
457- self.add_revision(repo, 'rev3', inv, ['rev2'])
458-
459- # rev4: a modification of a-file on top of rev3.
460- inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
461- self.add_revision(repo, 'rev4', inv, ['rev3'])
462- self.versioned_root = repo.supports_rich_root()
463-
464- def repository_text_key_references(self):
465- result = {}
466- if self.versioned_root:
467- result.update({('TREE_ROOT', 'rev1a'): True,
468- ('TREE_ROOT', 'rev1b'): True,
469- ('TREE_ROOT', 'rev2'): True,
470- ('TREE_ROOT', 'rev3'): True,
471- ('TREE_ROOT', 'rev4'): True})
472- result.update({('a-file-id', 'rev1a'): True,
473- ('a-file-id', 'rev1b'): True,
474- ('a-file-id', 'rev2'): False,
475- ('a-file-id', 'rev4'): True})
476- return result
477-
478- def repository_text_keys(self):
479- return {('a-file-id', 'rev1a'): [NULL_REVISION],
480- ('a-file-id', 'rev1b'): [NULL_REVISION],
481- ('a-file-id', 'rev2'): [NULL_REVISION],
482- ('a-file-id', 'rev4'): [('a-file-id', 'rev2')]}
483-
484- def versioned_repository_text_keys(self):
485- return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
486- ('TREE_ROOT', 'rev1b'): [NULL_REVISION],
487- ('TREE_ROOT', 'rev2'):
488- [('TREE_ROOT', 'rev1a'), ('TREE_ROOT', 'rev1b')],
489- ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev2')],
490- ('TREE_ROOT', 'rev4'): [('TREE_ROOT', 'rev3')]}
491-
492-
493-class TooManyParentsScenario(BrokenRepoScenario):
494- """A scenario where 'broken-revision' of 'a-file' claims to have parents
495- ['good-parent', 'bad-parent']. However 'bad-parent' is in the ancestry of
496- 'good-parent', so the correct parent list for that file version are is just
497- ['good-parent'].
498- """
499-
500- def all_versions_after_reconcile(self):
501- return ('bad-parent', 'good-parent', 'broken-revision')
502-
503- def populated_parents(self):
504- return (
505- ((), 'bad-parent'),
506- (('bad-parent',), 'good-parent'),
507- (('good-parent', 'bad-parent'), 'broken-revision'))
508-
509- def corrected_parents(self):
510- return (
511- ((), 'bad-parent'),
512- (('bad-parent',), 'good-parent'),
513- (('good-parent',), 'broken-revision'))
514-
515- def check_regexes(self, repo):
516- if repo.supports_rich_root():
517- # TREE_ROOT will be wrong; but we're not testing it. so just adjust
518- # the expected count of errors.
519- count = 3
520- else:
521- count = 1
522- return (
523- ' %d inconsistent parents' % count,
524- (r" \* a-file-id version broken-revision has parents "
525- r"\('good-parent', 'bad-parent'\) but "
526- r"should have \('good-parent',\)"))
527-
528- def populate_repository(self, repo):
529- inv = self.make_one_file_inventory(
530- repo, 'bad-parent', (), root_revision='bad-parent')
531- self.add_revision(repo, 'bad-parent', inv, ())
532-
533- inv = self.make_one_file_inventory(
534- repo, 'good-parent', ('bad-parent',))
535- self.add_revision(repo, 'good-parent', inv, ('bad-parent',))
536-
537- inv = self.make_one_file_inventory(
538- repo, 'broken-revision', ('good-parent', 'bad-parent'))
539- self.add_revision(repo, 'broken-revision', inv, ('good-parent',))
540- self.versioned_root = repo.supports_rich_root()
541-
542- def repository_text_key_references(self):
543- result = {}
544- if self.versioned_root:
545- result.update({('TREE_ROOT', 'bad-parent'): True,
546- ('TREE_ROOT', 'broken-revision'): True,
547- ('TREE_ROOT', 'good-parent'): True})
548- result.update({('a-file-id', 'bad-parent'): True,
549- ('a-file-id', 'broken-revision'): True,
550- ('a-file-id', 'good-parent'): True})
551- return result
552-
553- def repository_text_keys(self):
554- return {('a-file-id', 'bad-parent'): [NULL_REVISION],
555- ('a-file-id', 'broken-revision'):
556- [('a-file-id', 'good-parent')],
557- ('a-file-id', 'good-parent'): [('a-file-id', 'bad-parent')]}
558-
559- def versioned_repository_text_keys(self):
560- return {('TREE_ROOT', 'bad-parent'): [NULL_REVISION],
561- ('TREE_ROOT', 'broken-revision'):
562- [('TREE_ROOT', 'good-parent')],
563- ('TREE_ROOT', 'good-parent'): [('TREE_ROOT', 'bad-parent')]}
564-
565-
566-class ClaimedFileParentDidNotModifyFileScenario(BrokenRepoScenario):
567- """A scenario where the file parent is the same as the revision parent, but
568- should not be because that revision did not modify the file.
569-
570- Specifically, the parent revision of 'current' is
571- 'modified-something-else', which does not modify 'a-file', but the
572- 'current' version of 'a-file' erroneously claims that
573- 'modified-something-else' is the parent file version.
574- """
575-
576- def all_versions_after_reconcile(self):
577- return ('basis', 'current')
578-
579- def populated_parents(self):
580- return (
581- ((), 'basis'),
582- (('basis',), 'modified-something-else'),
583- (('modified-something-else',), 'current'))
584-
585- def corrected_parents(self):
586- return (
587- ((), 'basis'),
588- (None, 'modified-something-else'),
589- (('basis',), 'current'))
590-
591- def check_regexes(self, repo):
592- if repo.supports_rich_root():
593- # TREE_ROOT will be wrong; but we're not testing it. so just adjust
594- # the expected count of errors.
595- count = 3
596- else:
597- count = 1
598- return (
599- "%d inconsistent parents" % count,
600- r"\* a-file-id version current has parents "
601- r"\('modified-something-else',\) but should have \('basis',\)",
602- )
603-
604- def populate_repository(self, repo):
605- inv = self.make_one_file_inventory(repo, 'basis', ())
606- self.add_revision(repo, 'basis', inv, ())
607-
608- # 'modified-something-else' is a correctly recorded revision, but it
609- # does not modify the file we are looking at, so the inventory for that
610- # file in this revision points to 'basis'.
611- inv = self.make_one_file_inventory(
612- repo, 'modified-something-else', ('basis',), inv_revision='basis')
613- self.add_revision(repo, 'modified-something-else', inv, ('basis',))
614-
615- # The 'current' revision has 'modified-something-else' as its parent,
616- # but the 'current' version of 'a-file' should have 'basis' as its
617- # parent.
618- inv = self.make_one_file_inventory(
619- repo, 'current', ('modified-something-else',))
620- self.add_revision(repo, 'current', inv, ('modified-something-else',))
621- self.versioned_root = repo.supports_rich_root()
622-
623- def repository_text_key_references(self):
624- result = {}
625- if self.versioned_root:
626- result.update({('TREE_ROOT', 'basis'): True,
627- ('TREE_ROOT', 'current'): True,
628- ('TREE_ROOT', 'modified-something-else'): True})
629- result.update({('a-file-id', 'basis'): True,
630- ('a-file-id', 'current'): True})
631- return result
632-
633- def repository_text_keys(self):
634- return {('a-file-id', 'basis'): [NULL_REVISION],
635- ('a-file-id', 'current'): [('a-file-id', 'basis')]}
636-
637- def versioned_repository_text_keys(self):
638- return {('TREE_ROOT', 'basis'): ['null:'],
639- ('TREE_ROOT', 'current'):
640- [('TREE_ROOT', 'modified-something-else')],
641- ('TREE_ROOT', 'modified-something-else'):
642- [('TREE_ROOT', 'basis')]}
643-
644-
645-class IncorrectlyOrderedParentsScenario(BrokenRepoScenario):
646- """A scenario where the set parents of a version of a file are correct, but
647- the order of those parents is incorrect.
648-
649- This defines a 'broken-revision-1-2' and a 'broken-revision-2-1' which both
650- have their file version parents reversed compared to the revision parents,
651- which is invalid. (We use two revisions with opposite orderings of the
652- same parents to make sure that accidentally relying on dictionary/set
653- ordering cannot make the test pass; the assumption is that while dict/set
654- iteration order is arbitrary, it is also consistent within a single test).
655- """
656-
657- def all_versions_after_reconcile(self):
658- return ['parent-1', 'parent-2', 'broken-revision-1-2',
659- 'broken-revision-2-1']
660-
661- def populated_parents(self):
662- return (
663- ((), 'parent-1'),
664- ((), 'parent-2'),
665- (('parent-2', 'parent-1'), 'broken-revision-1-2'),
666- (('parent-1', 'parent-2'), 'broken-revision-2-1'))
667-
668- def corrected_parents(self):
669- return (
670- ((), 'parent-1'),
671- ((), 'parent-2'),
672- (('parent-1', 'parent-2'), 'broken-revision-1-2'),
673- (('parent-2', 'parent-1'), 'broken-revision-2-1'))
674-
675- def check_regexes(self, repo):
676- if repo.supports_rich_root():
677- # TREE_ROOT will be wrong; but we're not testing it. so just adjust
678- # the expected count of errors.
679- count = 4
680- else:
681- count = 2
682- return (
683- "%d inconsistent parents" % count,
684- r"\* a-file-id version broken-revision-1-2 has parents "
685- r"\('parent-2', 'parent-1'\) but should have "
686- r"\('parent-1', 'parent-2'\)",
687- r"\* a-file-id version broken-revision-2-1 has parents "
688- r"\('parent-1', 'parent-2'\) but should have "
689- r"\('parent-2', 'parent-1'\)")
690-
691- def populate_repository(self, repo):
692- inv = self.make_one_file_inventory(repo, 'parent-1', [])
693- self.add_revision(repo, 'parent-1', inv, [])
694-
695- inv = self.make_one_file_inventory(repo, 'parent-2', [])
696- self.add_revision(repo, 'parent-2', inv, [])
697-
698- inv = self.make_one_file_inventory(
699- repo, 'broken-revision-1-2', ['parent-2', 'parent-1'])
700- self.add_revision(
701- repo, 'broken-revision-1-2', inv, ['parent-1', 'parent-2'])
702-
703- inv = self.make_one_file_inventory(
704- repo, 'broken-revision-2-1', ['parent-1', 'parent-2'])
705- self.add_revision(
706- repo, 'broken-revision-2-1', inv, ['parent-2', 'parent-1'])
707- self.versioned_root = repo.supports_rich_root()
708-
709- def repository_text_key_references(self):
710- result = {}
711- if self.versioned_root:
712- result.update({('TREE_ROOT', 'broken-revision-1-2'): True,
713- ('TREE_ROOT', 'broken-revision-2-1'): True,
714- ('TREE_ROOT', 'parent-1'): True,
715- ('TREE_ROOT', 'parent-2'): True})
716- result.update({('a-file-id', 'broken-revision-1-2'): True,
717- ('a-file-id', 'broken-revision-2-1'): True,
718- ('a-file-id', 'parent-1'): True,
719- ('a-file-id', 'parent-2'): True})
720- return result
721-
722- def repository_text_keys(self):
723- return {('a-file-id', 'broken-revision-1-2'):
724- [('a-file-id', 'parent-1'), ('a-file-id', 'parent-2')],
725- ('a-file-id', 'broken-revision-2-1'):
726- [('a-file-id', 'parent-2'), ('a-file-id', 'parent-1')],
727- ('a-file-id', 'parent-1'): [NULL_REVISION],
728- ('a-file-id', 'parent-2'): [NULL_REVISION]}
729-
730- def versioned_repository_text_keys(self):
731- return {('TREE_ROOT', 'broken-revision-1-2'):
732- [('TREE_ROOT', 'parent-1'), ('TREE_ROOT', 'parent-2')],
733- ('TREE_ROOT', 'broken-revision-2-1'):
734- [('TREE_ROOT', 'parent-2'), ('TREE_ROOT', 'parent-1')],
735- ('TREE_ROOT', 'parent-1'): [NULL_REVISION],
736- ('TREE_ROOT', 'parent-2'): [NULL_REVISION]}
737-
738-
739-all_broken_scenario_classes = [
740- UndamagedRepositoryScenario,
741- FileParentIsNotInRevisionAncestryScenario,
742- FileParentHasInaccessibleInventoryScenario,
743- FileParentsNotReferencedByAnyInventoryScenario,
744- TooManyParentsScenario,
745- ClaimedFileParentDidNotModifyFileScenario,
746- IncorrectlyOrderedParentsScenario,
747- UnreferencedFileParentsFromNoOpMergeScenario,
748- ]
749-
750-
751 def load_tests(standard_tests, module, loader):
752 prefix = 'bzrlib.tests.per_repository.'
753 test_repository_modules = [
754@@ -846,7 +113,6 @@
755 'test_add_inventory_by_delta',
756 'test_break_lock',
757 'test_check',
758- # test_check_reconcile is intentionally omitted, see below.
759 'test_commit_builder',
760 'test_fetch',
761 'test_fileid_involved',
762@@ -870,14 +136,4 @@
763 submod_tests = loader.loadTestsFromModuleNames(
764 [prefix + module_name for module_name in test_repository_modules])
765 format_scenarios = all_repository_format_scenarios()
766- multiply_tests(submod_tests, format_scenarios, standard_tests)
767-
768- # test_check_reconcile needs to be parameterized by format *and* by broken
769- # repository scenario.
770- broken_scenarios = [(s.__name__, {'scenario_class': s})
771- for s in all_broken_scenario_classes]
772- broken_scenarios_for_all_formats = multiply_scenarios(
773- format_scenarios, broken_scenarios)
774- return multiply_tests(
775- loader.loadTestsFromModuleNames([prefix + 'test_check_reconcile']),
776- broken_scenarios_for_all_formats, standard_tests)
777+ return multiply_tests(submod_tests, format_scenarios, standard_tests)
778
779=== modified file 'bzrlib/tests/per_repository_vf/__init__.py'
780--- bzrlib/tests/per_repository_vf/__init__.py 2011-03-08 20:43:14 +0000
781+++ bzrlib/tests/per_repository_vf/__init__.py 2011-03-08 20:43:14 +0000
782@@ -37,6 +37,7 @@
783
784 def load_tests(basic_tests, module, loader):
785 testmod_names = [
786+ 'test_check_reconcile',
787 'test_repository',
788 ]
789 basic_tests.addTest(loader.loadTestsFromModuleNames(
790
791=== renamed file 'bzrlib/tests/per_repository/test_check_reconcile.py' => 'bzrlib/tests/per_repository_vf/test_check_reconcile.py'
792--- bzrlib/tests/per_repository/test_check_reconcile.py 2010-02-17 17:11:16 +0000
793+++ bzrlib/tests/per_repository_vf/test_check_reconcile.py 2011-03-08 20:43:14 +0000
794@@ -21,15 +21,773 @@
795
796 from bzrlib import osutils
797
798-from bzrlib.inventory import Inventory, InventoryFile
799-from bzrlib.revision import Revision
800-from bzrlib.tests import TestNotApplicable
801-from bzrlib.tests.per_repository import TestCaseWithRepository
802+from bzrlib.inventory import (
803+ Inventory,
804+ InventoryFile,
805+ )
806+from bzrlib.revision import (
807+ NULL_REVISION,
808+ Revision,
809+ )
810+from bzrlib.tests import (
811+ TestNotApplicable,
812+ multiply_scenarios,
813+ )
814+from bzrlib.tests.per_repository_vf import (
815+ TestCaseWithRepository,
816+ all_repository_vf_format_scenarios,
817+ )
818+from bzrlib.tests.scenarios import load_tests_apply_scenarios
819+
820+
821+load_tests = load_tests_apply_scenarios
822+
823+
824+class BrokenRepoScenario(object):
825+ """Base class for defining scenarios for testing check and reconcile.
826+
827+ A subclass needs to define the following methods:
828+ :populate_repository: a method to use to populate a repository with
829+ sample revisions, inventories and file versions.
830+ :all_versions_after_reconcile: all the versions in repository after
831+ reconcile. run_test verifies that the text of each of these
832+ versions of the file is unchanged by the reconcile.
833+ :populated_parents: a list of (parents list, revision). Each version
834+ of the file is verified to have the given parents before running
835+ the reconcile. i.e. this is used to assert that the repo from the
836+ factory is what we expect.
837+ :corrected_parents: a list of (parents list, revision). Each version
838+ of the file is verified to have the given parents after the
839+ reconcile. i.e. this is used to assert that reconcile made the
840+ changes we expect it to make.
841+
842+ A subclass may define the following optional method as well:
843+ :corrected_fulltexts: a list of file versions that should be stored as
844+ fulltexts (not deltas) after reconcile. run_test will verify that
845+ this occurs.
846+ """
847+
848+ def __init__(self, test_case):
849+ self.test_case = test_case
850+
851+ def make_one_file_inventory(self, repo, revision, parents,
852+ inv_revision=None, root_revision=None,
853+ file_contents=None, make_file_version=True):
854+ return self.test_case.make_one_file_inventory(
855+ repo, revision, parents, inv_revision=inv_revision,
856+ root_revision=root_revision, file_contents=file_contents,
857+ make_file_version=make_file_version)
858+
859+ def add_revision(self, repo, revision_id, inv, parent_ids):
860+ return self.test_case.add_revision(repo, revision_id, inv, parent_ids)
861+
862+ def corrected_fulltexts(self):
863+ return []
864+
865+ def repository_text_key_index(self):
866+ result = {}
867+ if self.versioned_root:
868+ result.update(self.versioned_repository_text_keys())
869+ result.update(self.repository_text_keys())
870+ return result
871+
872+
873+class UndamagedRepositoryScenario(BrokenRepoScenario):
874+ """A scenario where the repository has no damage.
875+
876+ It has a single revision, 'rev1a', with a single file.
877+ """
878+
879+ def all_versions_after_reconcile(self):
880+ return ('rev1a', )
881+
882+ def populated_parents(self):
883+ return (((), 'rev1a'), )
884+
885+ def corrected_parents(self):
886+ # Same as the populated parents, because there was nothing wrong.
887+ return self.populated_parents()
888+
889+ def check_regexes(self, repo):
890+ return ["0 unreferenced text versions"]
891+
892+ def populate_repository(self, repo):
893+ # make rev1a: A well-formed revision, containing 'a-file'
894+ inv = self.make_one_file_inventory(
895+ repo, 'rev1a', [], root_revision='rev1a')
896+ self.add_revision(repo, 'rev1a', inv, [])
897+ self.versioned_root = repo.supports_rich_root()
898+
899+ def repository_text_key_references(self):
900+ result = {}
901+ if self.versioned_root:
902+ result.update({('TREE_ROOT', 'rev1a'): True})
903+ result.update({('a-file-id', 'rev1a'): True})
904+ return result
905+
906+ def repository_text_keys(self):
907+ return {('a-file-id', 'rev1a'):[NULL_REVISION]}
908+
909+ def versioned_repository_text_keys(self):
910+ return {('TREE_ROOT', 'rev1a'):[NULL_REVISION]}
911+
912+
913+class FileParentIsNotInRevisionAncestryScenario(BrokenRepoScenario):
914+ """A scenario where a revision 'rev2' has 'a-file' with a
915+ parent 'rev1b' that is not in the revision ancestry.
916+
917+ Reconcile should remove 'rev1b' from the parents list of 'a-file' in
918+ 'rev2', preserving 'rev1a' as a parent.
919+ """
920+
921+ def all_versions_after_reconcile(self):
922+ return ('rev1a', 'rev2')
923+
924+ def populated_parents(self):
925+ return (
926+ ((), 'rev1a'),
927+ ((), 'rev1b'), # Will be gc'd
928+ (('rev1a', 'rev1b'), 'rev2')) # Will have parents trimmed
929+
930+ def corrected_parents(self):
931+ return (
932+ ((), 'rev1a'),
933+ (None, 'rev1b'),
934+ (('rev1a',), 'rev2'))
935+
936+ def check_regexes(self, repo):
937+ return [r"\* a-file-id version rev2 has parents \('rev1a', 'rev1b'\) "
938+ r"but should have \('rev1a',\)",
939+ "1 unreferenced text versions",
940+ ]
941+
942+ def populate_repository(self, repo):
943+ # make rev1a: A well-formed revision, containing 'a-file'
944+ inv = self.make_one_file_inventory(
945+ repo, 'rev1a', [], root_revision='rev1a')
946+ self.add_revision(repo, 'rev1a', inv, [])
947+
948+ # make rev1b, which has no Revision, but has an Inventory, and
949+ # a-file
950+ inv = self.make_one_file_inventory(
951+ repo, 'rev1b', [], root_revision='rev1b')
952+ repo.add_inventory('rev1b', inv, [])
953+
954+ # make rev2, with a-file.
955+ # a-file has 'rev1b' as an ancestor, even though this is not
956+ # mentioned by 'rev1a', making it an unreferenced ancestor
957+ inv = self.make_one_file_inventory(
958+ repo, 'rev2', ['rev1a', 'rev1b'])
959+ self.add_revision(repo, 'rev2', inv, ['rev1a'])
960+ self.versioned_root = repo.supports_rich_root()
961+
962+ def repository_text_key_references(self):
963+ result = {}
964+ if self.versioned_root:
965+ result.update({('TREE_ROOT', 'rev1a'): True,
966+ ('TREE_ROOT', 'rev2'): True})
967+ result.update({('a-file-id', 'rev1a'): True,
968+ ('a-file-id', 'rev2'): True})
969+ return result
970+
971+ def repository_text_keys(self):
972+ return {('a-file-id', 'rev1a'):[NULL_REVISION],
973+ ('a-file-id', 'rev2'):[('a-file-id', 'rev1a')]}
974+
975+ def versioned_repository_text_keys(self):
976+ return {('TREE_ROOT', 'rev1a'):[NULL_REVISION],
977+ ('TREE_ROOT', 'rev2'):[('TREE_ROOT', 'rev1a')]}
978+
979+
980+class FileParentHasInaccessibleInventoryScenario(BrokenRepoScenario):
981+ """A scenario where a revision 'rev3' containing 'a-file' modified in
982+ 'rev3', and with a parent which is in the revision ancestory, but whose
983+ inventory cannot be accessed at all.
984+
985+ Reconcile should remove the file version parent whose inventory is
986+ inaccessbile (i.e. remove 'rev1c' from the parents of a-file's rev3).
987+ """
988+
989+ def all_versions_after_reconcile(self):
990+ return ('rev2', 'rev3')
991+
992+ def populated_parents(self):
993+ return (
994+ ((), 'rev2'),
995+ (('rev1c',), 'rev3'))
996+
997+ def corrected_parents(self):
998+ return (
999+ ((), 'rev2'),
1000+ ((), 'rev3'))
1001+
1002+ def check_regexes(self, repo):
1003+ return [r"\* a-file-id version rev3 has parents "
1004+ r"\('rev1c',\) but should have \(\)",
1005+ ]
1006+
1007+ def populate_repository(self, repo):
1008+ # make rev2, with a-file
1009+ # a-file is sane
1010+ inv = self.make_one_file_inventory(repo, 'rev2', [])
1011+ self.add_revision(repo, 'rev2', inv, [])
1012+
1013+ # make ghost revision rev1c, with a version of a-file present so
1014+ # that we generate a knit delta against this version. In real life
1015+ # the ghost might never have been present or rev3 might have been
1016+ # generated against a revision that was present at the time. So
1017+ # currently we have the full history of a-file present even though
1018+ # the inventory and revision objects are not.
1019+ self.make_one_file_inventory(repo, 'rev1c', [])
1020+
1021+ # make rev3 with a-file
1022+ # a-file refers to 'rev1c', which is a ghost in this repository, so
1023+ # a-file cannot have rev1c as its ancestor.
1024+ inv = self.make_one_file_inventory(repo, 'rev3', ['rev1c'])
1025+ self.add_revision(repo, 'rev3', inv, ['rev1c', 'rev1a'])
1026+ self.versioned_root = repo.supports_rich_root()
1027+
1028+ def repository_text_key_references(self):
1029+ result = {}
1030+ if self.versioned_root:
1031+ result.update({('TREE_ROOT', 'rev2'): True,
1032+ ('TREE_ROOT', 'rev3'): True})
1033+ result.update({('a-file-id', 'rev2'): True,
1034+ ('a-file-id', 'rev3'): True})
1035+ return result
1036+
1037+ def repository_text_keys(self):
1038+ return {('a-file-id', 'rev2'):[NULL_REVISION],
1039+ ('a-file-id', 'rev3'):[NULL_REVISION]}
1040+
1041+ def versioned_repository_text_keys(self):
1042+ return {('TREE_ROOT', 'rev2'):[NULL_REVISION],
1043+ ('TREE_ROOT', 'rev3'):[NULL_REVISION]}
1044+
1045+
1046+class FileParentsNotReferencedByAnyInventoryScenario(BrokenRepoScenario):
1047+ """A scenario where a repository with file 'a-file' which has extra
1048+ per-file versions that are not referenced by any inventory (even though
1049+ they have the same ID as actual revisions). The inventory of 'rev2'
1050+ references 'rev1a' of 'a-file', but there is a 'rev2' of 'some-file' stored
1051+ and erroneously referenced by later per-file versions (revisions 'rev4' and
1052+ 'rev5').
1053+
1054+ Reconcile should remove the file parents that are not referenced by any
1055+ inventory.
1056+ """
1057+
1058+ def all_versions_after_reconcile(self):
1059+ return ('rev1a', 'rev2c', 'rev4', 'rev5')
1060+
1061+ def populated_parents(self):
1062+ return [
1063+ (('rev1a',), 'rev2'),
1064+ (('rev1a',), 'rev2b'),
1065+ (('rev2',), 'rev3'),
1066+ (('rev2',), 'rev4'),
1067+ (('rev2', 'rev2c'), 'rev5')]
1068+
1069+ def corrected_parents(self):
1070+ return (
1071+ # rev2 and rev2b have been removed.
1072+ (None, 'rev2'),
1073+ (None, 'rev2b'),
1074+ # rev3's accessible parent inventories all have rev1a as the last
1075+ # modifier.
1076+ (('rev1a',), 'rev3'),
1077+ # rev1a features in both rev4's parents but should only appear once
1078+ # in the result
1079+ (('rev1a',), 'rev4'),
1080+ # rev2c is the head of rev1a and rev2c, the inventory provided
1081+ # per-file last-modified revisions.
1082+ (('rev2c',), 'rev5'))
1083+
1084+ def check_regexes(self, repo):
1085+ if repo.supports_rich_root():
1086+ # TREE_ROOT will be wrong; but we're not testing it. so just adjust
1087+ # the expected count of errors.
1088+ count = 9
1089+ else:
1090+ count = 3
1091+ return [
1092+ # will be gc'd
1093+ r"unreferenced version: {rev2} in a-file-id",
1094+ r"unreferenced version: {rev2b} in a-file-id",
1095+ # will be corrected
1096+ r"a-file-id version rev3 has parents \('rev2',\) "
1097+ r"but should have \('rev1a',\)",
1098+ r"a-file-id version rev5 has parents \('rev2', 'rev2c'\) "
1099+ r"but should have \('rev2c',\)",
1100+ r"a-file-id version rev4 has parents \('rev2',\) "
1101+ r"but should have \('rev1a',\)",
1102+ "%d inconsistent parents" % count,
1103+ ]
1104+
1105+ def populate_repository(self, repo):
1106+ # make rev1a: A well-formed revision, containing 'a-file'
1107+ inv = self.make_one_file_inventory(
1108+ repo, 'rev1a', [], root_revision='rev1a')
1109+ self.add_revision(repo, 'rev1a', inv, [])
1110+
1111+ # make rev2, with a-file.
1112+ # a-file is unmodified from rev1a, and an unreferenced rev2 file
1113+ # version is present in the repository.
1114+ self.make_one_file_inventory(
1115+ repo, 'rev2', ['rev1a'], inv_revision='rev1a')
1116+ self.add_revision(repo, 'rev2', inv, ['rev1a'])
1117+
1118+ # make rev3 with a-file
1119+ # a-file has 'rev2' as its ancestor, but the revision in 'rev2' was
1120+ # rev1a so this is inconsistent with rev2's inventory - it should
1121+ # be rev1a, and at the revision level 1c is not present - it is a
1122+ # ghost, so only the details from rev1a are available for
1123+ # determining whether a delta is acceptable, or a full is needed,
1124+ # and what the correct parents are.
1125+ inv = self.make_one_file_inventory(repo, 'rev3', ['rev2'])
1126+ self.add_revision(repo, 'rev3', inv, ['rev1c', 'rev1a'])
1127+
1128+ # In rev2b, the true last-modifying-revision of a-file is rev1a,
1129+ # inherited from rev2, but there is a version rev2b of the file, which
1130+ # reconcile could remove, leaving no rev2b. Most importantly,
1131+ # revisions descending from rev2b should not have per-file parents of
1132+ # a-file-rev2b.
1133+ # ??? This is to test deduplication in fixing rev4
1134+ inv = self.make_one_file_inventory(
1135+ repo, 'rev2b', ['rev1a'], inv_revision='rev1a')
1136+ self.add_revision(repo, 'rev2b', inv, ['rev1a'])
1137+
1138+ # rev4 is for testing that when the last modified of a file in
1139+ # multiple parent revisions is the same, that it only appears once
1140+ # in the generated per file parents list: rev2 and rev2b both
1141+ # descend from 1a and do not change the file a-file, so there should
1142+ # be no version of a-file 'rev2' or 'rev2b', but rev4 does change
1143+ # a-file, and is a merge of rev2 and rev2b, so it should end up with
1144+ # a parent of just rev1a - the starting file parents list is simply
1145+ # completely wrong.
1146+ inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
1147+ self.add_revision(repo, 'rev4', inv, ['rev2', 'rev2b'])
1148+
1149+ # rev2c changes a-file from rev1a, so the version it of a-file it
1150+ # introduces is a head revision when rev5 is checked.
1151+ inv = self.make_one_file_inventory(repo, 'rev2c', ['rev1a'])
1152+ self.add_revision(repo, 'rev2c', inv, ['rev1a'])
1153+
1154+ # rev5 descends from rev2 and rev2c; as rev2 does not alter a-file,
1155+ # but rev2c does, this should use rev2c as the parent for the per
1156+ # file history, even though more than one per-file parent is
1157+ # available, because we use the heads of the revision parents for
1158+ # the inventory modification revisions of the file to determine the
1159+ # parents for the per file graph.
1160+ inv = self.make_one_file_inventory(repo, 'rev5', ['rev2', 'rev2c'])
1161+ self.add_revision(repo, 'rev5', inv, ['rev2', 'rev2c'])
1162+ self.versioned_root = repo.supports_rich_root()
1163+
1164+ def repository_text_key_references(self):
1165+ result = {}
1166+ if self.versioned_root:
1167+ result.update({('TREE_ROOT', 'rev1a'): True,
1168+ ('TREE_ROOT', 'rev2'): True,
1169+ ('TREE_ROOT', 'rev2b'): True,
1170+ ('TREE_ROOT', 'rev2c'): True,
1171+ ('TREE_ROOT', 'rev3'): True,
1172+ ('TREE_ROOT', 'rev4'): True,
1173+ ('TREE_ROOT', 'rev5'): True})
1174+ result.update({('a-file-id', 'rev1a'): True,
1175+ ('a-file-id', 'rev2c'): True,
1176+ ('a-file-id', 'rev3'): True,
1177+ ('a-file-id', 'rev4'): True,
1178+ ('a-file-id', 'rev5'): True})
1179+ return result
1180+
1181+ def repository_text_keys(self):
1182+ return {('a-file-id', 'rev1a'): [NULL_REVISION],
1183+ ('a-file-id', 'rev2c'): [('a-file-id', 'rev1a')],
1184+ ('a-file-id', 'rev3'): [('a-file-id', 'rev1a')],
1185+ ('a-file-id', 'rev4'): [('a-file-id', 'rev1a')],
1186+ ('a-file-id', 'rev5'): [('a-file-id', 'rev2c')]}
1187+
1188+ def versioned_repository_text_keys(self):
1189+ return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
1190+ ('TREE_ROOT', 'rev2'): [('TREE_ROOT', 'rev1a')],
1191+ ('TREE_ROOT', 'rev2b'): [('TREE_ROOT', 'rev1a')],
1192+ ('TREE_ROOT', 'rev2c'): [('TREE_ROOT', 'rev1a')],
1193+ ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev1a')],
1194+ ('TREE_ROOT', 'rev4'):
1195+ [('TREE_ROOT', 'rev2'), ('TREE_ROOT', 'rev2b')],
1196+ ('TREE_ROOT', 'rev5'):
1197+ [('TREE_ROOT', 'rev2'), ('TREE_ROOT', 'rev2c')]}
1198+
1199+
1200+class UnreferencedFileParentsFromNoOpMergeScenario(BrokenRepoScenario):
1201+ """
1202+ rev1a and rev1b with identical contents
1203+ rev2 revision has parents of [rev1a, rev1b]
1204+ There is a a-file:rev2 file version, not referenced by the inventory.
1205+ """
1206+
1207+ def all_versions_after_reconcile(self):
1208+ return ('rev1a', 'rev1b', 'rev2', 'rev4')
1209+
1210+ def populated_parents(self):
1211+ return (
1212+ ((), 'rev1a'),
1213+ ((), 'rev1b'),
1214+ (('rev1a', 'rev1b'), 'rev2'),
1215+ (None, 'rev3'),
1216+ (('rev2',), 'rev4'),
1217+ )
1218+
1219+ def corrected_parents(self):
1220+ return (
1221+ ((), 'rev1a'),
1222+ ((), 'rev1b'),
1223+ ((), 'rev2'),
1224+ (None, 'rev3'),
1225+ (('rev2',), 'rev4'),
1226+ )
1227+
1228+ def corrected_fulltexts(self):
1229+ return ['rev2']
1230+
1231+ def check_regexes(self, repo):
1232+ return []
1233+
1234+ def populate_repository(self, repo):
1235+ # make rev1a: A well-formed revision, containing 'a-file'
1236+ inv1a = self.make_one_file_inventory(
1237+ repo, 'rev1a', [], root_revision='rev1a')
1238+ self.add_revision(repo, 'rev1a', inv1a, [])
1239+
1240+ # make rev1b: A well-formed revision, containing 'a-file'
1241+ # rev1b of a-file has the exact same contents as rev1a.
1242+ file_contents = repo.revision_tree('rev1a').get_file_text('a-file-id')
1243+ inv = self.make_one_file_inventory(
1244+ repo, 'rev1b', [], root_revision='rev1b',
1245+ file_contents=file_contents)
1246+ self.add_revision(repo, 'rev1b', inv, [])
1247+
1248+ # make rev2, a merge of rev1a and rev1b, with a-file.
1249+ # a-file is unmodified from rev1a and rev1b, but a new version is
1250+ # wrongly present anyway.
1251+ inv = self.make_one_file_inventory(
1252+ repo, 'rev2', ['rev1a', 'rev1b'], inv_revision='rev1a',
1253+ file_contents=file_contents)
1254+ self.add_revision(repo, 'rev2', inv, ['rev1a', 'rev1b'])
1255+
1256+ # rev3: a-file unchanged from rev2, but wrongly referencing rev2 of the
1257+ # file in its inventory.
1258+ inv = self.make_one_file_inventory(
1259+ repo, 'rev3', ['rev2'], inv_revision='rev2',
1260+ file_contents=file_contents, make_file_version=False)
1261+ self.add_revision(repo, 'rev3', inv, ['rev2'])
1262+
1263+ # rev4: a modification of a-file on top of rev3.
1264+ inv = self.make_one_file_inventory(repo, 'rev4', ['rev2'])
1265+ self.add_revision(repo, 'rev4', inv, ['rev3'])
1266+ self.versioned_root = repo.supports_rich_root()
1267+
1268+ def repository_text_key_references(self):
1269+ result = {}
1270+ if self.versioned_root:
1271+ result.update({('TREE_ROOT', 'rev1a'): True,
1272+ ('TREE_ROOT', 'rev1b'): True,
1273+ ('TREE_ROOT', 'rev2'): True,
1274+ ('TREE_ROOT', 'rev3'): True,
1275+ ('TREE_ROOT', 'rev4'): True})
1276+ result.update({('a-file-id', 'rev1a'): True,
1277+ ('a-file-id', 'rev1b'): True,
1278+ ('a-file-id', 'rev2'): False,
1279+ ('a-file-id', 'rev4'): True})
1280+ return result
1281+
1282+ def repository_text_keys(self):
1283+ return {('a-file-id', 'rev1a'): [NULL_REVISION],
1284+ ('a-file-id', 'rev1b'): [NULL_REVISION],
1285+ ('a-file-id', 'rev2'): [NULL_REVISION],
1286+ ('a-file-id', 'rev4'): [('a-file-id', 'rev2')]}
1287+
1288+ def versioned_repository_text_keys(self):
1289+ return {('TREE_ROOT', 'rev1a'): [NULL_REVISION],
1290+ ('TREE_ROOT', 'rev1b'): [NULL_REVISION],
1291+ ('TREE_ROOT', 'rev2'):
1292+ [('TREE_ROOT', 'rev1a'), ('TREE_ROOT', 'rev1b')],
1293+ ('TREE_ROOT', 'rev3'): [('TREE_ROOT', 'rev2')],
1294+ ('TREE_ROOT', 'rev4'): [('TREE_ROOT', 'rev3')]}
1295+
1296+
1297+class TooManyParentsScenario(BrokenRepoScenario):
1298+ """A scenario where 'broken-revision' of 'a-file' claims to have parents
1299+ ['good-parent', 'bad-parent']. However 'bad-parent' is in the ancestry of
1300+ 'good-parent', so the correct parent list for that file version are is just
1301+ ['good-parent'].
1302+ """
1303+
1304+ def all_versions_after_reconcile(self):
1305+ return ('bad-parent', 'good-parent', 'broken-revision')
1306+
1307+ def populated_parents(self):
1308+ return (
1309+ ((), 'bad-parent'),
1310+ (('bad-parent',), 'good-parent'),
1311+ (('good-parent', 'bad-parent'), 'broken-revision'))
1312+
1313+ def corrected_parents(self):
1314+ return (
1315+ ((), 'bad-parent'),
1316+ (('bad-parent',), 'good-parent'),
1317+ (('good-parent',), 'broken-revision'))
1318+
1319+ def check_regexes(self, repo):
1320+ if repo.supports_rich_root():
1321+ # TREE_ROOT will be wrong; but we're not testing it. so just adjust
1322+ # the expected count of errors.
1323+ count = 3
1324+ else:
1325+ count = 1
1326+ return (
1327+ ' %d inconsistent parents' % count,
1328+ (r" \* a-file-id version broken-revision has parents "
1329+ r"\('good-parent', 'bad-parent'\) but "
1330+ r"should have \('good-parent',\)"))
1331+
1332+ def populate_repository(self, repo):
1333+ inv = self.make_one_file_inventory(
1334+ repo, 'bad-parent', (), root_revision='bad-parent')
1335+ self.add_revision(repo, 'bad-parent', inv, ())
1336+
1337+ inv = self.make_one_file_inventory(
1338+ repo, 'good-parent', ('bad-parent',))
1339+ self.add_revision(repo, 'good-parent', inv, ('bad-parent',))
1340+
1341+ inv = self.make_one_file_inventory(
1342+ repo, 'broken-revision', ('good-parent', 'bad-parent'))
1343+ self.add_revision(repo, 'broken-revision', inv, ('good-parent',))
1344+ self.versioned_root = repo.supports_rich_root()
1345+
1346+ def repository_text_key_references(self):
1347+ result = {}
1348+ if self.versioned_root:
1349+ result.update({('TREE_ROOT', 'bad-parent'): True,
1350+ ('TREE_ROOT', 'broken-revision'): True,
1351+ ('TREE_ROOT', 'good-parent'): True})
1352+ result.update({('a-file-id', 'bad-parent'): True,
1353+ ('a-file-id', 'broken-revision'): True,
1354+ ('a-file-id', 'good-parent'): True})
1355+ return result
1356+
1357+ def repository_text_keys(self):
1358+ return {('a-file-id', 'bad-parent'): [NULL_REVISION],
1359+ ('a-file-id', 'broken-revision'):
1360+ [('a-file-id', 'good-parent')],
1361+ ('a-file-id', 'good-parent'): [('a-file-id', 'bad-parent')]}
1362+
1363+ def versioned_repository_text_keys(self):
1364+ return {('TREE_ROOT', 'bad-parent'): [NULL_REVISION],
1365+ ('TREE_ROOT', 'broken-revision'):
1366+ [('TREE_ROOT', 'good-parent')],
1367+ ('TREE_ROOT', 'good-parent'): [('TREE_ROOT', 'bad-parent')]}
1368+
1369+
1370+class ClaimedFileParentDidNotModifyFileScenario(BrokenRepoScenario):
1371+ """A scenario where the file parent is the same as the revision parent, but
1372+ should not be because that revision did not modify the file.
1373+
1374+ Specifically, the parent revision of 'current' is
1375+ 'modified-something-else', which does not modify 'a-file', but the
1376+ 'current' version of 'a-file' erroneously claims that
1377+ 'modified-something-else' is the parent file version.
1378+ """
1379+
1380+ def all_versions_after_reconcile(self):
1381+ return ('basis', 'current')
1382+
1383+ def populated_parents(self):
1384+ return (
1385+ ((), 'basis'),
1386+ (('basis',), 'modified-something-else'),
1387+ (('modified-something-else',), 'current'))
1388+
1389+ def corrected_parents(self):
1390+ return (
1391+ ((), 'basis'),
1392+ (None, 'modified-something-else'),
1393+ (('basis',), 'current'))
1394+
1395+ def check_regexes(self, repo):
1396+ if repo.supports_rich_root():
1397+ # TREE_ROOT will be wrong; but we're not testing it. so just adjust
1398+ # the expected count of errors.
1399+ count = 3
1400+ else:
1401+ count = 1
1402+ return (
1403+ "%d inconsistent parents" % count,
1404+ r"\* a-file-id version current has parents "
1405+ r"\('modified-something-else',\) but should have \('basis',\)",
1406+ )
1407+
1408+ def populate_repository(self, repo):
1409+ inv = self.make_one_file_inventory(repo, 'basis', ())
1410+ self.add_revision(repo, 'basis', inv, ())
1411+
1412+ # 'modified-something-else' is a correctly recorded revision, but it
1413+ # does not modify the file we are looking at, so the inventory for that
1414+ # file in this revision points to 'basis'.
1415+ inv = self.make_one_file_inventory(
1416+ repo, 'modified-something-else', ('basis',), inv_revision='basis')
1417+ self.add_revision(repo, 'modified-something-else', inv, ('basis',))
1418+
1419+ # The 'current' revision has 'modified-something-else' as its parent,
1420+ # but the 'current' version of 'a-file' should have 'basis' as its
1421+ # parent.
1422+ inv = self.make_one_file_inventory(
1423+ repo, 'current', ('modified-something-else',))
1424+ self.add_revision(repo, 'current', inv, ('modified-something-else',))
1425+ self.versioned_root = repo.supports_rich_root()
1426+
1427+ def repository_text_key_references(self):
1428+ result = {}
1429+ if self.versioned_root:
1430+ result.update({('TREE_ROOT', 'basis'): True,
1431+ ('TREE_ROOT', 'current'): True,
1432+ ('TREE_ROOT', 'modified-something-else'): True})
1433+ result.update({('a-file-id', 'basis'): True,
1434+ ('a-file-id', 'current'): True})
1435+ return result
1436+
1437+ def repository_text_keys(self):
1438+ return {('a-file-id', 'basis'): [NULL_REVISION],
1439+ ('a-file-id', 'current'): [('a-file-id', 'basis')]}
1440+
1441+ def versioned_repository_text_keys(self):
1442+ return {('TREE_ROOT', 'basis'): ['null:'],
1443+ ('TREE_ROOT', 'current'):
1444+ [('TREE_ROOT', 'modified-something-else')],
1445+ ('TREE_ROOT', 'modified-something-else'):
1446+ [('TREE_ROOT', 'basis')]}
1447+
1448+
1449+class IncorrectlyOrderedParentsScenario(BrokenRepoScenario):
1450+ """A scenario where the set parents of a version of a file are correct, but
1451+ the order of those parents is incorrect.
1452+
1453+ This defines a 'broken-revision-1-2' and a 'broken-revision-2-1' which both
1454+ have their file version parents reversed compared to the revision parents,
1455+ which is invalid. (We use two revisions with opposite orderings of the
1456+ same parents to make sure that accidentally relying on dictionary/set
1457+ ordering cannot make the test pass; the assumption is that while dict/set
1458+ iteration order is arbitrary, it is also consistent within a single test).
1459+ """
1460+
1461+ def all_versions_after_reconcile(self):
1462+ return ['parent-1', 'parent-2', 'broken-revision-1-2',
1463+ 'broken-revision-2-1']
1464+
1465+ def populated_parents(self):
1466+ return (
1467+ ((), 'parent-1'),
1468+ ((), 'parent-2'),
1469+ (('parent-2', 'parent-1'), 'broken-revision-1-2'),
1470+ (('parent-1', 'parent-2'), 'broken-revision-2-1'))
1471+
1472+ def corrected_parents(self):
1473+ return (
1474+ ((), 'parent-1'),
1475+ ((), 'parent-2'),
1476+ (('parent-1', 'parent-2'), 'broken-revision-1-2'),
1477+ (('parent-2', 'parent-1'), 'broken-revision-2-1'))
1478+
1479+ def check_regexes(self, repo):
1480+ if repo.supports_rich_root():
1481+ # TREE_ROOT will be wrong; but we're not testing it. so just adjust
1482+ # the expected count of errors.
1483+ count = 4
1484+ else:
1485+ count = 2
1486+ return (
1487+ "%d inconsistent parents" % count,
1488+ r"\* a-file-id version broken-revision-1-2 has parents "
1489+ r"\('parent-2', 'parent-1'\) but should have "
1490+ r"\('parent-1', 'parent-2'\)",
1491+ r"\* a-file-id version broken-revision-2-1 has parents "
1492+ r"\('parent-1', 'parent-2'\) but should have "
1493+ r"\('parent-2', 'parent-1'\)")
1494+
1495+ def populate_repository(self, repo):
1496+ inv = self.make_one_file_inventory(repo, 'parent-1', [])
1497+ self.add_revision(repo, 'parent-1', inv, [])
1498+
1499+ inv = self.make_one_file_inventory(repo, 'parent-2', [])
1500+ self.add_revision(repo, 'parent-2', inv, [])
1501+
1502+ inv = self.make_one_file_inventory(
1503+ repo, 'broken-revision-1-2', ['parent-2', 'parent-1'])
1504+ self.add_revision(
1505+ repo, 'broken-revision-1-2', inv, ['parent-1', 'parent-2'])
1506+
1507+ inv = self.make_one_file_inventory(
1508+ repo, 'broken-revision-2-1', ['parent-1', 'parent-2'])
1509+ self.add_revision(
1510+ repo, 'broken-revision-2-1', inv, ['parent-2', 'parent-1'])
1511+ self.versioned_root = repo.supports_rich_root()
1512+
1513+ def repository_text_key_references(self):
1514+ result = {}
1515+ if self.versioned_root:
1516+ result.update({('TREE_ROOT', 'broken-revision-1-2'): True,
1517+ ('TREE_ROOT', 'broken-revision-2-1'): True,
1518+ ('TREE_ROOT', 'parent-1'): True,
1519+ ('TREE_ROOT', 'parent-2'): True})
1520+ result.update({('a-file-id', 'broken-revision-1-2'): True,
1521+ ('a-file-id', 'broken-revision-2-1'): True,
1522+ ('a-file-id', 'parent-1'): True,
1523+ ('a-file-id', 'parent-2'): True})
1524+ return result
1525+
1526+ def repository_text_keys(self):
1527+ return {('a-file-id', 'broken-revision-1-2'):
1528+ [('a-file-id', 'parent-1'), ('a-file-id', 'parent-2')],
1529+ ('a-file-id', 'broken-revision-2-1'):
1530+ [('a-file-id', 'parent-2'), ('a-file-id', 'parent-1')],
1531+ ('a-file-id', 'parent-1'): [NULL_REVISION],
1532+ ('a-file-id', 'parent-2'): [NULL_REVISION]}
1533+
1534+ def versioned_repository_text_keys(self):
1535+ return {('TREE_ROOT', 'broken-revision-1-2'):
1536+ [('TREE_ROOT', 'parent-1'), ('TREE_ROOT', 'parent-2')],
1537+ ('TREE_ROOT', 'broken-revision-2-1'):
1538+ [('TREE_ROOT', 'parent-2'), ('TREE_ROOT', 'parent-1')],
1539+ ('TREE_ROOT', 'parent-1'): [NULL_REVISION],
1540+ ('TREE_ROOT', 'parent-2'): [NULL_REVISION]}
1541+
1542+
1543+all_broken_scenario_classes = [
1544+ UndamagedRepositoryScenario,
1545+ FileParentIsNotInRevisionAncestryScenario,
1546+ FileParentHasInaccessibleInventoryScenario,
1547+ FileParentsNotReferencedByAnyInventoryScenario,
1548+ TooManyParentsScenario,
1549+ ClaimedFileParentDidNotModifyFileScenario,
1550+ IncorrectlyOrderedParentsScenario,
1551+ UnreferencedFileParentsFromNoOpMergeScenario,
1552+ ]
1553+
1554+
1555+def broken_scenarios_for_all_formats():
1556+ format_scenarios = all_repository_vf_format_scenarios()
1557+ # test_check_reconcile needs to be parameterized by format *and* by broken
1558+ # repository scenario.
1559+ broken_scenarios = [(s.__name__, {'scenario_class': s})
1560+ for s in all_broken_scenario_classes]
1561+ return multiply_scenarios(format_scenarios, broken_scenarios)
1562
1563
1564 class TestFileParentReconciliation(TestCaseWithRepository):
1565 """Tests for how reconcile corrects errors in parents of file versions."""
1566
1567+ scenarios = broken_scenarios_for_all_formats()
1568+
1569 def make_populated_repository(self, factory):
1570 """Create a new repository populated by the given factory."""
1571 repo = self.make_repository('broken-repo')