1"""Unit tests for BazaarClient."""
2
3from __future__ import unicode_literals
4
5import os
6from hashlib import md5
7
8from nose import SkipTest
9
10from rbtools.clients import RepositoryInfo
11from rbtools.clients.bazaar import BazaarClient
12from rbtools.clients.errors import TooManyRevisionsError
13from rbtools.clients.tests import FOO, FOO1, FOO2, FOO3, SCMClientTests
14from rbtools.utils.filesystem import is_exe_in_path, make_tempdir
15from rbtools.utils.process import execute
16
17
18class BazaarClientTests(SCMClientTests):
19    """Unit tests for BazaarClient."""
20
21    def setUp(self):
22        if not is_exe_in_path('bzr'):
23            raise SkipTest('bzr not found in path')
24
25        super(BazaarClientTests, self).setUp()
26
27        self.set_user_home(
28            os.path.join(self.testdata_dir, 'homedir'))
29
30        self.original_branch = make_tempdir()
31        self._run_bzr(['init', '.'], cwd=self.original_branch)
32        self._bzr_add_file_commit('foo.txt', FOO, 'initial commit',
33                                  cwd=self.original_branch)
34
35        self.child_branch = make_tempdir()
36        self._run_bzr(['branch', '--use-existing-dir', self.original_branch,
37                       self.child_branch],
38                      cwd=self.original_branch)
39        self.client = BazaarClient(options=self.options)
40
41        self.options.parent_branch = None
42
43    def _run_bzr(self, command, *args, **kwargs):
44        return execute(['bzr'] + command, *args, **kwargs)
45
46    def _bzr_add_file_commit(self, filename, data, msg, cwd=None, *args,
47                             **kwargs):
48        """Add a file to a Bazaar repository.
49
50        Args:
51            filename (unicode):
52                The name of the file to add.
53
54            data (bytes):
55                The data to write to the file.
56
57            msg (unicode):
58                The commit message to use.
59
60            cwd (unicode, optional):
61                A working directory to use when running the bzr commands.
62
63            *args (list):
64                Positional arguments to pass through to
65                :py:func:`rbtools.utils.process.execute`.
66
67            **kwargs (dict):
68                Keyword arguments to pass through to
69                :py:func:`rbtools.utils.process.execute`.
70        """
71        if cwd is not None:
72            filename = os.path.join(cwd, filename)
73
74        with open(filename, 'wb') as f:
75            f.write(data)
76
77        self._run_bzr(['add', filename], cwd=cwd, *args, **kwargs)
78        self._run_bzr(['commit', '-m', msg, '--author', 'Test User'],
79                      cwd=cwd, *args, **kwargs)
80
81    def _compare_diffs(self, filename, full_diff, expected_diff_digest,
82                       change_type='modified'):
83        """Testing that the full_diff for ``filename`` matches the
84        ``expected_diff``."""
85        diff_lines = full_diff.splitlines()
86
87        self.assertEqual(('=== %s file \'%s\''
88                          % (change_type, filename)).encode('utf-8'),
89                         diff_lines[0])
90        self.assertTrue(diff_lines[1].startswith(
91            ('--- %s\t' % filename).encode('utf-8')))
92        self.assertTrue(diff_lines[2].startswith(
93            ('+++ %s\t' % filename).encode('utf-8')))
94
95        diff_body = b'\n'.join(diff_lines[3:])
96        self.assertEqual(md5(diff_body).hexdigest(), expected_diff_digest)
97
98    def _count_files_in_diff(self, diff):
99        return len([
100            line
101            for line in diff.split(b'\n')
102            if line.startswith(b'===')
103        ])
104
105    def test_get_repository_info_original_branch(self):
106        """Testing BazaarClient get_repository_info with original branch"""
107        os.chdir(self.original_branch)
108        ri = self.client.get_repository_info()
109
110        self.assertTrue(isinstance(ri, RepositoryInfo))
111        self.assertEqual(os.path.realpath(ri.path),
112                         os.path.realpath(self.original_branch))
113        self.assertTrue(ri.supports_parent_diffs)
114
115        self.assertEqual(ri.base_path, '/')
116        self.assertFalse(ri.supports_changesets)
117
118    def test_get_repository_info_child_branch(self):
119        """Testing BazaarClient get_repository_info with child branch"""
120        os.chdir(self.child_branch)
121        ri = self.client.get_repository_info()
122
123        self.assertTrue(isinstance(ri, RepositoryInfo))
124        self.assertEqual(os.path.realpath(ri.path),
125                         os.path.realpath(self.child_branch))
126        self.assertTrue(ri.supports_parent_diffs)
127
128        self.assertEqual(ri.base_path, "/")
129        self.assertFalse(ri.supports_changesets)
130
131    def test_get_repository_info_no_branch(self):
132        """Testing BazaarClient get_repository_info, no branch"""
133        self.chdir_tmp()
134        ri = self.client.get_repository_info()
135        self.assertEqual(ri, None)
136
137    def test_too_many_revisions(self):
138        """Testing BazaarClient parse_revision_spec with too many revisions"""
139        self.assertRaises(TooManyRevisionsError,
140                          self.client.parse_revision_spec,
141                          [1, 2, 3])
142
143    def test_diff_simple(self):
144        """Testing BazaarClient simple diff case"""
145        os.chdir(self.child_branch)
146
147        self._bzr_add_file_commit('foo.txt', FOO1, 'delete and modify stuff')
148
149        revisions = self.client.parse_revision_spec([])
150        result = self.client.diff(revisions)
151        self.assertTrue(isinstance(result, dict))
152        self.assertTrue('diff' in result)
153
154        self._compare_diffs('foo.txt', result['diff'],
155                            'a6326b53933f8b255a4b840485d8e210')
156
157    def test_diff_exclude(self):
158        """Testing BazaarClient diff with file exclusion"""
159        os.chdir(self.child_branch)
160
161        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
162        self._bzr_add_file_commit('exclude.txt', FOO2, 'commit 2')
163
164        revisions = self.client.parse_revision_spec([])
165        result = self.client.diff(revisions, exclude_patterns=['exclude.txt'])
166        self.assertTrue(isinstance(result, dict))
167        self.assertTrue('diff' in result)
168
169        self._compare_diffs('foo.txt', result['diff'],
170                            'a6326b53933f8b255a4b840485d8e210')
171
172        self.assertEqual(self._count_files_in_diff(result['diff']), 1)
173
174    def test_diff_exclude_in_subdir(self):
175        """Testing BazaarClient diff with file exclusion in a subdirectory"""
176        os.chdir(self.child_branch)
177
178        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
179
180        os.mkdir('subdir')
181        os.chdir('subdir')
182
183        self._bzr_add_file_commit('exclude.txt', FOO2, 'commit 2')
184
185        revisions = self.client.parse_revision_spec([])
186        result = self.client.diff(revisions,
187                                  exclude_patterns=['exclude.txt', '.'])
188        self.assertTrue(isinstance(result, dict))
189        self.assertTrue('diff' in result)
190
191        self._compare_diffs('foo.txt', result['diff'],
192                            'a6326b53933f8b255a4b840485d8e210')
193
194        self.assertEqual(self._count_files_in_diff(result['diff']), 1)
195
196    def test_diff_exclude_root_pattern_in_subdir(self):
197        """Testing BazaarClient diff with file exclusion in the repo root"""
198        os.chdir(self.child_branch)
199
200        self._bzr_add_file_commit('exclude.txt', FOO2, 'commit 1')
201
202        os.mkdir('subdir')
203        os.chdir('subdir')
204
205        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 2')
206
207        revisions = self.client.parse_revision_spec([])
208        result = self.client.diff(
209            revisions,
210            exclude_patterns=[os.path.sep + 'exclude.txt',
211                              os.path.sep + 'subdir'])
212
213        self.assertTrue(isinstance(result, dict))
214        self.assertTrue('diff' in result)
215
216        self._compare_diffs(os.path.join('subdir', 'foo.txt'), result['diff'],
217                            '4deffcb296180fa166eddff2512bd0e4',
218                            change_type='added')
219
220    def test_diff_specific_files(self):
221        """Testing BazaarClient diff with specific files"""
222        os.chdir(self.child_branch)
223
224        self._bzr_add_file_commit('foo.txt', FOO1, 'delete and modify stuff')
225        self._bzr_add_file_commit('bar.txt', b'baz', 'added bar')
226
227        revisions = self.client.parse_revision_spec([])
228        result = self.client.diff(revisions, ['foo.txt'])
229        self.assertTrue(isinstance(result, dict))
230        self.assertTrue('diff' in result)
231
232        self._compare_diffs('foo.txt', result['diff'],
233                            'a6326b53933f8b255a4b840485d8e210')
234
235    def test_diff_simple_multiple(self):
236        """Testing BazaarClient simple diff with multiple commits case"""
237        os.chdir(self.child_branch)
238
239        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
240        self._bzr_add_file_commit('foo.txt', FOO2, 'commit 2')
241        self._bzr_add_file_commit('foo.txt', FOO3, 'commit 3')
242
243        revisions = self.client.parse_revision_spec([])
244        result = self.client.diff(revisions)
245        self.assertTrue(isinstance(result, dict))
246        self.assertTrue('diff' in result)
247
248        self._compare_diffs('foo.txt', result['diff'],
249                            '4109cc082dce22288c2f1baca9b107b6')
250
251    def test_diff_parent(self):
252        """Testing BazaarClient diff with changes only in the parent branch"""
253        self._bzr_add_file_commit('foo.txt', FOO1, 'delete and modify stuff',
254                                  cwd=self.child_branch)
255
256        grand_child_branch = make_tempdir()
257        self._run_bzr(['branch', '--use-existing-dir', self.child_branch,
258                       grand_child_branch],
259                      cwd=self.child_branch)
260        os.chdir(grand_child_branch)
261
262        revisions = self.client.parse_revision_spec([])
263        result = self.client.diff(revisions)
264        self.assertTrue(isinstance(result, dict))
265        self.assertTrue('diff' in result)
266
267        self.assertEqual(result['diff'], None)
268
269    def test_diff_grand_parent(self):
270        """Testing BazaarClient diff with changes between a 2nd level
271        descendant"""
272        self._bzr_add_file_commit('foo.txt', FOO1, 'delete and modify stuff',
273                                  cwd=self.child_branch)
274
275        grand_child_branch = make_tempdir()
276        self._run_bzr(['branch', '--use-existing-dir', self.child_branch,
277                       grand_child_branch],
278                      cwd=self.child_branch)
279        os.chdir(grand_child_branch)
280
281        # Requesting the diff between the grand child branch and its grand
282        # parent:
283        self.options.parent_branch = self.original_branch
284
285        revisions = self.client.parse_revision_spec([])
286        result = self.client.diff(revisions)
287        self.assertTrue(isinstance(result, dict))
288        self.assertTrue('diff' in result)
289
290        self._compare_diffs('foo.txt', result['diff'],
291                            'a6326b53933f8b255a4b840485d8e210')
292
293    def test_guessed_summary_and_description(self):
294        """Testing BazaarClient guessing summary and description"""
295        os.chdir(self.child_branch)
296
297        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
298        self._bzr_add_file_commit('foo.txt', FOO2, 'commit 2')
299        self._bzr_add_file_commit('foo.txt', FOO3, 'commit 3')
300
301        self.options.guess_summary = True
302        self.options.guess_description = True
303        revisions = self.client.parse_revision_spec([])
304        commit_message = self.client.get_commit_message(revisions)
305
306        self.assertEqual('commit 3', commit_message['summary'])
307
308        description = commit_message['description']
309        self.assertTrue('commit 1' in description)
310        self.assertTrue('commit 2' in description)
311        self.assertFalse('commit 3' in description)
312
313    def test_guessed_summary_and_description_in_grand_parent_branch(self):
314        """Testing BazaarClient guessing summary and description for grand
315        parent branch"""
316        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1',
317                                  cwd=self.child_branch)
318        self._bzr_add_file_commit('foo.txt', FOO2, 'commit 2',
319                                  cwd=self.child_branch)
320        self._bzr_add_file_commit('foo.txt', FOO3, 'commit 3',
321                                  cwd=self.child_branch)
322
323        self.options.guess_summary = True
324        self.options.guess_description = True
325
326        grand_child_branch = make_tempdir()
327        self._run_bzr(['branch', '--use-existing-dir', self.child_branch,
328                       grand_child_branch],
329                      cwd=self.child_branch)
330        os.chdir(grand_child_branch)
331
332        # Requesting the diff between the grand child branch and its grand
333        # parent:
334        self.options.parent_branch = self.original_branch
335
336        revisions = self.client.parse_revision_spec([])
337        commit_message = self.client.get_commit_message(revisions)
338
339        self.assertEqual('commit 3', commit_message['summary'])
340
341        description = commit_message['description']
342        self.assertTrue('commit 1' in description)
343        self.assertTrue('commit 2' in description)
344        self.assertFalse('commit 3' in description)
345
346    def test_guessed_summary_and_description_with_revision_range(self):
347        """Testing BazaarClient guessing summary and description with a
348        revision range"""
349        os.chdir(self.child_branch)
350
351        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
352        self._bzr_add_file_commit('foo.txt', FOO2, 'commit 2')
353        self._bzr_add_file_commit('foo.txt', FOO3, 'commit 3')
354
355        self.options.guess_summary = True
356        self.options.guess_description = True
357        revisions = self.client.parse_revision_spec(['2..3'])
358        commit_message = self.client.get_commit_message(revisions)
359
360        self.assertEqual('commit 2', commit_message['summary'])
361        self.assertEqual('commit 2', commit_message['description'])
362
363    def test_parse_revision_spec_no_args(self):
364        """Testing BazaarClient.parse_revision_spec with no specified
365        revisions"""
366        os.chdir(self.child_branch)
367
368        base_commit_id = self.client._get_revno()
369        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
370        tip_commit_id = self.client._get_revno()
371
372        revisions = self.client.parse_revision_spec()
373        self.assertTrue(isinstance(revisions, dict))
374        self.assertTrue('base' in revisions)
375        self.assertTrue('tip' in revisions)
376        self.assertTrue('parent_base' not in revisions)
377        self.assertEqual(revisions['base'], base_commit_id)
378        self.assertEqual(revisions['tip'], tip_commit_id)
379
380    def test_parse_revision_spec_one_arg(self):
381        """Testing BazaarClient.parse_revision_spec with one specified
382        revision"""
383        os.chdir(self.child_branch)
384
385        base_commit_id = self.client._get_revno()
386        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
387        tip_commit_id = self.client._get_revno()
388
389        revisions = self.client.parse_revision_spec([tip_commit_id])
390        self.assertTrue(isinstance(revisions, dict))
391        self.assertTrue('base' in revisions)
392        self.assertTrue('tip' in revisions)
393        self.assertTrue('parent_base' not in revisions)
394        self.assertEqual(revisions['base'], base_commit_id)
395        self.assertEqual(revisions['tip'], tip_commit_id)
396
397    def test_parse_revision_spec_one_arg_parent(self):
398        """Testing BazaarClient.parse_revision_spec with one specified
399        revision and a parent diff"""
400        os.chdir(self.original_branch)
401        parent_base_commit_id = self.client._get_revno()
402
403        grand_child_branch = make_tempdir()
404        self._run_bzr(['branch', '--use-existing-dir', self.child_branch,
405                       grand_child_branch])
406        os.chdir(grand_child_branch)
407
408        base_commit_id = self.client._get_revno()
409        self._bzr_add_file_commit('foo.txt', FOO2, 'commit 2')
410        tip_commit_id = self.client._get_revno()
411
412        self.options.parent_branch = self.child_branch
413
414        revisions = self.client.parse_revision_spec([tip_commit_id])
415        self.assertTrue(isinstance(revisions, dict))
416        self.assertTrue('parent_base' in revisions)
417        self.assertTrue('base' in revisions)
418        self.assertTrue('tip' in revisions)
419        self.assertEqual(revisions['parent_base'], parent_base_commit_id)
420        self.assertEqual(revisions['base'], base_commit_id)
421        self.assertEqual(revisions['tip'], tip_commit_id)
422
423    def test_parse_revision_spec_one_arg_split(self):
424        """Testing BazaarClient.parse_revision_spec with R1..R2 syntax"""
425        os.chdir(self.child_branch)
426
427        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
428        base_commit_id = self.client._get_revno()
429        self._bzr_add_file_commit('foo.txt', FOO2, 'commit 2')
430        tip_commit_id = self.client._get_revno()
431
432        revisions = self.client.parse_revision_spec(
433            ['%s..%s' % (base_commit_id, tip_commit_id)])
434        self.assertTrue(isinstance(revisions, dict))
435        self.assertTrue('parent_base' not in revisions)
436        self.assertTrue('base' in revisions)
437        self.assertTrue('tip' in revisions)
438        self.assertEqual(revisions['base'], base_commit_id)
439        self.assertEqual(revisions['tip'], tip_commit_id)
440
441    def test_parse_revision_spec_two_args(self):
442        """Testing BazaarClient.parse_revision_spec with two revisions"""
443        os.chdir(self.child_branch)
444
445        self._bzr_add_file_commit('foo.txt', FOO1, 'commit 1')
446        base_commit_id = self.client._get_revno()
447        self._bzr_add_file_commit('foo.txt', FOO2, 'commit 2')
448        tip_commit_id = self.client._get_revno()
449
450        revisions = self.client.parse_revision_spec(
451            [base_commit_id, tip_commit_id])
452        self.assertTrue(isinstance(revisions, dict))
453        self.assertTrue('parent_base' not in revisions)
454        self.assertTrue('base' in revisions)
455        self.assertTrue('tip' in revisions)
456        self.assertEqual(revisions['base'], base_commit_id)
457        self.assertEqual(revisions['tip'], tip_commit_id)
458