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."""
15 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(
16 os.path.abspath(__file__)))))
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
29 _GetNumber = iter(itertools.count()).next
32 "project":"tacos/chromite", "branch":"master",
33 "id":"Iee5c89d929f1850d7d4e1a4ff5f21adda800025f",
35 "number":"2", "ref":gerrit.GetChangeRef(1112, 2),
36 "revision":"ff10979dd360e75ff21f5cf53b7f8647578785ef",
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",
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'
54 class TestGitRepoPatch(cros_test_lib.TempDirTestCase):
55 """Unittests for git patch related methods."""
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.
61 patch_kls = cros_patch.GitRepoPatch
69 I am the first commit.
77 # Boolean controlling whether the target class natively knows its
78 # ChangeId; only GerritPatches do.
79 has_native_change_id = False
81 DEFAULT_TRACKING = 'refs/remotes/%s/master' % constants.EXTERNAL_REMOTE
83 def _CreateSourceRepo(self, path):
84 """Generate a new repo with a single commit."""
85 tmp_path = '%s-tmp' % path
88 self._run(['git', 'init', '--separate-git-dir', path], cwd=tmp_path)
90 # Add an initial commit then wipe the working tree.
91 self._run(['git', 'commit', '--allow-empty', '-m', 'initial commit'],
93 shutil.rmtree(tmp_path)
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
105 os.chmod(self.default_cwd, 0o500)
108 if hasattr(self, 'original_cwd'):
109 os.chdir(self.original_cwd)
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),
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.
122 cwd = self.default_cwd
123 return cros_build_lib.RunCommand(
124 cmd, cwd=cwd, print_cmd=False, capture_output=True).output.strip()
126 def _GetSha1(self, cwd, refspec):
127 return self._run(['git', 'rev-list', '-n1', refspec], cwd=cwd)
129 def _MakeRepo(self, name, clone, remote=None, alternates=True):
130 path = os.path.join(self.tempdir, name)
131 cmd = ['git', 'clone', clone, path]
133 cmd += ['--reference', clone]
135 remote = constants.EXTERNAL_REMOTE
136 cmd += ['--origin', remote]
140 def _MakeCommit(self, repo, commit=None):
142 commit = "commit at %s" % (time.time(),)
143 self._run(['git', 'commit', '-a', '-m', commit], repo)
144 return self._GetSha1(repo, 'HEAD')
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)
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
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))
178 _, git2, patch = self._CommonGitSetup()
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)
186 self.assertEqual(patch.sha1, self._GetSha1(git3, patch.sha1))
188 def testFetchFirstPatchInSeries(self):
189 git1, git2, patch = self._CommonGitSetup()
190 self.CommitFile(git1, 'monkeys', 'foon2')
193 def testFetchWithoutSha1(self):
194 git1, git2, _ = self._CommonGitSetup()
195 patch2 = self.CommitFile(git1, 'monkeys', 'foon2')
196 sha1, patch2.sha1 = patch2.sha1, None
198 self.assertEqual(sha1, patch2.sha1)
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})
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'))
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)
222 # Delete an ebuild that exists in TOT, but does not exist in the current
224 check_attrs['inflight'] = True
225 self.assertRaises2(cros_patch.EbuildConflict, patch2.Apply, git1,
226 patch1.sha1, check_attrs=check_attrs)
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
240 patch.project_url = '/dev/null'
241 patch.Apply(git3, self.DEFAULT_TRACKING)
242 self.assertEqual(patch.sha1, self._GetSha1(git3, 'HEAD'))
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})
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)
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'))
275 self.assertRaises2(cros_patch.PatchAlreadyApplied,
276 patch1.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
277 check_attrs={'inflight':False, 'trivial':False})
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},
286 # Now test trivial conflict; this would've merged fine were it not for
288 self.assertRaises2(cros_patch.PatchAlreadyApplied,
289 patch4.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
290 check_attrs={'inflight':False, 'trivial':False},
293 # Move us into inflight testing.
294 patch2.Apply(git2, self.DEFAULT_TRACKING, trivial=True)
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})
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},
307 self.assertRaises2(cros_patch.PatchAlreadyApplied,
308 patch1.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
309 check_attrs={'inflight':False})
311 self.assertRaises2(cros_patch.ApplyPatchException,
312 patch5.Apply, git2, self.DEFAULT_TRACKING, trivial=True,
313 check_attrs={'inflight':True, 'trivial':False},
316 # And this should apply without issue, despite the differing history.
317 patch6.Apply(git2, self.DEFAULT_TRACKING, trivial=True)
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()))
332 def testExternalLookupAliases(self):
333 self._assertLookupAliases(constants.EXTERNAL_REMOTE)
335 def testInternalLookupAliases(self):
336 self._assertLookupAliases(constants.INTERNAL_REMOTE)
338 def MakeChangeId(self, how_many=1):
339 l = [cros_patch.MakeChangeId() for _ in xrange(how_many)]
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
349 changeid = self.MakeChangeId()
350 if raw_changeid_text is None:
351 raw_changeid_text = 'Change-Id: %s' % (changeid,)
354 commit = template % {'change-id': raw_changeid_text, 'extra':extra}
356 return self.CommitFile(repo, filename, content, commit=commit,
357 ChangeId=changeid, **kwargs)
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.
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]
375 change_tuple = cros_patch.StripPrefix(input_id)
376 self.assertTrue(change_tuple in dep_ids)
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.
406 self.assertRaises(cros_patch.BrokenCQDepends,
407 self._CheckPaladin, git1, cid1,
408 [sha1], 'CQ-DEPEND=%s' % sha1)
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))
417 self._CheckPaladin(git1, cid1, [cid2, '*245', cid4],
418 'CQ-DEPEND=%s, %s\nCQ-DEPEND=%s' % (cid2, '*245', cid4))
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')
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')
437 class TestLocalPatchGit(TestGitRepoPatch):
438 """Test Local patch handling."""
440 patch_kls = cros_patch.LocalPatch
443 self.sourceroot = os.path.join(self.tempdir, 'sourceroot')
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)
451 def testUpload(self):
452 def ProjectDirMock(_sourceroot):
455 git1, git2, patch = self._CommonGitSetup()
457 git2_sha1 = self._GetSha1(git2, 'HEAD')
459 patch.ProjectDir = ProjectDirMock
460 # First suppress carbon copy behaviour so we verify pushing
462 # pylint: disable=E1101
464 patch._GetCarbonCopy = lambda: sha1
465 patch.Upload(git2, 'refs/testing/test1')
466 self.assertEqual(self._GetSha1(git2, 'refs/testing/test1'),
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'),
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)
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']
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']
488 self._run(base + ['refs/testing/test1'], git2),
489 self._run(base + ['refs/testing/test2'], git2))
492 class TestUploadedLocalPatch(TestGitRepoPatch):
493 """Test uploading of local git patches."""
495 PROJECT = 'chromiumos/chromite'
496 ORIGINAL_BRANCH = 'original_branch'
497 ORIGINAL_SHA1 = 'ffffffff'.ljust(40, '0')
499 patch_kls = cros_patch.UploadedLocalPatch
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,
506 kwargs.pop('remote', constants.EXTERNAL_REMOTE),
507 carbon_copy_sha1=sha1, **kwargs)
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))
517 class TestGerritPatch(TestGitRepoPatch):
518 """Test Gerrit patch handling."""
520 has_native_change_id = True
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']
533 return copy.deepcopy(FAKE_PATCH_JSON)
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()
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
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))
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
570 self._run(['git', 'push', source, '%s:%s' % (sha1, refspec)], source)
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)
586 def _assertGerritDependencies(self, remote=constants.EXTERNAL_REMOTE):
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'
594 # Test cases with no dependencies, 1 dependency, and 2 dependencies.
595 self.assertEqual(patch.GerritDependencies(), [])
596 patch.patch_dict['dependsOn'] = [{'number': cid1}]
598 [cros_patch.AddPrefix(x, x.gerrit_number)
599 for x in patch.GerritDependencies()],
601 patch.patch_dict['dependsOn'].append({'number': cid2})
603 [cros_patch.AddPrefix(x, x.gerrit_number)
604 for x in patch.GerritDependencies()],
605 [convert(cid1), convert(cid2)])
607 def testExternalGerritDependencies(self):
608 self._assertGerritDependencies()
610 def testInternalGerritDependencies(self):
611 self._assertGerritDependencies(constants.INTERNAL_REMOTE)
614 class PrepareRemotePatchesTest(cros_test_lib.TestCase):
615 """Test preparing remote patches."""
618 project='my/project', original_branch='my-local',
619 ref='refs/tryjobs/elmer/patches', tracking_branch='master',
622 l = [project, original_branch, ref, tracking_branch,
623 getattr(constants, '%s_PATCH_TAG' % (
624 'INTERNAL' if internal else 'EXTERNAL'))]
627 def assertRemote(self, patch, project='my/project',
628 original_branch='my-local',
629 ref='refs/tryjobs/elmer/patches', tracking_branch='master',
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)
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])
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')
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 + [':']))
660 class PrepareLocalPatchesTests(cros_build_lib_unittest.RunCommandTestCase):
661 """Test preparing local patches."""
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,
671 checkout = git.ProjectCheckout(attrs)
673 self.manifest, 'FindCheckouts', return_value=[checkout]
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)
687 def testBranchSpecifiedSuccessRun(self):
688 """Test success with branch specified by user."""
689 self.PrepareLocalPatches('12345'.rjust(40, '0'))
691 def testBranchSpecifiedNoChanges(self):
692 """Test when no changes on the branch specified by user."""
693 self.assertRaises(SystemExit, self.PrepareLocalPatches, '')
696 class TestFormatting(cros_test_lib.TestCase):
697 """Test formatting of output."""
699 def _assertResult(self, functor, value, expected=None, raises=False,
702 self.assertRaises2(ValueError, functor, value,
703 msg="%s(%r) did not throw a ValueError"
704 % (functor.__name__, value), **kwargs)
706 self.assertEqual(functor(value, **kwargs), expected,
707 msg="failed: %s(%r) != %r"
708 % (functor.__name__, value, expected))
710 def _assertBad(self, functor, values, **kwargs):
712 self._assertResult(functor, value, raises=True, **kwargs)
714 def _assertGood(self, functor, values, **kwargs):
715 for value, expected in values:
716 self._assertResult(functor, value, expected, **kwargs)
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])
725 cros_patch.ParseGerritNumber,
726 ['is', 'i1325', '01234567', '012345a', '**12345', '+123', '/0123'],
729 def TestChangeID(self):
730 """Tests that we can parse a change-ID."""
731 self._assertGood(cros_patch.ParseChangeID,
732 [('I47ea30385af60ae4cc2acc5d1a283a46423bc6e1',) * 2])
734 # Change-IDs too short/long, with unexpected characters in it.
736 cros_patch.ParseChangeID,
737 ['is', '**i1325', 'i134'.ljust(41, '0'), 'I1234+'.ljust(41, '0'),
738 'I123'.ljust(42, '0')],
742 """Tests that we can parse a SHA1 hash."""
743 self._assertGood(cros_patch.ParseSHA1,
746 ('1a7e034'.ljust(40, '0'),) *2])
749 cros_patch.ParseSHA1,
750 ['0abcg', 'Z', '**a', '+123', '1234ab' * 10],
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))])
762 cros_patch.ParseFullChangeID,
763 ['foo', 'foo~bar', 'foo~bar~baz', 'foo~refs/bar~%s' % change_id],
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,
772 self.assertTrue(cros_patch.ParsePatchDep(val) is not None)
774 self._assertBad(cros_patch.ParsePatchDep,
775 ['1454623', 'I47ea3', 'i47ea3'.ljust(41, '0')])
778 if __name__ == '__main__':