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.
6 """Unittests for chromite.lib.patch."""
8 from __future__ import print_function
17 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
18 os.path.abspath(__file__)))))
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
31 _GetNumber = iter(itertools.count()).next
34 "project":"tacos/chromite", "branch":"master",
35 "id":"Iee5c89d929f1850d7d4e1a4ff5f21adda800025f",
37 "number":"2", "ref":gerrit.GetChangeRef(1112, 2),
38 "revision":"ff10979dd360e75ff21f5cf53b7f8647578785ef",
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",
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'
56 class GitRepoPatchTestCase(cros_test_lib.TempDirTestCase):
57 """Helper TestCase class for writing test cases."""
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.
63 patch_kls = cros_patch.GitRepoPatch
71 I am the first commit.
79 # Boolean controlling whether the target class natively knows its
80 # ChangeId; only GerritPatches do.
81 has_native_change_id = False
83 DEFAULT_TRACKING = 'refs/remotes/%s/master' % constants.EXTERNAL_REMOTE
85 def _CreateSourceRepo(self, path):
86 """Generate a new repo with a single commit."""
87 tmp_path = '%s-tmp' % path
90 self._run(['git', 'init', '--separate-git-dir', path], cwd=tmp_path)
92 # Add an initial commit then wipe the working tree.
93 self._run(['git', 'commit', '--allow-empty', '-m', 'initial commit'],
95 shutil.rmtree(tmp_path)
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
107 os.chmod(self.default_cwd, 0o500)
110 if hasattr(self, 'original_cwd'):
111 os.chdir(self.original_cwd)
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),
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.
124 cwd = self.default_cwd
125 return cros_build_lib.RunCommand(
126 cmd, cwd=cwd, print_cmd=False, capture_output=True).output.strip()
128 def _GetSha1(self, cwd, refspec):
129 return self._run(['git', 'rev-list', '-n1', refspec], cwd=cwd)
131 def _MakeRepo(self, name, clone, remote=None, alternates=True):
132 path = os.path.join(self.tempdir, name)
133 cmd = ['git', 'clone', clone, path]
135 cmd += ['--reference', clone]
137 remote = constants.EXTERNAL_REMOTE
138 cmd += ['--origin', remote]
142 def _MakeCommit(self, repo, commit=None):
144 commit = "commit at %s" % (time.time(),)
145 self._run(['git', 'commit', '-a', '-m', commit], repo)
146 return self._GetSha1(repo, 'HEAD')
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)
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
164 def MakeChangeId(self, how_many=1):
165 l = [cros_patch.MakeChangeId() for _ in xrange(how_many)]
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
175 changeid = self.MakeChangeId()
176 if raw_changeid_text is None:
177 raw_changeid_text = 'Change-Id: %s' % (changeid,)
180 commit = template % {'change-id': raw_changeid_text, 'extra':extra}
182 return self.CommitFile(repo, filename, content, commit=commit,
183 ChangeId=changeid, **kwargs)
186 class TestGitRepoPatch(GitRepoPatchTestCase):
187 """Unittests for git patch related methods."""
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))
205 _, git2, patch = self._CommonGitSetup()
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)
213 self.assertEqual(patch.sha1, self._GetSha1(git3, patch.sha1))
215 def testFetchFirstPatchInSeries(self):
216 git1, git2, patch = self._CommonGitSetup()
217 self.CommitFile(git1, 'monkeys', 'foon2')
220 def testFetchWithoutSha1(self):
221 git1, git2, _ = self._CommonGitSetup()
222 patch2 = self.CommitFile(git1, 'monkeys', 'foon2')
223 sha1, patch2.sha1 = patch2.sha1, None
225 self.assertEqual(sha1, patch2.sha1)
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})
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'))
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)
249 # Delete an ebuild that exists in TOT, but does not exist in the current
251 check_attrs['inflight'] = True
252 self.assertRaises2(cros_patch.EbuildConflict, patch2.Apply, git1,
253 patch1.sha1, check_attrs=check_attrs)
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
267 patch.project_url = '/dev/null'
268 patch.Apply(git3, self.DEFAULT_TRACKING)
269 self.assertEqual(patch.sha1, self._GetSha1(git3, 'HEAD'))
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})
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)
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'))
302 self.assertRaises2(cros_patch.PatchIsEmpty,
303 patch1.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
304 check_attrs={'inflight':False, 'trivial':False})
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},
313 # Now test trivial conflict; this would've merged fine were it not for
315 self.assertRaises2(cros_patch.PatchIsEmpty,
316 patch4.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
317 check_attrs={'inflight':False, 'trivial':False},
320 # Move us into inflight testing.
321 patch2.Apply(git2, self.DEFAULT_TRACKING, trivial=True)
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})
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},
334 self.assertRaises2(cros_patch.PatchIsEmpty,
335 patch1.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
336 check_attrs={'inflight':False})
338 self.assertRaises2(cros_patch.ApplyPatchException,
339 patch5.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
340 check_attrs={'inflight':True, 'trivial':False},
343 # And this should apply without issue, despite the differing history.
344 patch6.Apply(git2, self.DEFAULT_TRACKING, trivial=True)
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()))
359 def testExternalLookupAliases(self):
360 self._assertLookupAliases(constants.EXTERNAL_REMOTE)
362 def testInternalLookupAliases(self):
363 self._assertLookupAliases(constants.INTERNAL_REMOTE)
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.
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]
381 change_tuple = cros_patch.StripPrefix(input_id)
382 self.assertTrue(change_tuple in dep_ids)
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.
412 self.assertRaises(cros_patch.BrokenCQDepends,
413 self._CheckPaladin, git1, cid1,
414 [sha1], 'CQ-DEPEND=%s' % sha1)
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))
423 self._CheckPaladin(git1, cid1, [cid2, '*245', cid4],
424 'CQ-DEPEND=%s, %s\nCQ-DEPEND=%s' % (cid2, '*245', cid4))
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')
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')
443 class TestApplyAgainstManifest(GitRepoPatchTestCase,
444 cros_test_lib.MockTestCase):
445 """Test applying a patch against a manifest"""
447 MANIFEST_TEMPLATE = (
448 """<?xml version="1.0" encoding="UTF-8"?>
450 <remote name="cros" />
451 <default revision="refs/heads/master" remote="cros" />
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')
462 for project in projects:
463 proj_pieces.append('<project')
464 for key, val in project.items():
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}
473 osutils.WriteFile(manifest_file, content)
476 def testApplyAgainstManifest(self):
477 git1, git2, _ = self._CommonGitSetup()
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)
486 git1_proj = {'path': git1,
487 'name': 'chromiumos/chromite',
488 'revision': str(readme1.sha1),
489 'upstream': 'refs/heads/master'
491 git2_proj = {'path': git2,
494 basedir = self._CommonRepoSetup(git1_proj, git2_proj)
496 # pylint: disable=E1101
497 self.PatchObject(git.ManifestCheckout, '_GetManifestsBranch',
499 manifest = git.ManifestCheckout(basedir)
501 readme2.ApplyAgainstManifest(manifest)
502 readme3.ApplyAgainstManifest(manifest)
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)],
508 self.assertEqual(shas, [str(readme3.sha1), str(readme2.sha1)])
511 class TestLocalPatchGit(GitRepoPatchTestCase):
512 """Test Local patch handling."""
514 patch_kls = cros_patch.LocalPatch
517 self.sourceroot = os.path.join(self.tempdir, 'sourceroot')
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)
524 def testUpload(self):
525 def ProjectDirMock(_sourceroot):
528 git1, git2, patch = self._CommonGitSetup()
530 git2_sha1 = self._GetSha1(git2, 'HEAD')
532 patch.ProjectDir = ProjectDirMock
533 # First suppress carbon copy behaviour so we verify pushing
535 # pylint: disable=E1101
537 patch._GetCarbonCopy = lambda: sha1
538 patch.Upload(git2, 'refs/testing/test1')
539 self.assertEqual(self._GetSha1(git2, 'refs/testing/test1'),
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'),
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)
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']
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']
561 self._run(base + ['refs/testing/test1'], git2),
562 self._run(base + ['refs/testing/test2'], git2))
565 class TestUploadedLocalPatch(GitRepoPatchTestCase):
566 """Test uploading of local git patches."""
568 PROJECT = 'chromiumos/chromite'
569 ORIGINAL_BRANCH = 'original_branch'
570 ORIGINAL_SHA1 = 'ffffffff'.ljust(40, '0')
572 patch_kls = cros_patch.UploadedLocalPatch
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,
579 kwargs.pop('remote', constants.EXTERNAL_REMOTE),
580 carbon_copy_sha1=sha1, **kwargs)
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))
590 class TestGerritPatch(GitRepoPatchTestCase):
591 """Test Gerrit patch handling."""
593 has_native_change_id = True
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']
606 return copy.deepcopy(FAKE_PATCH_JSON)
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()
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
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))
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
643 self._run(['git', 'push', source, '%s:%s' % (sha1, refspec)], source)
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)
659 def _assertGerritDependencies(self, remote=constants.EXTERNAL_REMOTE):
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'
667 # Test cases with no dependencies, 1 dependency, and 2 dependencies.
668 self.assertEqual(patch.GerritDependencies(), [])
669 patch.patch_dict['dependsOn'] = [{'number': cid1}]
671 [cros_patch.AddPrefix(x, x.gerrit_number)
672 for x in patch.GerritDependencies()],
674 patch.patch_dict['dependsOn'].append({'number': cid2})
676 [cros_patch.AddPrefix(x, x.gerrit_number)
677 for x in patch.GerritDependencies()],
678 [convert(cid1), convert(cid2)])
680 def testExternalGerritDependencies(self):
681 self._assertGerritDependencies()
683 def testInternalGerritDependencies(self):
684 self._assertGerritDependencies(constants.INTERNAL_REMOTE)
687 class PrepareRemotePatchesTest(cros_test_lib.TestCase):
688 """Test preparing remote patches."""
691 project='my/project', original_branch='my-local',
692 ref='refs/tryjobs/elmer/patches', tracking_branch='master',
695 l = [project, original_branch, ref, tracking_branch,
696 getattr(constants, '%s_PATCH_TAG' % (
697 'INTERNAL' if internal else 'EXTERNAL'))]
700 def assertRemote(self, patch, project='my/project',
701 original_branch='my-local',
702 ref='refs/tryjobs/elmer/patches', tracking_branch='master',
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)
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])
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')
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 + [':']))
733 class PrepareLocalPatchesTests(cros_build_lib_unittest.RunCommandTestCase):
734 """Test preparing local patches."""
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,
744 checkout = git.ProjectCheckout(attrs)
746 self.manifest, 'FindCheckouts', return_value=[checkout]
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)
760 def testBranchSpecifiedSuccessRun(self):
761 """Test success with branch specified by user."""
762 self.PrepareLocalPatches('12345'.rjust(40, '0'))
764 def testBranchSpecifiedNoChanges(self):
765 """Test when no changes on the branch specified by user."""
766 self.assertRaises(SystemExit, self.PrepareLocalPatches, '')
769 class TestFormatting(cros_test_lib.TestCase):
770 """Test formatting of output."""
772 def _assertResult(self, functor, value, expected=None, raises=False,
775 self.assertRaises2(ValueError, functor, value,
776 msg="%s(%r) did not throw a ValueError"
777 % (functor.__name__, value), **kwargs)
779 self.assertEqual(functor(value, **kwargs), expected,
780 msg="failed: %s(%r) != %r"
781 % (functor.__name__, value, expected))
783 def _assertBad(self, functor, values, **kwargs):
785 self._assertResult(functor, value, raises=True, **kwargs)
787 def _assertGood(self, functor, values, **kwargs):
788 for value, expected in values:
789 self._assertResult(functor, value, expected, **kwargs)
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])
798 cros_patch.ParseGerritNumber,
799 ['is', 'i1325', '01234567', '012345a', '**12345', '+123', '/0123'],
802 def TestChangeID(self):
803 """Tests that we can parse a change-ID."""
804 self._assertGood(cros_patch.ParseChangeID,
805 [('I47ea30385af60ae4cc2acc5d1a283a46423bc6e1',) * 2])
807 # Change-IDs too short/long, with unexpected characters in it.
809 cros_patch.ParseChangeID,
810 ['is', '**i1325', 'i134'.ljust(41, '0'), 'I1234+'.ljust(41, '0'),
811 'I123'.ljust(42, '0')],
815 """Tests that we can parse a SHA1 hash."""
816 self._assertGood(cros_patch.ParseSHA1,
819 ('1a7e034'.ljust(40, '0'),) *2])
822 cros_patch.ParseSHA1,
823 ['0abcg', 'Z', '**a', '+123', '1234ab' * 10],
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))])
835 cros_patch.ParseFullChangeID,
836 ['foo', 'foo~bar', 'foo~bar~baz', 'foo~refs/bar~%s' % change_id],
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,
845 self.assertTrue(cros_patch.ParsePatchDep(val) is not None)
847 self._assertBad(cros_patch.ParsePatchDep,
848 ['1454623', 'I47ea3', 'i47ea3'.ljust(41, '0')])
851 if __name__ == '__main__':