Upstream version 8.36.161.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / lib / patch_unittest.py
1 #!/usr/bin/python
2 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 """Unittests for chromite.lib.patch."""
7
8 import copy
9 import itertools
10 import os
11 import shutil
12 import sys
13 import time
14
15 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
16     os.path.abspath(__file__)))))
17
18 from chromite.buildbot import constants
19 from chromite.lib import cros_build_lib
20 from chromite.lib import cros_build_lib_unittest
21 from chromite.lib import cros_test_lib
22 from chromite.lib import gerrit
23 from chromite.lib import git
24 from chromite.lib import osutils
25 from chromite.lib import patch as cros_patch
26
27 import mock
28
29 _GetNumber = iter(itertools.count()).next
30
31 FAKE_PATCH_JSON = {
32   "project":"tacos/chromite", "branch":"master",
33   "id":"Iee5c89d929f1850d7d4e1a4ff5f21adda800025f",
34   "currentPatchSet": {
35     "number":"2", "ref":gerrit.GetChangeRef(1112, 2),
36     "revision":"ff10979dd360e75ff21f5cf53b7f8647578785ef",
37   },
38   "number":"1112",
39   "subject":"chromite commit",
40   "owner":{"name":"Chromite Master", "email":"chromite@chromium.org"},
41   "url":"https://chromium-review.googlesource.com/1112",
42   "lastUpdated":1311024529,
43   "sortKey":"00166e8700001052",
44   "open": True,
45   "status":"NEW",
46 }
47
48 # Change-ID of a known open change in public gerrit.
49 GERRIT_OPEN_CHANGEID = '8366'
50 GERRIT_MERGED_CHANGEID = '3'
51 GERRIT_ABANDONED_CHANGEID = '2'
52
53
54 class TestGitRepoPatch(cros_test_lib.TempDirTestCase):
55   """Unittests for git patch related methods."""
56
57   # No mock bits are to be used in this class's tests.
58   # This needs to actually validate git output, and git behaviour, rather
59   # than test our assumptions about git's behaviour/output.
60
61   patch_kls = cros_patch.GitRepoPatch
62
63   COMMIT_TEMPLATE = (
64 """commit abcdefgh
65
66 Author: Fake person
67 Date:  Tue Oct 99
68
69 I am the first commit.
70
71 %(extra)s
72
73 %(change-id)s
74 """
75   )
76
77   # Boolean controlling whether the target class natively knows its
78   # ChangeId; only GerritPatches do.
79   has_native_change_id = False
80
81   DEFAULT_TRACKING = 'refs/remotes/%s/master' % constants.EXTERNAL_REMOTE
82
83   def _CreateSourceRepo(self, path):
84     """Generate a new repo with a single commit."""
85     tmp_path = '%s-tmp' % path
86     os.mkdir(path)
87     os.mkdir(tmp_path)
88     self._run(['git', 'init', '--separate-git-dir', path], cwd=tmp_path)
89
90     # Add an initial commit then wipe the working tree.
91     self._run(['git', 'commit', '--allow-empty', '-m', 'initial commit'],
92               cwd=tmp_path)
93     shutil.rmtree(tmp_path)
94
95   def setUp(self):
96     # Create an empty repo to work from.
97     self.source = os.path.join(self.tempdir, 'source.git')
98     self._CreateSourceRepo(self.source)
99     self.default_cwd = os.path.join(self.tempdir, 'unwritable')
100     self.original_cwd = os.getcwd()
101     os.mkdir(self.default_cwd)
102     os.chdir(self.default_cwd)
103     # Disallow write so as to smoke out any invalid writes to
104     # cwd.
105     os.chmod(self.default_cwd, 0o500)
106
107   def tearDown(self):
108     if hasattr(self, 'original_cwd'):
109       os.chdir(self.original_cwd)
110
111   def _MkPatch(self, source, sha1, ref='refs/heads/master', **kwargs):
112     return self.patch_kls(source, 'chromiumos/chromite', ref,
113                           '%s/master' % constants.EXTERNAL_REMOTE,
114                           kwargs.pop('remote', constants.EXTERNAL_REMOTE),
115                           sha1=sha1, **kwargs)
116
117   def _run(self, cmd, cwd=None):
118     # Note that cwd is intentionally set to a location the user can't write
119     # to; this flushes out any bad usage in the tests that would work by
120     # fluke of being invoked from w/in a git repo.
121     if cwd is None:
122       cwd = self.default_cwd
123     return cros_build_lib.RunCommand(
124         cmd, cwd=cwd, print_cmd=False, capture_output=True).output.strip()
125
126   def _GetSha1(self, cwd, refspec):
127     return self._run(['git', 'rev-list', '-n1', refspec], cwd=cwd)
128
129   def _MakeRepo(self, name, clone, remote=None, alternates=True):
130     path = os.path.join(self.tempdir, name)
131     cmd = ['git', 'clone', clone, path]
132     if alternates:
133       cmd += ['--reference', clone]
134     if remote is None:
135       remote = constants.EXTERNAL_REMOTE
136     cmd += ['--origin', remote]
137     self._run(cmd)
138     return path
139
140   def _MakeCommit(self, repo, commit=None):
141     if commit is None:
142       commit = "commit at %s" % (time.time(),)
143     self._run(['git', 'commit', '-a', '-m', commit], repo)
144     return self._GetSha1(repo, 'HEAD')
145
146   def CommitFile(self, repo, filename, content, commit=None, **kwargs):
147     osutils.WriteFile(os.path.join(repo, filename), content)
148     self._run(['git', 'add', filename], repo)
149     sha1 = self._MakeCommit(repo, commit=commit)
150     if not self.has_native_change_id:
151       kwargs.pop('ChangeId', None)
152     patch = self._MkPatch(repo, sha1, **kwargs)
153     self.assertEqual(patch.sha1, sha1)
154     return patch
155
156   def _CommonGitSetup(self):
157     git1 = self._MakeRepo('git1', self.source)
158     git2 = self._MakeRepo('git2', self.source)
159     patch = self.CommitFile(git1, 'monkeys', 'foon')
160     return git1, git2, patch
161
162   def testGetDiffStatus(self):
163     git1, _, patch1 = self._CommonGitSetup()
164     # Ensure that it can work on the first commit, even if it
165     # doesn't report anything (no delta; it's the first files).
166     patch1 = self._MkPatch(git1, self._GetSha1(git1, self.DEFAULT_TRACKING))
167     self.assertEqual({}, patch1.GetDiffStatus(git1))
168     patch2 = self.CommitFile(git1, 'monkeys', 'blah')
169     self.assertEqual({'monkeys': 'M'}, patch2.GetDiffStatus(git1))
170     git.RunGit(git1, ['mv', 'monkeys', 'monkeys2'])
171     patch3 = self._MkPatch(git1, self._MakeCommit(git1, commit="mv"))
172     self.assertEqual({'monkeys': 'D', 'monkeys2': 'A'},
173                      patch3.GetDiffStatus(git1))
174     patch4 = self.CommitFile(git1, 'monkey2', 'blah')
175     self.assertEqual({'monkey2': 'A'}, patch4.GetDiffStatus(git1))
176
177   def testFetch(self):
178     _, git2, patch = self._CommonGitSetup()
179     patch.Fetch(git2)
180     self.assertEqual(patch.sha1, self._GetSha1(git2, 'FETCH_HEAD'))
181     # Verify reuse; specifically that Fetch doesn't actually run since
182     # the rev is already available locally via alternates.
183     patch.project_url = '/dev/null'
184     git3 = self._MakeRepo('git3', git2)
185     patch.Fetch(git3)
186     self.assertEqual(patch.sha1, self._GetSha1(git3, patch.sha1))
187
188   def testFetchFirstPatchInSeries(self):
189     git1, git2, patch = self._CommonGitSetup()
190     self.CommitFile(git1, 'monkeys', 'foon2')
191     patch.Fetch(git2)
192
193   def testFetchWithoutSha1(self):
194     git1, git2, _ = self._CommonGitSetup()
195     patch2 = self.CommitFile(git1, 'monkeys', 'foon2')
196     sha1, patch2.sha1 = patch2.sha1, None
197     patch2.Fetch(git2)
198     self.assertEqual(sha1, patch2.sha1)
199
200   def testAlreadyApplied(self):
201     git1 = self._MakeRepo('git1', self.source)
202     patch1 = self._MkPatch(git1, self._GetSha1(git1, 'HEAD'))
203     self.assertRaises2(cros_patch.PatchAlreadyApplied, patch1.Apply, git1,
204                        self.DEFAULT_TRACKING, check_attrs={'inflight':False})
205     patch2 = self.CommitFile(git1, 'monkeys', 'rule')
206     self.assertRaises2(cros_patch.PatchAlreadyApplied, patch2.Apply, git1,
207                        self.DEFAULT_TRACKING, check_attrs={'inflight':True})
208
209   def testDeleteEbuildTwice(self):
210     """Test that double-deletes of ebuilds are flagged as conflicts."""
211     # Create monkeys.ebuild for testing.
212     git1 = self._MakeRepo('git1', self.source)
213     patch1 = self.CommitFile(git1, 'monkeys.ebuild', 'rule')
214     git.RunGit(git1, ['rm', 'monkeys.ebuild'])
215     patch2 = self._MkPatch(git1, self._MakeCommit(git1, commit='rm'))
216
217     # Delete an ebuild that does not exist in TOT.
218     check_attrs = {'inflight': False, 'files': ('monkeys.ebuild',)}
219     self.assertRaises2(cros_patch.EbuildConflict, patch2.Apply, git1,
220                        self.DEFAULT_TRACKING, check_attrs=check_attrs)
221
222     # Delete an ebuild that exists in TOT, but does not exist in the current
223     # patch series.
224     check_attrs['inflight'] = True
225     self.assertRaises2(cros_patch.EbuildConflict, patch2.Apply, git1,
226                        patch1.sha1, check_attrs=check_attrs)
227
228   def testCleanlyApply(self):
229     _, git2, patch = self._CommonGitSetup()
230     # Clone git3 before we modify git2; else we'll just wind up
231     # cloning it's master.
232     git3 = self._MakeRepo('git3', git2)
233     patch.Apply(git2, self.DEFAULT_TRACKING)
234     self.assertEqual(patch.sha1, self._GetSha1(git2, 'HEAD'))
235     # Verify reuse; specifically that Fetch doesn't actually run since
236     # the object is available in alternates.  testFetch partially
237     # validates this; the Apply usage here fully validates it via
238     # ensuring that the attempted Apply goes boom if it can't get the
239     # required sha1.
240     patch.project_url = '/dev/null'
241     patch.Apply(git3, self.DEFAULT_TRACKING)
242     self.assertEqual(patch.sha1, self._GetSha1(git3, 'HEAD'))
243
244   def testFailsApply(self):
245     _, git2, patch1 = self._CommonGitSetup()
246     patch2 = self.CommitFile(git2, 'monkeys', 'not foon')
247     # Note that Apply creates it's own branch, resetting to master
248     # thus we have to re-apply (even if it looks stupid, it's right).
249     patch2.Apply(git2, self.DEFAULT_TRACKING)
250     self.assertRaises2(cros_patch.ApplyPatchException,
251                        patch1.Apply, git2, self.DEFAULT_TRACKING,
252                        exact_kls=True, check_attrs={'inflight':True})
253
254   def testTrivial(self):
255     _, git2, patch1 = self._CommonGitSetup()
256     # Throw in a bunch of newlines so that content-merging would work.
257     content = 'not foon%s' % ('\n' * 100)
258     patch1 = self._MkPatch(git2, self._GetSha1(git2, 'HEAD'))
259     patch1 = self.CommitFile(git2, 'monkeys', content)
260     git.RunGit(
261         git2, ['update-ref', self.DEFAULT_TRACKING, patch1.sha1])
262     patch2 = self.CommitFile(git2, 'monkeys', '%sblah' % content)
263     patch3 = self.CommitFile(git2, 'monkeys', '%sblahblah' % content)
264     # Get us a back to the basic, then derive from there; this is used to
265     # verify that even if content merging works, trivial is flagged.
266     self.CommitFile(git2, 'monkeys', 'foon')
267     patch4 = self.CommitFile(git2, 'monkeys', content)
268     patch5 = self.CommitFile(git2, 'monkeys', '%sfoon' % content)
269     # Reset so we derive the next changes from patch1.
270     git.RunGit(git2, ['reset', '--hard', patch1.sha1])
271     patch6 = self.CommitFile(git2, 'blah', 'some-other-file')
272     self.CommitFile(git2, 'monkeys',
273                     '%sblah' % content.replace('not', 'bot'))
274
275     self.assertRaises2(cros_patch.PatchAlreadyApplied,
276                        patch1.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
277                        check_attrs={'inflight':False, 'trivial':False})
278
279     # Now test conflicts since we're still at ToT; note that this is an actual
280     # conflict because the fuzz anchors have changed.
281     self.assertRaises2(cros_patch.ApplyPatchException,
282                        patch3.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
283                        check_attrs={'inflight':False, 'trivial':False},
284                        exact_kls=True)
285
286     # Now test trivial conflict; this would've merged fine were it not for
287     # trivial.
288     self.assertRaises2(cros_patch.PatchAlreadyApplied,
289                        patch4.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
290                        check_attrs={'inflight':False, 'trivial':False},
291                        exact_kls=True)
292
293     # Move us into inflight testing.
294     patch2.Apply(git2, self.DEFAULT_TRACKING, trivial=True)
295
296     # Repeat the tests from above; should still be the same.
297     self.assertRaises2(cros_patch.PatchAlreadyApplied,
298                        patch4.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
299                        check_attrs={'inflight':False, 'trivial':False})
300
301     # Actual conflict merge conflict due to inflight; non trivial induced.
302     self.assertRaises2(cros_patch.ApplyPatchException,
303                        patch5.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
304                        check_attrs={'inflight':True, 'trivial':False},
305                        exact_kls=True)
306
307     self.assertRaises2(cros_patch.PatchAlreadyApplied,
308                        patch1.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
309                        check_attrs={'inflight':False})
310
311     self.assertRaises2(cros_patch.ApplyPatchException,
312                        patch5.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
313                        check_attrs={'inflight':True, 'trivial':False},
314                        exact_kls=True)
315
316     # And this should apply without issue, despite the differing history.
317     patch6.Apply(git2, self.DEFAULT_TRACKING, trivial=True)
318
319   def _assertLookupAliases(self, remote):
320     git1 = self._MakeRepo('git1', self.source)
321     patch = self.CommitChangeIdFile(git1, remote=remote)
322     prefix = '*' if patch.internal else ''
323     vals = [patch.sha1, getattr(patch, 'gerrit_number', None),
324             getattr(patch, 'original_sha1', None)]
325     # Append full Change-ID if it exists.
326     if patch.project and patch.tracking_branch and patch.change_id:
327       vals.append('%s~%s~%s' % (
328           patch.project, patch.tracking_branch, patch.change_id))
329     vals = [x for x in vals if x is not None]
330     self.assertEqual(set(prefix + x for x in vals), set(patch.LookupAliases()))
331
332   def testExternalLookupAliases(self):
333     self._assertLookupAliases(constants.EXTERNAL_REMOTE)
334
335   def testInternalLookupAliases(self):
336     self._assertLookupAliases(constants.INTERNAL_REMOTE)
337
338   def MakeChangeId(self, how_many=1):
339     l = [cros_patch.MakeChangeId() for _ in xrange(how_many)]
340     if how_many == 1:
341       return l[0]
342     return l
343
344   def CommitChangeIdFile(self, repo, changeid=None, extra=None,
345                          filename='monkeys', content='flinging',
346                          raw_changeid_text=None, **kwargs):
347     template = self.COMMIT_TEMPLATE
348     if changeid is None:
349       changeid = self.MakeChangeId()
350     if raw_changeid_text is None:
351       raw_changeid_text = 'Change-Id: %s' % (changeid,)
352     if extra is None:
353       extra = ''
354     commit = template % {'change-id': raw_changeid_text, 'extra':extra}
355
356     return self.CommitFile(repo, filename, content, commit=commit,
357                            ChangeId=changeid, **kwargs)
358
359   def _CheckPaladin(self, repo, master_id, ids, extra):
360     patch = self.CommitChangeIdFile(
361         repo, master_id, extra=extra,
362         filename='paladincheck', content=str(_GetNumber()))
363     deps = patch.PaladinDependencies(repo)
364     # Assert that our parsing unique'ifies the results.
365     self.assertEqual(len(deps), len(set(deps)))
366     # Verify that we have the correct dependencies.
367     dep_ids = []
368     dep_ids += [(dep.remote, dep.change_id) for dep in deps
369                 if dep.change_id is not None]
370     dep_ids += [(dep.remote, dep.gerrit_number) for dep in deps
371                 if dep.gerrit_number is not None]
372     dep_ids += [(dep.remote, dep.sha1) for dep in deps
373                 if dep.sha1 is not None]
374     for input_id in ids:
375       change_tuple = cros_patch.StripPrefix(input_id)
376       self.assertTrue(change_tuple in dep_ids)
377
378     return patch
379
380   def testPaladinDependencies(self):
381     git1 = self._MakeRepo('git1', self.source)
382     cid1, cid2, cid3, cid4 = self.MakeChangeId(4)
383     # Verify it handles nonexistant CQ-DEPEND.
384     self._CheckPaladin(git1, cid1, [], '')
385     # Single key, single value.
386     self._CheckPaladin(git1, cid1, [cid2],
387                        'CQ-DEPEND=%s' % cid2)
388     # Single key, gerrit number.
389     self._CheckPaladin(git1, cid1, ['123'],
390                        'CQ-DEPEND=%s' % 123)
391     # Single key, gerrit number.
392     self._CheckPaladin(git1, cid1, ['123456'],
393                        'CQ-DEPEND=%s' % 123456)
394     # Single key, gerrit number; ensure it
395     # cuts off before a million changes (this
396     # is done to avoid collisions w/ sha1 when
397     # we're using shortened versions).
398     self.assertRaises(cros_patch.BrokenCQDepends,
399                       self._CheckPaladin, git1, cid1,
400                       ['1234567'], 'CQ-DEPEND=%s' % '1234567')
401     # Single key, gerrit number, internal.
402     self._CheckPaladin(git1, cid1, ['*123'],
403                        'CQ-DEPEND=%s' % '*123')
404     # Ensure SHA1's aren't allowed.
405     sha1 = '0' * 40
406     self.assertRaises(cros_patch.BrokenCQDepends,
407                       self._CheckPaladin, git1, cid1,
408                       [sha1], 'CQ-DEPEND=%s' % sha1)
409
410     # Single key, multiple values
411     self._CheckPaladin(git1, cid1, [cid2, '1223'],
412                        'CQ-DEPEND=%s %s' % (cid2, '1223'))
413     # Dumb comma behaviour
414     self._CheckPaladin(git1, cid1, [cid2, cid3],
415                       'CQ-DEPEND=%s, %s,' % (cid2, cid3))
416     # Multiple keys.
417     self._CheckPaladin(git1, cid1, [cid2, '*245', cid4],
418                       'CQ-DEPEND=%s, %s\nCQ-DEPEND=%s' % (cid2, '*245', cid4))
419
420     # Ensure it goes boom on invalid data.
421     self.assertRaises(cros_patch.BrokenCQDepends, self._CheckPaladin,
422                       git1, cid1, [], 'CQ-DEPEND=monkeys')
423     self.assertRaises(cros_patch.BrokenCQDepends, self._CheckPaladin,
424                       git1, cid1, [], 'CQ-DEPEND=%s monkeys' % (cid2,))
425     # Validate numeric is allowed.
426     self._CheckPaladin(git1, cid1, [cid2, '1'], 'CQ-DEPEND=1 %s' % cid2)
427     # Validate that it unique'ifies the results.
428     self._CheckPaladin(git1, cid1, ['1'], 'CQ-DEPEND=1 1')
429
430     # Invalid syntax
431     self.assertRaises(cros_patch.BrokenCQDepends, self._CheckPaladin,
432                       git1, cid1, [], 'CQ-DEPENDS=1')
433     self.assertRaises(cros_patch.BrokenCQDepends, self._CheckPaladin,
434                       git1, cid1, [], 'CQ_DEPEND=1')
435
436
437 class TestLocalPatchGit(TestGitRepoPatch):
438   """Test Local patch handling."""
439
440   patch_kls = cros_patch.LocalPatch
441
442   def setUp(self):
443     self.sourceroot = os.path.join(self.tempdir, 'sourceroot')
444
445
446   def _MkPatch(self, source, sha1, ref='refs/heads/master', **kwargs):
447     remote = kwargs.pop('remote', constants.EXTERNAL_REMOTE)
448     return self.patch_kls(source, 'chromiumos/chromite', ref,
449                           '%s/master' % remote, remote, sha1, **kwargs)
450
451   def testUpload(self):
452     def ProjectDirMock(_sourceroot):
453       return git1
454
455     git1, git2, patch = self._CommonGitSetup()
456
457     git2_sha1 = self._GetSha1(git2, 'HEAD')
458
459     patch.ProjectDir = ProjectDirMock
460     # First suppress carbon copy behaviour so we verify pushing
461     # plain works.
462     # pylint: disable=E1101
463     sha1 = patch.sha1
464     patch._GetCarbonCopy = lambda: sha1
465     patch.Upload(git2, 'refs/testing/test1')
466     self.assertEqual(self._GetSha1(git2, 'refs/testing/test1'),
467                      patch.sha1)
468
469     # Enable CarbonCopy behaviour; verify it lands a different
470     # sha1.  Additionally verify it didn't corrupt the patch's sha1 locally.
471     del patch._GetCarbonCopy
472     patch.Upload(git2, 'refs/testing/test2')
473     self.assertNotEqual(self._GetSha1(git2, 'refs/testing/test2'),
474                         patch.sha1)
475     self.assertEqual(patch.sha1, sha1)
476     # Ensure the carbon creation didn't damage the target repo.
477     self.assertEqual(self._GetSha1(git1, 'HEAD'), sha1)
478
479     # Ensure we didn't damage the target repo's state at all.
480     self.assertEqual(git2_sha1, self._GetSha1(git2, 'HEAD'))
481     # Ensure the content is the same.
482     base = ['git', 'show']
483     self.assertEqual(
484         self._run(base + ['refs/testing/test1:monkeys'], git2),
485         self._run(base + ['refs/testing/test2:monkeys'], git2))
486     base = ['git', 'log', '--format=%B', '-n1']
487     self.assertEqual(
488         self._run(base + ['refs/testing/test1'], git2),
489         self._run(base + ['refs/testing/test2'], git2))
490
491
492 class TestUploadedLocalPatch(TestGitRepoPatch):
493   """Test uploading of local git patches."""
494
495   PROJECT = 'chromiumos/chromite'
496   ORIGINAL_BRANCH = 'original_branch'
497   ORIGINAL_SHA1 = 'ffffffff'.ljust(40, '0')
498
499   patch_kls = cros_patch.UploadedLocalPatch
500
501   def _MkPatch(self, source, sha1, ref='refs/heads/master', **kwargs):
502     return self.patch_kls(source, self.PROJECT, ref,
503                           '%s/master' % constants.EXTERNAL_REMOTE,
504                           self.ORIGINAL_BRANCH,
505                           self.ORIGINAL_SHA1,
506                           kwargs.pop('remote', constants.EXTERNAL_REMOTE),
507                           carbon_copy_sha1=sha1, **kwargs)
508
509   def testStringRepresentation(self):
510     _, _, patch = self._CommonGitSetup()
511     str_rep = str(patch).split(':')
512     for element in [self.PROJECT, self.ORIGINAL_BRANCH, self.ORIGINAL_SHA1[:8]]:
513       self.assertTrue(element in str_rep,
514                       msg="Couldn't find %s in %s" % (element, str_rep))
515
516
517 class TestGerritPatch(TestGitRepoPatch):
518   """Test Gerrit patch handling."""
519
520   has_native_change_id = True
521
522   class patch_kls(cros_patch.GerritPatch):
523     """Test helper class to suppress pointing to actual gerrit."""
524     # Suppress the behaviour pointing the project url at actual gerrit,
525     # instead slaving it back to a local repo for tests.
526     def __init__(self, *args, **kwargs):
527       cros_patch.GerritPatch.__init__(self, *args, **kwargs)
528       assert hasattr(self, 'patch_dict')
529       self.project_url = self.patch_dict['_unittest_url_bypass']
530
531   @property
532   def test_json(self):
533     return copy.deepcopy(FAKE_PATCH_JSON)
534
535   def _MkPatch(self, source, sha1, ref='refs/heads/master', **kwargs):
536     json = self.test_json
537     remote = kwargs.pop('remote', constants.EXTERNAL_REMOTE)
538     url_prefix = kwargs.pop('url_prefix', constants.EXTERNAL_GERRIT_URL)
539     suppress_branch = kwargs.pop('suppress_branch', False)
540     change_id = kwargs.pop('ChangeId', None)
541     if change_id is None:
542       change_id = self.MakeChangeId()
543     json.update(kwargs)
544     change_num, patch_num = _GetNumber(), _GetNumber()
545     # Note we intentionally use a gerrit like refspec here; we want to
546     # ensure that none of our common code pathways puke on a non head/tag.
547     refspec = gerrit.GetChangeRef(change_num + 1000, patch_num)
548     json['currentPatchSet'].update(
549         dict(number=patch_num, ref=refspec, revision=sha1))
550     json['branch'] = os.path.basename(ref)
551     json['_unittest_url_bypass'] = source
552     json['id'] = change_id
553
554     obj = self.patch_kls(json.copy(), remote, url_prefix)
555     self.assertEqual(obj.patch_dict, json)
556     self.assertEqual(obj.remote, remote)
557     self.assertEqual(obj.url_prefix, url_prefix)
558     self.assertEqual(obj.project, json['project'])
559     self.assertEqual(obj.ref, refspec)
560     self.assertEqual(obj.change_id, change_id)
561     self.assertEqual(obj.id, '%s%s~%s~%s' % (
562         constants.CHANGE_PREFIX[remote], json['project'],
563         json['branch'], change_id))
564
565     # Now make the fetching actually work, if desired.
566     if not suppress_branch:
567       # Note that a push is needed here, rather than a branch; branch
568       # will just make it under refs/heads, we want it literally in
569       # refs/changes/
570       self._run(['git', 'push', source, '%s:%s' % (sha1, refspec)], source)
571     return obj
572
573   def testApprovalTimestamp(self):
574     """Test that the approval timestamp is correctly extracted from JSON."""
575     repo = self._MakeRepo('git', self.source)
576     for approvals, expected in [(None, 0), ([], 0), ([1], 1), ([1, 3, 2], 3)]:
577       currentPatchSet = copy.deepcopy(FAKE_PATCH_JSON['currentPatchSet'])
578       if approvals is not None:
579         currentPatchSet['approvals'] = [{'grantedOn': x} for x in approvals]
580       patch = self._MkPatch(repo, self._GetSha1(repo, self.DEFAULT_TRACKING),
581                             currentPatchSet=currentPatchSet)
582       msg = 'Expected %r, but got %r (approvals=%r)' % (
583           expected, patch.approval_timestamp, approvals)
584       self.assertEqual(patch.approval_timestamp, expected, msg)
585
586   def _assertGerritDependencies(self, remote=constants.EXTERNAL_REMOTE):
587     convert = str
588     if remote == constants.INTERNAL_REMOTE:
589       convert = lambda val: '*%s' % (val,)
590     git1 = self._MakeRepo('git1', self.source, remote=remote)
591     patch = self._MkPatch(git1, self._GetSha1(git1, 'HEAD'), remote=remote)
592     cid1, cid2 = '1', '2'
593
594     # Test cases with no dependencies, 1 dependency, and 2 dependencies.
595     self.assertEqual(patch.GerritDependencies(), [])
596     patch.patch_dict['dependsOn'] = [{'number': cid1}]
597     self.assertEqual(
598         [cros_patch.AddPrefix(x, x.gerrit_number)
599          for x in patch.GerritDependencies()],
600         [convert(cid1)])
601     patch.patch_dict['dependsOn'].append({'number': cid2})
602     self.assertEqual(
603         [cros_patch.AddPrefix(x, x.gerrit_number)
604          for x in patch.GerritDependencies()],
605         [convert(cid1), convert(cid2)])
606
607   def testExternalGerritDependencies(self):
608     self._assertGerritDependencies()
609
610   def testInternalGerritDependencies(self):
611     self._assertGerritDependencies(constants.INTERNAL_REMOTE)
612
613
614 class PrepareRemotePatchesTest(cros_test_lib.TestCase):
615   """Test preparing remote patches."""
616
617   def MkRemote(self,
618                project='my/project', original_branch='my-local',
619                ref='refs/tryjobs/elmer/patches', tracking_branch='master',
620                internal=False):
621
622     l = [project, original_branch, ref, tracking_branch,
623          getattr(constants, '%s_PATCH_TAG' % (
624             'INTERNAL' if internal else 'EXTERNAL'))]
625     return ':'.join(l)
626
627   def assertRemote(self, patch, project='my/project',
628                    original_branch='my-local',
629                    ref='refs/tryjobs/elmer/patches', tracking_branch='master',
630                    internal=False):
631     self.assertEqual(patch.project, project)
632     self.assertEqual(patch.original_branch, original_branch)
633     self.assertEqual(patch.ref, ref)
634     self.assertEqual(patch.tracking_branch, tracking_branch)
635     self.assertEqual(patch.internal, internal)
636
637   def test(self):
638     # Check handling of a single patch...
639     patches = cros_patch.PrepareRemotePatches([self.MkRemote()])
640     self.assertEqual(len(patches), 1)
641     self.assertRemote(patches[0])
642
643     # Check handling of a multiple...
644     patches = cros_patch.PrepareRemotePatches(
645         [self.MkRemote(), self.MkRemote(project='foon')])
646     self.assertEqual(len(patches), 2)
647     self.assertRemote(patches[0])
648     self.assertRemote(patches[1], project='foon')
649
650     # Ensure basic validation occurs:
651     chunks = self.MkRemote().split(':')
652     self.assertRaises(ValueError, cros_patch.PrepareRemotePatches,
653                       ':'.join(chunks[:-1]))
654     self.assertRaises(ValueError, cros_patch.PrepareRemotePatches,
655                       ':'.join(chunks[:-1] + ['monkeys']))
656     self.assertRaises(ValueError, cros_patch.PrepareRemotePatches,
657                       ':'.join(chunks + [':']))
658
659
660 class PrepareLocalPatchesTests(cros_build_lib_unittest.RunCommandTestCase):
661   """Test preparing local patches."""
662
663   def setUp(self):
664     self.path, self.project, self.branch = 'mydir', 'my/project', 'mybranch'
665     self.tracking_branch = 'kernel'
666     self.patches = ['%s:%s' % (self.project, self.branch)]
667     self.manifest = mock.MagicMock()
668     attrs = dict(tracking_branch=self.tracking_branch,
669                  local_path=self.path,
670                  remote='cros')
671     checkout = git.ProjectCheckout(attrs)
672     self.PatchObject(
673         self.manifest, 'FindCheckouts', return_value=[checkout]
674     )
675
676   def PrepareLocalPatches(self, output):
677     """Check the returned GitRepoPatchInfo against golden values."""
678     output_obj = mock.MagicMock()
679     output_obj.output = output
680     self.PatchObject(cros_patch.LocalPatch, 'Fetch', return_value=output_obj)
681     self.PatchObject(git, 'RunGit', return_value=output_obj)
682     patch_info, = cros_patch.PrepareLocalPatches(self.manifest, self.patches)
683     self.assertEquals(patch_info.project, self.project)
684     self.assertEquals(patch_info.ref, self.branch)
685     self.assertEquals(patch_info.tracking_branch, self.tracking_branch)
686
687   def testBranchSpecifiedSuccessRun(self):
688     """Test success with branch specified by user."""
689     self.PrepareLocalPatches('12345'.rjust(40, '0'))
690
691   def testBranchSpecifiedNoChanges(self):
692     """Test when no changes on the branch specified by user."""
693     self.assertRaises(SystemExit, self.PrepareLocalPatches, '')
694
695
696 class TestFormatting(cros_test_lib.TestCase):
697   """Test formatting of output."""
698
699   def _assertResult(self, functor, value, expected=None, raises=False,
700                     **kwargs):
701     if raises:
702       self.assertRaises2(ValueError, functor, value,
703                          msg="%s(%r) did not throw a ValueError"
704                          % (functor.__name__, value),  **kwargs)
705     else:
706       self.assertEqual(functor(value, **kwargs), expected,
707                        msg="failed: %s(%r) != %r"
708                        % (functor.__name__, value, expected))
709
710   def _assertBad(self, functor, values, **kwargs):
711     for value in values:
712       self._assertResult(functor, value, raises=True, **kwargs)
713
714   def _assertGood(self, functor, values, **kwargs):
715     for value, expected in values:
716       self._assertResult(functor, value, expected, **kwargs)
717
718
719   def TestGerritNumber(self):
720     """Tests that we can pasre a Gerrit number."""
721     self._assertGood(cros_patch.ParseGerritNumber,
722         [('12345',) * 2, ('12',) * 2, ('123',) * 2])
723
724     self._assertBad(
725         cros_patch.ParseGerritNumber,
726         ['is', 'i1325', '01234567', '012345a', '**12345', '+123', '/0123'],
727         error_ok=False)
728
729   def TestChangeID(self):
730     """Tests that we can parse a change-ID."""
731     self._assertGood(cros_patch.ParseChangeID,
732         [('I47ea30385af60ae4cc2acc5d1a283a46423bc6e1',) * 2])
733
734     # Change-IDs too short/long, with unexpected characters in it.
735     self._assertBad(
736         cros_patch.ParseChangeID,
737         ['is', '**i1325', 'i134'.ljust(41, '0'), 'I1234+'.ljust(41, '0'),
738          'I123'.ljust(42, '0')],
739         error_ok=False)
740
741   def TestSHA1(self):
742     """Tests that we can parse a SHA1 hash."""
743     self._assertGood(cros_patch.ParseSHA1,
744                      [('1' * 40,) * 2,
745                       ('a' * 40,) * 2,
746                       ('1a7e034'.ljust(40, '0'),) *2])
747
748     self._assertBad(
749         cros_patch.ParseSHA1,
750         ['0abcg', 'Z', '**a', '+123', '1234ab' * 10],
751         error_ok=False)
752
753   def TestFullChangeID(self):
754     """Tests that we can parse a full change-ID."""
755     change_id = 'I47ea30385af60ae4cc2acc5d1a283a46423bc6e1'
756     self._assertGood(cros_patch.ParseFullChangeID,
757         [('foo~bar~%s' % change_id, ('foo', 'bar', change_id)),
758          ('foo/bar/baz~refs/heads/_my-branch_~%s' % change_id,
759           ('foo/bar/baz', '_my-branch_', change_id))])
760
761     self._assertBad(
762         cros_patch.ParseFullChangeID,
763         ['foo', 'foo~bar', 'foo~bar~baz', 'foo~refs/bar~%s' % change_id],
764         error_ok=False)
765
766   def testParsePatchDeps(self):
767     """Tests that we can parse the dependency specified by the user."""
768     change_id = 'I47ea30385af60ae4cc2acc5d1a283a46423bc6e1'
769     vals = ['CL:12345', 'project~branch~%s' % change_id, change_id,
770             change_id[1:]]
771     for val in vals:
772       self.assertTrue(cros_patch.ParsePatchDep(val) is not None)
773
774     self._assertBad(cros_patch.ParsePatchDep,
775                     ['1454623', 'I47ea3', 'i47ea3'.ljust(41, '0')])
776
777
778 if __name__ == '__main__':
779   cros_test_lib.main()