1# Copyright (C) 2006-2010 Canonical Ltd
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
17
18"""Tree implementation tests for bzr.
19
20These test the conformance of all the tree variations to the expected API.
21Specific tests for individual variations are in other places such as:
22 - tests/per_workingtree/*.py.
23 - tests/test_tree.py
24 - tests/test_revision.py
25 - tests/test_workingtree.py
26"""
27
28import contextlib
29
30from breezy import (
31    errors,
32    tests,
33    transform,
34    transport,
35    )
36from breezy.git.tree import GitRevisionTree
37from breezy.git.workingtree import GitWorkingTreeFormat
38from breezy.tests.per_controldir.test_controldir import TestCaseWithControlDir
39from breezy.tests.per_workingtree import (
40    make_scenarios as wt_make_scenarios,
41    make_scenario as wt_make_scenario,
42    )
43from breezy.revisiontree import RevisionTree
44from breezy.tests import (
45    features,
46    )
47from breezy.workingtree import (
48    format_registry,
49    )
50from breezy.bzr.workingtree_4 import (
51    DirStateRevisionTree,
52    WorkingTreeFormat4,
53    WorkingTreeFormat5,
54    )
55
56
57def return_parameter(testcase, something):
58    """A trivial thunk to return its input."""
59    return something
60
61
62def revision_tree_from_workingtree(testcase, tree):
63    """Create a revision tree from a working tree."""
64    revid = tree.commit('save tree', allow_pointless=True, recursive=None)
65    return tree.branch.repository.revision_tree(revid)
66
67
68def _dirstate_tree_from_workingtree(testcase, tree):
69    revid = tree.commit('save tree', allow_pointless=True, recursive=None)
70    return tree.basis_tree()
71
72
73def preview_tree_pre(testcase, tree):
74    tt = tree.preview_transform()
75    testcase.addCleanup(tt.finalize)
76    preview_tree = tt.get_preview_tree()
77    preview_tree.set_parent_ids(tree.get_parent_ids())
78    return preview_tree
79
80
81def preview_tree_post(testcase, tree):
82    basis = tree.basis_tree()
83    tt = basis.preview_transform()
84    testcase.addCleanup(tt.finalize)
85    tree.lock_read()
86    testcase.addCleanup(tree.unlock)
87    pp = None
88    es = contextlib.ExitStack()
89    testcase.addCleanup(es.close)
90    transform._prepare_revert_transform(es, basis, tree, tt, None, False, None,
91                                        basis, {})
92    preview_tree = tt.get_preview_tree()
93    preview_tree.set_parent_ids(tree.get_parent_ids())
94    return preview_tree
95
96
97class TestTreeImplementationSupport(tests.TestCaseWithTransport):
98
99    def test_revision_tree_from_workingtree_bzr(self):
100        tree = self.make_branch_and_tree('.', format='bzr')
101        tree = revision_tree_from_workingtree(self, tree)
102        self.assertIsInstance(tree, RevisionTree)
103
104    def test_revision_tree_from_workingtree(self):
105        tree = self.make_branch_and_tree('.', format='git')
106        tree = revision_tree_from_workingtree(self, tree)
107        self.assertIsInstance(tree, GitRevisionTree)
108
109
110class TestCaseWithTree(TestCaseWithControlDir):
111
112    def make_branch_and_tree(self, relpath):
113        bzrdir_format = self.workingtree_format.get_controldir_for_branch()
114        made_control = self.make_controldir(relpath, format=bzrdir_format)
115        made_control.create_repository()
116        b = made_control.create_branch()
117        if getattr(self, 'repo_is_remote', False):
118            # If the repo is remote, then we just create a local lightweight
119            # checkout
120            # XXX: This duplicates a lot of Branch.create_checkout, but we know
121            #      we want a) lightweight, and b) a specific WT format. We also
122            #      know that nothing should already exist, etc.
123            t = transport.get_transport(relpath)
124            t.ensure_base()
125            wt_dir = bzrdir_format.initialize_on_transport(t)
126            branch_ref = wt_dir.set_branch_reference(b)
127            wt = wt_dir.create_workingtree(None, from_branch=branch_ref)
128        else:
129            wt = self.workingtree_format.initialize(made_control)
130        return wt
131
132    def workingtree_to_test_tree(self, tree):
133        return self._workingtree_to_test_tree(self, tree)
134
135    def _convert_tree(self, tree, converter=None):
136        """helper to convert using the converter or a supplied one."""
137        # convert that to the final shape
138        if converter is None:
139            converter = self.workingtree_to_test_tree
140        return converter(tree)
141
142    def get_tree_no_parents_no_content(self, empty_tree, converter=None):
143        """Make a tree with no parents and no contents from empty_tree.
144
145        :param empty_tree: A working tree with no content and no parents to
146            modify.
147        """
148        if empty_tree.supports_setting_file_ids():
149            empty_tree.set_root_id(b'empty-root-id')
150        return self._convert_tree(empty_tree, converter)
151
152    def _make_abc_tree(self, tree):
153        """setup an abc content tree."""
154        files = ['a', 'b/', 'b/c']
155        self.build_tree(files, line_endings='binary',
156                        transport=tree.controldir.root_transport)
157        tree.add(files)
158
159    def get_tree_no_parents_abc_content(self, tree, converter=None):
160        """return a test tree with a, b/, b/c contents."""
161        self._make_abc_tree(tree)
162        return self._convert_tree(tree, converter)
163
164    def get_tree_no_parents_abc_content_2(self, tree, converter=None):
165        """return a test tree with a, b/, b/c contents.
166
167        This variation changes the content of 'a' to foobar\n.
168        """
169        self._make_abc_tree(tree)
170        with open(tree.basedir + '/a', 'wb') as f:
171            f.write(b'foobar\n')
172        return self._convert_tree(tree, converter)
173
174    def get_tree_no_parents_abc_content_3(self, tree, converter=None):
175        """return a test tree with a, b/, b/c contents.
176
177        This variation changes the executable flag of b/c to True.
178        """
179        self._make_abc_tree(tree)
180        tt = tree.transform()
181        trans_id = tt.trans_id_tree_path('b/c')
182        tt.set_executability(True, trans_id)
183        tt.apply()
184        return self._convert_tree(tree, converter)
185
186    def get_tree_no_parents_abc_content_4(self, tree, converter=None):
187        """return a test tree with d, b/, b/c contents.
188
189        This variation renames a to d.
190        """
191        self._make_abc_tree(tree)
192        tree.rename_one('a', 'd')
193        return self._convert_tree(tree, converter)
194
195    def get_tree_no_parents_abc_content_5(self, tree, converter=None):
196        """return a test tree with d, b/, b/c contents.
197
198        This variation renames a to d and alters its content to 'bar\n'.
199        """
200        self._make_abc_tree(tree)
201        tree.rename_one('a', 'd')
202        with open(tree.basedir + '/d', 'wb') as f:
203            f.write(b'bar\n')
204        return self._convert_tree(tree, converter)
205
206    def get_tree_no_parents_abc_content_6(self, tree, converter=None):
207        """return a test tree with a, b/, e contents.
208
209        This variation renames b/c to e, and makes it executable.
210        """
211        self._make_abc_tree(tree)
212        tt = tree.transform()
213        trans_id = tt.trans_id_tree_path('b/c')
214        parent_trans_id = tt.trans_id_tree_path('')
215        tt.adjust_path('e', parent_trans_id, trans_id)
216        tt.set_executability(True, trans_id)
217        tt.apply()
218        return self._convert_tree(tree, converter)
219
220    def get_tree_no_parents_abc_content_7(self, tree, converter=None):
221        """return a test tree with a, b/, d/e contents.
222
223        This variation adds a dir 'd' (b'd-id'), renames b to d/e.
224        """
225        self._make_abc_tree(tree)
226        self.build_tree(['d/'], transport=tree.controldir.root_transport)
227        tree.add(['d'])
228        tt = tree.transform()
229        trans_id = tt.trans_id_tree_path('b')
230        parent_trans_id = tt.trans_id_tree_path('d')
231        tt.adjust_path('e', parent_trans_id, trans_id)
232        tt.apply()
233        return self._convert_tree(tree, converter)
234
235    def get_tree_with_subdirs_and_all_content_types(self):
236        """Return a test tree with subdirs and all content types.
237        See get_tree_with_subdirs_and_all_supported_content_types for details.
238        """
239        return self.get_tree_with_subdirs_and_all_supported_content_types(True)
240
241    def get_tree_with_subdirs_and_all_supported_content_types(self, symlinks):
242        """Return a test tree with subdirs and all supported content types.
243        Some content types may not be created on some platforms
244        (like symlinks on native win32)
245
246        :param  symlinks:   control is symlink should be created in the tree.
247                            Note: if you wish to automatically set this
248                            parameters depending on underlying system,
249                            please use value returned
250                            by breezy.osutils.has_symlinks() function.
251
252        The returned tree has the following inventory:
253            ['',
254             '0file',
255             '1top-dir',
256             u'2utf\u1234file',
257             'symlink',            # only if symlinks arg is True
258             '1top-dir/0file-in-1topdir',
259             '1top-dir/1dir-in-1topdir']
260        where each component has the type of its name -
261        i.e. '1file..' is afile.
262
263        note that the order of the paths and fileids is deliberately
264        mismatched to ensure that the result order is path based.
265        """
266        self.requireFeature(features.UnicodeFilenameFeature)
267        tree = self.make_branch_and_tree('.')
268        paths = ['0file',
269                 '1top-dir/',
270                 u'2utf\u1234file',
271                 '1top-dir/0file-in-1topdir',
272                 '1top-dir/1dir-in-1topdir/'
273                 ]
274        self.build_tree(paths)
275        tree.add(paths)
276        tt = tree.transform()
277        if symlinks:
278            root_transaction_id = tt.trans_id_tree_path('')
279            tt.new_symlink('symlink',
280                           root_transaction_id, 'link-target', b'symlink')
281        tt.apply()
282        return self.workingtree_to_test_tree(tree)
283
284
285def make_scenarios(transport_server, transport_readonly_server, formats):
286    """Generate test suites for each Tree implementation in breezy.
287
288    Currently this covers all working tree formats, and RevisionTree and
289    DirStateRevisionTree by committing a working tree to create the revision
290    tree.
291    """
292    # TODO(jelmer): Test MemoryTree here
293    # TODO(jelmer): Test GitMemoryTree here
294    scenarios = wt_make_scenarios(transport_server, transport_readonly_server,
295                                  formats)
296    # now adjust the scenarios and add the non-working-tree tree scenarios.
297    for scenario in scenarios:
298        # for working tree format tests, preserve the tree
299        scenario[1]["_workingtree_to_test_tree"] = return_parameter
300    # add RevisionTree scenario
301    workingtree_format = format_registry.get_default()
302    scenarios.append((RevisionTree.__name__,
303                      create_tree_scenario(transport_server, transport_readonly_server,
304                                           workingtree_format, revision_tree_from_workingtree,)))
305    scenarios.append((GitRevisionTree.__name__,
306                      create_tree_scenario(transport_server, transport_readonly_server,
307                                           GitWorkingTreeFormat(), revision_tree_from_workingtree,)))
308
309    # also test WorkingTree4/5's RevisionTree implementation which is
310    # specialised.
311    # XXX: Ask igc if WT5 revision tree actually is different.
312    scenarios.append((DirStateRevisionTree.__name__ + ",WT4",
313                      create_tree_scenario(transport_server, transport_readonly_server,
314                                           WorkingTreeFormat4(), _dirstate_tree_from_workingtree)))
315    scenarios.append((DirStateRevisionTree.__name__ + ",WT5",
316                      create_tree_scenario(transport_server, transport_readonly_server,
317                                           WorkingTreeFormat5(), _dirstate_tree_from_workingtree)))
318    scenarios.append(("PreviewTree", create_tree_scenario(transport_server,
319                                                          transport_readonly_server, workingtree_format, preview_tree_pre)))
320    scenarios.append(("PreviewTreePost", create_tree_scenario(transport_server,
321                                                              transport_readonly_server, workingtree_format, preview_tree_post)))
322    return scenarios
323
324
325def create_tree_scenario(transport_server, transport_readonly_server,
326                         workingtree_format, converter):
327    """Create a scenario for the specified converter
328
329    :param converter: A function that converts a workingtree into the
330        desired format.
331    :param workingtree_format: The particular workingtree format to
332        convert from.
333    :return: a (name, options) tuple, where options is a dict of values
334        to be used as members of the TestCase.
335    """
336    scenario_options = wt_make_scenario(transport_server,
337                                        transport_readonly_server,
338                                        workingtree_format)
339    scenario_options["_workingtree_to_test_tree"] = converter
340    return scenario_options
341
342
343def load_tests(loader, standard_tests, pattern):
344    per_tree_mod_names = [
345        'archive',
346        'annotate_iter',
347        'export',
348        'get_file_mtime',
349        'get_file_with_stat',
350        'get_root_id',
351        'get_symlink_target',
352        'ids',
353        'iter_search_rules',
354        'is_executable',
355        'list_files',
356        'locking',
357        'path_content_summary',
358        'revision_tree',
359        'symlinks',
360        'test_trees',
361        'transform',
362        'tree',
363        'walkdirs',
364        ]
365    submod_tests = loader.loadTestsFromModuleNames(
366        [__name__ + '.test_' + name
367         for name in per_tree_mod_names])
368    scenarios = make_scenarios(
369        tests.default_transport,
370        # None here will cause a readonly decorator to be created
371        # by the TestCaseWithTransport.get_readonly_transport method.
372        None,
373        format_registry._get_all())
374    # add the tests for the sub modules
375    return tests.multiply_tests(submod_tests, scenarios, standard_tests)
376