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