git_cl_test.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754
  1. #!/usr/bin/env python
  2. # Copyright (c) 2012 The Chromium 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. """Unit tests for git_cl.py."""
  6. import os
  7. import StringIO
  8. import stat
  9. import sys
  10. import unittest
  11. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  12. from testing_support.auto_stub import TestCase
  13. import git_cl
  14. import git_common
  15. import subprocess2
  16. class PresubmitMock(object):
  17. def __init__(self, *args, **kwargs):
  18. self.reviewers = []
  19. @staticmethod
  20. def should_continue():
  21. return True
  22. class RietveldMock(object):
  23. def __init__(self, *args, **kwargs):
  24. pass
  25. @staticmethod
  26. def get_description(issue):
  27. return 'Issue: %d' % issue
  28. @staticmethod
  29. def get_issue_properties(_issue, _messages):
  30. return {
  31. 'reviewers': ['joe@chromium.org', 'john@chromium.org'],
  32. 'messages': [
  33. {
  34. 'approval': True,
  35. 'sender': 'john@chromium.org',
  36. },
  37. ],
  38. }
  39. class WatchlistsMock(object):
  40. def __init__(self, _):
  41. pass
  42. @staticmethod
  43. def GetWatchersForPaths(_):
  44. return ['joe@example.com']
  45. class CodereviewSettingsFileMock(object):
  46. def __init__(self):
  47. pass
  48. # pylint: disable=R0201
  49. def read(self):
  50. return ("CODE_REVIEW_SERVER: gerrit.chromium.org\n" +
  51. "GERRIT_HOST: gerrit.chromium.org\n" +
  52. "GERRIT_PORT: 29418\n")
  53. class TestGitCl(TestCase):
  54. def setUp(self):
  55. super(TestGitCl, self).setUp()
  56. self.calls = []
  57. self._calls_done = 0
  58. self.mock(subprocess2, 'call', self._mocked_call)
  59. self.mock(subprocess2, 'check_call', self._mocked_call)
  60. self.mock(subprocess2, 'check_output', self._mocked_call)
  61. self.mock(subprocess2, 'communicate', self._mocked_call)
  62. self.mock(subprocess2, 'Popen', self._mocked_call)
  63. self.mock(git_common, 'get_or_create_merge_base',
  64. lambda *a: (
  65. self._mocked_call(['get_or_create_merge_base']+list(a))))
  66. self.mock(git_cl, 'FindCodereviewSettingsFile', lambda: '')
  67. self.mock(git_cl, 'ask_for_data', self._mocked_call)
  68. self.mock(git_cl.breakpad, 'post', self._mocked_call)
  69. self.mock(git_cl.breakpad, 'SendStack', self._mocked_call)
  70. self.mock(git_cl.presubmit_support, 'DoPresubmitChecks', PresubmitMock)
  71. self.mock(git_cl.rietveld, 'Rietveld', RietveldMock)
  72. self.mock(git_cl.rietveld, 'CachingRietveld', RietveldMock)
  73. self.mock(git_cl.upload, 'RealMain', self.fail)
  74. self.mock(git_cl.watchlists, 'Watchlists', WatchlistsMock)
  75. # It's important to reset settings to not have inter-tests interference.
  76. git_cl.settings = None
  77. def tearDown(self):
  78. if not self.has_failed():
  79. self.assertEquals([], self.calls)
  80. super(TestGitCl, self).tearDown()
  81. def _mocked_call(self, *args, **_kwargs):
  82. self.assertTrue(
  83. self.calls,
  84. '@%d Expected: <Missing> Actual: %r' % (self._calls_done, args))
  85. expected_args, result = self.calls.pop(0)
  86. # Also logs otherwise it could get caught in a try/finally and be hard to
  87. # diagnose.
  88. if expected_args != args:
  89. msg = '@%d Expected: %r Actual: %r' % (
  90. self._calls_done, expected_args, args)
  91. git_cl.logging.error(msg)
  92. self.fail(msg)
  93. self._calls_done += 1
  94. return result
  95. @classmethod
  96. def _upload_calls(cls, similarity, find_copies, private):
  97. return (cls._git_base_calls(similarity, find_copies) +
  98. cls._git_upload_calls(private))
  99. @classmethod
  100. def _upload_no_rev_calls(cls, similarity, find_copies):
  101. return (cls._git_base_calls(similarity, find_copies) +
  102. cls._git_upload_no_rev_calls())
  103. @classmethod
  104. def _git_base_calls(cls, similarity, find_copies):
  105. if similarity is None:
  106. similarity = '50'
  107. similarity_call = ((['git', 'config', '--int', '--get',
  108. 'branch.master.git-cl-similarity'],), '')
  109. else:
  110. similarity_call = ((['git', 'config', '--int',
  111. 'branch.master.git-cl-similarity', similarity],), '')
  112. if find_copies is None:
  113. find_copies = True
  114. find_copies_call = ((['git', 'config', '--int', '--get',
  115. 'branch.master.git-find-copies'],), '')
  116. else:
  117. val = str(int(find_copies))
  118. find_copies_call = ((['git', 'config', '--int',
  119. 'branch.master.git-find-copies', val],), '')
  120. if find_copies:
  121. stat_call = ((['git', 'diff', '--no-ext-diff', '--stat',
  122. '--find-copies-harder', '-l100000', '-C'+similarity,
  123. 'fake_ancestor_sha', 'HEAD'],), '+dat')
  124. else:
  125. stat_call = ((['git', 'diff', '--no-ext-diff', '--stat',
  126. '-M'+similarity, 'fake_ancestor_sha', 'HEAD'],), '+dat')
  127. return [
  128. ((['git', 'config', 'rietveld.autoupdate'],), ''),
  129. ((['git', 'config', 'rietveld.server'],),
  130. 'codereview.example.com'),
  131. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  132. similarity_call,
  133. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  134. find_copies_call,
  135. ((['git', 'update-index', '--refresh', '-q'],), ''),
  136. ((['git', 'diff-index', '--name-status', 'HEAD'],), ''),
  137. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  138. ((['git', 'config', 'branch.master.merge'],), 'master'),
  139. ((['git', 'config', 'branch.master.remote'],), 'origin'),
  140. ((['get_or_create_merge_base', 'master', 'master'],),
  141. 'fake_ancestor_sha'),
  142. ] + cls._git_sanity_checks('fake_ancestor_sha', 'master') + [
  143. ((['git', 'rev-parse', '--show-cdup'],), ''),
  144. ((['git', 'rev-parse', 'HEAD'],), '12345'),
  145. ((['git', 'diff', '--name-status', '--no-renames', '-r',
  146. 'fake_ancestor_sha...', '.'],),
  147. 'M\t.gitignore\n'),
  148. ((['git', 'config', 'branch.master.rietveldissue'],), ''),
  149. ((['git', 'config', 'branch.master.rietveldpatchset'],),
  150. ''),
  151. ((['git', 'log', '--pretty=format:%s%n%n%b',
  152. 'fake_ancestor_sha...'],),
  153. 'foo'),
  154. ((['git', 'config', 'user.email'],), 'me@example.com'),
  155. stat_call,
  156. ((['git', 'config', 'gerrit.host'],), ''),
  157. ((['git', 'log', '--pretty=format:%s\n\n%b',
  158. 'fake_ancestor_sha..HEAD'],),
  159. 'desc\n'),
  160. ((['git', 'config', 'rietveld.bug-prefix'],), ''),
  161. ]
  162. @classmethod
  163. def _git_upload_no_rev_calls(cls):
  164. return [
  165. ((['git', 'config', 'core.editor'],), ''),
  166. ]
  167. @classmethod
  168. def _git_upload_calls(cls, private):
  169. if private:
  170. cc_call = []
  171. private_call = []
  172. else:
  173. cc_call = [((['git', 'config', 'rietveld.cc'],), '')]
  174. private_call = [
  175. ((['git', 'config', 'rietveld.private'],), '')]
  176. return [
  177. ((['git', 'config', 'core.editor'],), ''),
  178. ] + cc_call + private_call + [
  179. ((['git', 'config', 'branch.master.base-url'],), ''),
  180. ((['git',
  181. 'config', '--local', '--get-regexp', '^svn-remote\\.'],),
  182. (('', None), 0)),
  183. ((['git', 'rev-parse', '--show-cdup'],), ''),
  184. ((['git', 'svn', 'info'],), ''),
  185. ((['git',
  186. 'config', 'branch.master.rietveldissue', '1'],), ''),
  187. ((['git', 'config', 'branch.master.rietveldserver',
  188. 'https://codereview.example.com'],), ''),
  189. ((['git',
  190. 'config', 'branch.master.rietveldpatchset', '2'],), ''),
  191. ((['git', 'rev-parse', 'HEAD'],), 'hash'),
  192. ((['git', 'symbolic-ref', 'HEAD'],), 'hash'),
  193. ((['git',
  194. 'config', 'branch.hash.last-upload-hash', 'hash'],), ''),
  195. ]
  196. @staticmethod
  197. def _git_sanity_checks(diff_base, working_branch):
  198. fake_ancestor = 'fake_ancestor'
  199. fake_cl = 'fake_cl_for_patch'
  200. return [
  201. # Calls to verify branch point is ancestor
  202. ((['git',
  203. 'rev-parse', '--verify', diff_base],), fake_ancestor),
  204. ((['git',
  205. 'merge-base', fake_ancestor, 'HEAD'],), fake_ancestor),
  206. ((['git',
  207. 'rev-list', '^' + fake_ancestor, 'HEAD'],), fake_cl),
  208. # Mock a config miss (error code 1)
  209. ((['git',
  210. 'config', 'gitcl.remotebranch'],), (('', None), 1)),
  211. # Call to GetRemoteBranch()
  212. ((['git',
  213. 'config', 'branch.%s.merge' % working_branch],),
  214. 'refs/heads/master'),
  215. ((['git',
  216. 'config', 'branch.%s.remote' % working_branch],), 'origin'),
  217. ((['git', 'rev-list', '^' + fake_ancestor,
  218. 'refs/remotes/origin/master'],), ''),
  219. ]
  220. @classmethod
  221. def _dcommit_calls_1(cls):
  222. return [
  223. ((['git',
  224. 'config', '--local', '--get-regexp', '^svn-remote\\.'],),
  225. ((('svn-remote.svn.url svn://svn.chromium.org/chrome\n'
  226. 'svn-remote.svn.fetch trunk/src:refs/remotes/origin/master'),
  227. None),
  228. 0)),
  229. ((['git', 'config', 'rietveld.autoupdate'],),
  230. ''),
  231. ((['git',
  232. 'config', 'rietveld.server'],), 'codereview.example.com'),
  233. ((['git', 'symbolic-ref', 'HEAD'],), 'refs/heads/working'),
  234. ((['git', 'config', '--int', '--get',
  235. 'branch.working.git-cl-similarity'],), ''),
  236. ((['git', 'symbolic-ref', 'HEAD'],), 'refs/heads/working'),
  237. ((['git', 'config', '--int', '--get',
  238. 'branch.working.git-find-copies'],), ''),
  239. ((['git', 'symbolic-ref', 'HEAD'],), 'refs/heads/working'),
  240. ((['git',
  241. 'config', 'branch.working.merge'],), 'refs/heads/master'),
  242. ((['git', 'config', 'branch.working.remote'],), 'origin'),
  243. ((['git', 'config', 'branch.working.merge'],),
  244. 'refs/heads/master'),
  245. ((['git', 'config', 'branch.working.remote'],), 'origin'),
  246. ((['git', 'rev-list', '--merges',
  247. '--grep=^SVN changes up to revision [0-9]*$',
  248. 'refs/remotes/origin/master^!'],), ''),
  249. ((['git', 'update-index', '--refresh', '-q'],), ''),
  250. ((['git', 'diff-index', '--name-status', 'HEAD'],), ''),
  251. ((['git', 'rev-list', '^refs/heads/working',
  252. 'refs/remotes/origin/master'],),
  253. ''),
  254. ((['git',
  255. 'log', '--grep=^git-svn-id:', '-1', '--pretty=format:%H'],),
  256. '3fc18b62c4966193eb435baabe2d18a3810ec82e'),
  257. ((['git',
  258. 'rev-list', '^3fc18b62c4966193eb435baabe2d18a3810ec82e',
  259. 'refs/remotes/origin/master'],), ''),
  260. ((['git',
  261. 'merge-base', 'refs/remotes/origin/master', 'HEAD'],),
  262. 'fake_ancestor_sha'),
  263. ]
  264. @classmethod
  265. def _dcommit_calls_normal(cls):
  266. return [
  267. ((['git', 'rev-parse', '--show-cdup'],), ''),
  268. ((['git', 'rev-parse', 'HEAD'],),
  269. '00ff397798ea57439712ed7e04ab96e13969ef40'),
  270. ((['git',
  271. 'diff', '--name-status', '--no-renames', '-r', 'fake_ancestor_sha...',
  272. '.'],),
  273. 'M\tPRESUBMIT.py'),
  274. ((['git',
  275. 'config', 'branch.working.rietveldissue'],), '12345'),
  276. ((['git',
  277. 'config', 'branch.working.rietveldpatchset'],), '31137'),
  278. ((['git', 'config', 'branch.working.rietveldserver'],),
  279. 'codereview.example.com'),
  280. ((['git', 'config', 'user.email'],), 'author@example.com'),
  281. ((['git', 'config', 'rietveld.tree-status-url'],), ''),
  282. ]
  283. @classmethod
  284. def _dcommit_calls_bypassed(cls):
  285. return [
  286. ((['git',
  287. 'config', 'branch.working.rietveldissue'],), '12345'),
  288. ((['git', 'config', 'branch.working.rietveldserver'],),
  289. 'codereview.example.com'),
  290. ((['git', 'config', 'rietveld.tree-status-url'],), ''),
  291. (('GitClHooksBypassedCommit',
  292. 'Issue https://codereview.example.com/12345 bypassed hook when '
  293. 'committing (tree status was "unset")'), None),
  294. ]
  295. @classmethod
  296. def _dcommit_calls_3(cls):
  297. return [
  298. ((['git',
  299. 'diff', '--no-ext-diff', '--stat', '--find-copies-harder',
  300. '-l100000', '-C50', 'fake_ancestor_sha',
  301. 'refs/heads/working'],),
  302. (' PRESUBMIT.py | 2 +-\n'
  303. ' 1 files changed, 1 insertions(+), 1 deletions(-)\n')),
  304. (('About to commit; enter to confirm.',), None),
  305. ((['git', 'show-ref', '--quiet', '--verify',
  306. 'refs/heads/git-cl-commit'],),
  307. (('', None), 0)),
  308. ((['git', 'branch', '-D', 'git-cl-commit'],), ''),
  309. ((['git', 'show-ref', '--quiet', '--verify',
  310. 'refs/heads/git-cl-cherry-pick'],), ''),
  311. ((['git', 'rev-parse', '--show-cdup'],), '\n'),
  312. ((['git', 'checkout', '-q', '-b', 'git-cl-commit'],), ''),
  313. ((['git', 'reset', '--soft', 'fake_ancestor_sha'],), ''),
  314. ((['git', 'commit', '-m',
  315. 'Issue: 12345\n\nR=john@chromium.org\n\n'
  316. 'Review URL: https://codereview.example.com/12345'],),
  317. ''),
  318. ((['git',
  319. 'svn', 'dcommit', '-C50', '--no-rebase', '--rmdir'],),
  320. (('', None), 0)),
  321. ((['git', 'checkout', '-q', 'working'],), ''),
  322. ((['git', 'branch', '-D', 'git-cl-commit'],), ''),
  323. ]
  324. @staticmethod
  325. def _cmd_line(description, args, similarity, find_copies, private):
  326. """Returns the upload command line passed to upload.RealMain()."""
  327. return [
  328. 'upload', '--assume_yes', '--server',
  329. 'https://codereview.example.com',
  330. '--message', description
  331. ] + args + [
  332. '--cc', 'joe@example.com',
  333. ] + (['--private'] if private else []) + [
  334. '--git_similarity', similarity or '50'
  335. ] + (['--git_no_find_copies'] if find_copies == False else []) + [
  336. 'fake_ancestor_sha', 'HEAD'
  337. ]
  338. def _run_reviewer_test(
  339. self,
  340. upload_args,
  341. expected_description,
  342. returned_description,
  343. final_description,
  344. reviewers,
  345. private=False):
  346. """Generic reviewer test framework."""
  347. try:
  348. similarity = upload_args[upload_args.index('--similarity')+1]
  349. except ValueError:
  350. similarity = None
  351. if '--find-copies' in upload_args:
  352. find_copies = True
  353. elif '--no-find-copies' in upload_args:
  354. find_copies = False
  355. else:
  356. find_copies = None
  357. private = '--private' in upload_args
  358. self.calls = self._upload_calls(similarity, find_copies, private)
  359. def RunEditor(desc, _, **kwargs):
  360. self.assertEquals(
  361. '# Enter a description of the change.\n'
  362. '# This will be displayed on the codereview site.\n'
  363. '# The first line will also be used as the subject of the review.\n'
  364. '#--------------------This line is 72 characters long'
  365. '--------------------\n' +
  366. expected_description,
  367. desc)
  368. return returned_description
  369. self.mock(git_cl.gclient_utils, 'RunEditor', RunEditor)
  370. def check_upload(args):
  371. cmd_line = self._cmd_line(final_description, reviewers, similarity,
  372. find_copies, private)
  373. self.assertEquals(cmd_line, args)
  374. return 1, 2
  375. self.mock(git_cl.upload, 'RealMain', check_upload)
  376. git_cl.main(['upload'] + upload_args)
  377. def test_no_reviewer(self):
  378. self._run_reviewer_test(
  379. [],
  380. 'desc\n\nBUG=',
  381. '# Blah blah comment.\ndesc\n\nBUG=',
  382. 'desc\n\nBUG=',
  383. [])
  384. def test_keep_similarity(self):
  385. self._run_reviewer_test(
  386. ['--similarity', '70'],
  387. 'desc\n\nBUG=',
  388. '# Blah blah comment.\ndesc\n\nBUG=',
  389. 'desc\n\nBUG=',
  390. [])
  391. def test_keep_find_copies(self):
  392. self._run_reviewer_test(
  393. ['--no-find-copies'],
  394. 'desc\n\nBUG=',
  395. '# Blah blah comment.\ndesc\n\nBUG=\n',
  396. 'desc\n\nBUG=',
  397. [])
  398. def test_private(self):
  399. self._run_reviewer_test(
  400. ['--private'],
  401. 'desc\n\nBUG=',
  402. '# Blah blah comment.\ndesc\n\nBUG=\n',
  403. 'desc\n\nBUG=',
  404. [])
  405. def test_reviewers_cmd_line(self):
  406. # Reviewer is passed as-is
  407. description = 'desc\n\nR=foo@example.com\nBUG='
  408. self._run_reviewer_test(
  409. ['-r' 'foo@example.com'],
  410. description,
  411. '\n%s\n' % description,
  412. description,
  413. ['--reviewers=foo@example.com'])
  414. def test_reviewer_tbr_overriden(self):
  415. # Reviewer is overriden with TBR
  416. # Also verifies the regexp work without a trailing LF
  417. description = 'Foo Bar\n\nTBR=reviewer@example.com'
  418. self._run_reviewer_test(
  419. ['-r' 'foo@example.com'],
  420. 'desc\n\nR=foo@example.com\nBUG=',
  421. description.strip('\n'),
  422. description,
  423. ['--reviewers=reviewer@example.com'])
  424. def test_reviewer_multiple(self):
  425. # Handles multiple R= or TBR= lines.
  426. description = (
  427. 'Foo Bar\nTBR=reviewer@example.com\nBUG=\nR=another@example.com')
  428. self._run_reviewer_test(
  429. [],
  430. 'desc\n\nBUG=',
  431. description,
  432. description,
  433. ['--reviewers=another@example.com,reviewer@example.com'])
  434. def test_reviewer_send_mail(self):
  435. # --send-mail can be used without -r if R= is used
  436. description = 'Foo Bar\nR=reviewer@example.com'
  437. self._run_reviewer_test(
  438. ['--send-mail'],
  439. 'desc\n\nBUG=',
  440. description.strip('\n'),
  441. description,
  442. ['--reviewers=reviewer@example.com', '--send_mail'])
  443. def test_reviewer_send_mail_no_rev(self):
  444. # Fails without a reviewer.
  445. stdout = StringIO.StringIO()
  446. stderr = StringIO.StringIO()
  447. try:
  448. self.calls = self._upload_no_rev_calls(None, None)
  449. def RunEditor(desc, _, **kwargs):
  450. return desc
  451. self.mock(git_cl.gclient_utils, 'RunEditor', RunEditor)
  452. self.mock(sys, 'stdout', stdout)
  453. self.mock(sys, 'stderr', stderr)
  454. git_cl.main(['upload', '--send-mail'])
  455. self.fail()
  456. except SystemExit:
  457. self.assertEqual(
  458. 'Using 50% similarity for rename/copy detection. Override with '
  459. '--similarity.\n',
  460. stdout.getvalue())
  461. self.assertEqual(
  462. 'Must specify reviewers to send email.\n', stderr.getvalue())
  463. def test_dcommit(self):
  464. self.calls = (
  465. self._dcommit_calls_1() +
  466. self._git_sanity_checks('fake_ancestor_sha', 'working') +
  467. self._dcommit_calls_normal() +
  468. self._dcommit_calls_3())
  469. git_cl.main(['dcommit'])
  470. def test_dcommit_bypass_hooks(self):
  471. self.calls = (
  472. self._dcommit_calls_1() +
  473. self._dcommit_calls_bypassed() +
  474. self._dcommit_calls_3())
  475. git_cl.main(['dcommit', '--bypass-hooks'])
  476. @classmethod
  477. def _gerrit_base_calls(cls):
  478. return [
  479. ((['git', 'config', 'rietveld.autoupdate'],),
  480. ''),
  481. ((['git',
  482. 'config', 'rietveld.server'],), 'codereview.example.com'),
  483. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  484. ((['git', 'config', '--int', '--get',
  485. 'branch.master.git-cl-similarity'],), ''),
  486. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  487. ((['git', 'config', '--int', '--get',
  488. 'branch.master.git-find-copies'],), ''),
  489. ((['git', 'update-index', '--refresh', '-q'],), ''),
  490. ((['git', 'diff-index', '--name-status', 'HEAD'],), ''),
  491. ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
  492. ((['git', 'config', 'branch.master.merge'],), 'master'),
  493. ((['git', 'config', 'branch.master.remote'],), 'origin'),
  494. ((['get_or_create_merge_base', 'master', 'master'],),
  495. 'fake_ancestor_sha'),
  496. ] + cls._git_sanity_checks('fake_ancestor_sha', 'master') + [
  497. ((['git', 'rev-parse', '--show-cdup'],), ''),
  498. ((['git', 'rev-parse', 'HEAD'],), '12345'),
  499. ((['git',
  500. 'diff', '--name-status', '--no-renames', '-r',
  501. 'fake_ancestor_sha...', '.'],),
  502. 'M\t.gitignore\n'),
  503. ((['git', 'config', 'branch.master.rietveldissue'],), ''),
  504. ((['git',
  505. 'config', 'branch.master.rietveldpatchset'],), ''),
  506. ((['git',
  507. 'log', '--pretty=format:%s%n%n%b', 'fake_ancestor_sha...'],),
  508. 'foo'),
  509. ((['git', 'config', 'user.email'],), 'me@example.com'),
  510. ((['git',
  511. 'diff', '--no-ext-diff', '--stat', '--find-copies-harder',
  512. '-l100000', '-C50', 'fake_ancestor_sha', 'HEAD'],),
  513. '+dat'),
  514. ]
  515. @staticmethod
  516. def _gerrit_upload_calls(description, reviewers):
  517. calls = [
  518. ((['git', 'config', 'gerrit.host'],),
  519. 'gerrit.example.com'),
  520. ((['git', 'log', '--pretty=format:%s\n\n%b',
  521. 'fake_ancestor_sha..HEAD'],),
  522. description)
  523. ]
  524. if git_cl.CHANGE_ID not in description:
  525. calls += [
  526. ((['git', 'log', '--pretty=format:%s\n\n%b',
  527. 'fake_ancestor_sha..HEAD'],),
  528. description),
  529. ((['git', 'commit', '--amend', '-m', description],),
  530. ''),
  531. ((['git', 'log', '--pretty=format:%s\n\n%b',
  532. 'fake_ancestor_sha..HEAD'],),
  533. description)
  534. ]
  535. calls += [
  536. ((['git', 'rev-list', 'origin/master...'],), ''),
  537. ((['git', 'config', 'rietveld.cc'],), '')
  538. ]
  539. receive_pack = '--receive-pack=git receive-pack '
  540. receive_pack += '--cc=joe@example.com' # from watch list
  541. if reviewers:
  542. receive_pack += ' '
  543. receive_pack += ' '.join(
  544. '--reviewer=' + email for email in sorted(reviewers))
  545. receive_pack += ''
  546. calls += [
  547. ((['git',
  548. 'push', receive_pack, 'origin', 'HEAD:refs/for/master'],),
  549. '')
  550. ]
  551. return calls
  552. def _run_gerrit_upload_test(
  553. self,
  554. upload_args,
  555. description,
  556. reviewers):
  557. """Generic gerrit upload test framework."""
  558. self.calls = self._gerrit_base_calls()
  559. self.calls += self._gerrit_upload_calls(description, reviewers)
  560. git_cl.main(['upload'] + upload_args)
  561. def test_gerrit_upload_without_change_id(self):
  562. self._run_gerrit_upload_test(
  563. [],
  564. 'desc\n\nBUG=\n',
  565. [])
  566. def test_gerrit_no_reviewer(self):
  567. self._run_gerrit_upload_test(
  568. [],
  569. 'desc\n\nBUG=\nChange-Id:123456789\n',
  570. [])
  571. def test_gerrit_reviewers_cmd_line(self):
  572. self._run_gerrit_upload_test(
  573. ['-r', 'foo@example.com'],
  574. 'desc\n\nBUG=\nChange-Id:123456789',
  575. ['foo@example.com'])
  576. def test_gerrit_reviewer_multiple(self):
  577. self._run_gerrit_upload_test(
  578. [],
  579. 'desc\nTBR=reviewer@example.com\nBUG=\nR=another@example.com\n'
  580. 'Change-Id:123456789\n',
  581. ['reviewer@example.com', 'another@example.com'])
  582. def test_config_gerrit_download_hook(self):
  583. self.mock(git_cl, 'FindCodereviewSettingsFile', CodereviewSettingsFileMock)
  584. def ParseCodereviewSettingsContent(content):
  585. keyvals = {}
  586. keyvals['CODE_REVIEW_SERVER'] = 'gerrit.chromium.org'
  587. keyvals['GERRIT_HOST'] = 'gerrit.chromium.org'
  588. keyvals['GERRIT_PORT'] = '29418'
  589. return keyvals
  590. self.mock(git_cl.gclient_utils, 'ParseCodereviewSettingsContent',
  591. ParseCodereviewSettingsContent)
  592. self.mock(git_cl.os, 'access', self._mocked_call)
  593. self.mock(git_cl.os, 'chmod', self._mocked_call)
  594. src_dir = os.path.join(os.path.sep, 'usr', 'local', 'src')
  595. def AbsPath(path):
  596. if not path.startswith(os.path.sep):
  597. return os.path.join(src_dir, path)
  598. return path
  599. self.mock(git_cl.os.path, 'abspath', AbsPath)
  600. commit_msg_path = os.path.join(src_dir, '.git', 'hooks', 'commit-msg')
  601. def Exists(path):
  602. if path == commit_msg_path:
  603. return False
  604. # others paths, such as /usr/share/locale/....
  605. return True
  606. self.mock(git_cl.os.path, 'exists', Exists)
  607. self.mock(git_cl, 'urlretrieve', self._mocked_call)
  608. self.mock(git_cl, 'hasSheBang', self._mocked_call)
  609. self.calls = [
  610. ((['git', 'config', 'rietveld.autoupdate'],),
  611. ''),
  612. ((['git', 'config', 'rietveld.server',
  613. 'gerrit.chromium.org'],), ''),
  614. ((['git', 'config', '--unset-all', 'rietveld.cc'],), ''),
  615. ((['git', 'config', '--unset-all',
  616. 'rietveld.private'],), ''),
  617. ((['git', 'config', '--unset-all',
  618. 'rietveld.tree-status-url'],), ''),
  619. ((['git', 'config', '--unset-all',
  620. 'rietveld.viewvc-url'],), ''),
  621. ((['git', 'config', '--unset-all',
  622. 'rietveld.bug-prefix'],), ''),
  623. ((['git', 'config', '--unset-all',
  624. 'rietveld.cpplint-regex'],), ''),
  625. ((['git', 'config', '--unset-all',
  626. 'rietveld.cpplint-ignore-regex'],), ''),
  627. ((['git', 'config', 'gerrit.host',
  628. 'gerrit.chromium.org'],), ''),
  629. # DownloadHooks(False)
  630. ((['git', 'config', 'gerrit.host'],),
  631. 'gerrit.chromium.org'),
  632. ((['git', 'rev-parse', '--show-cdup'],), ''),
  633. ((commit_msg_path, os.X_OK,), False),
  634. (('https://gerrit-review.googlesource.com/tools/hooks/commit-msg',
  635. commit_msg_path,), ''),
  636. ((commit_msg_path,), True),
  637. ((commit_msg_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR,), ''),
  638. # GetCodereviewSettingsInteractively
  639. ((['git', 'config', 'rietveld.server'],),
  640. 'gerrit.chromium.org'),
  641. (('Rietveld server (host[:port]) [https://gerrit.chromium.org]:',),
  642. ''),
  643. ((['git', 'config', 'rietveld.cc'],), ''),
  644. (('CC list:',), ''),
  645. ((['git', 'config', 'rietveld.private'],), ''),
  646. (('Private flag (rietveld only):',), ''),
  647. ((['git', 'config', 'rietveld.tree-status-url'],), ''),
  648. (('Tree status URL:',), ''),
  649. ((['git', 'config', 'rietveld.viewvc-url'],), ''),
  650. (('ViewVC URL:',), ''),
  651. # DownloadHooks(True)
  652. ((['git', 'config', 'rietveld.bug-prefix'],), ''),
  653. (('Bug Prefix:',), ''),
  654. ((commit_msg_path, os.X_OK,), True),
  655. ]
  656. git_cl.main(['config'])
  657. def test_update_reviewers(self):
  658. data = [
  659. ('foo', [], 'foo'),
  660. ('foo\nR=xx', [], 'foo\nR=xx'),
  661. ('foo\nTBR=xx', [], 'foo\nTBR=xx'),
  662. ('foo', ['a@c'], 'foo\n\nR=a@c'),
  663. ('foo\nR=xx', ['a@c'], 'foo\n\nR=a@c, xx'),
  664. ('foo\nTBR=xx', ['a@c'], 'foo\n\nR=a@c\nTBR=xx'),
  665. ('foo\nTBR=xx\nR=yy', ['a@c'], 'foo\n\nR=a@c, yy\nTBR=xx'),
  666. ('foo\nBUG=', ['a@c'], 'foo\nBUG=\nR=a@c'),
  667. ('foo\nR=xx\nTBR=yy\nR=bar', ['a@c'], 'foo\n\nR=a@c, xx, bar\nTBR=yy'),
  668. ('foo', ['a@c', 'b@c'], 'foo\n\nR=a@c, b@c'),
  669. ('foo\nBar\n\nR=\nBUG=', ['c@c'], 'foo\nBar\n\nR=c@c\nBUG='),
  670. ('foo\nBar\n\nR=\nBUG=\nR=', ['c@c'], 'foo\nBar\n\nR=c@c\nBUG='),
  671. # Same as the line before, but full of whitespaces.
  672. (
  673. 'foo\nBar\n\n R = \n BUG = \n R = ', ['c@c'],
  674. 'foo\nBar\n\nR=c@c\n BUG =',
  675. ),
  676. # Whitespaces aren't interpreted as new lines.
  677. ('foo BUG=allo R=joe ', ['c@c'], 'foo BUG=allo R=joe\n\nR=c@c'),
  678. ]
  679. expected = [i[2] for i in data]
  680. actual = []
  681. for orig, reviewers, _expected in data:
  682. obj = git_cl.ChangeDescription(orig)
  683. obj.update_reviewers(reviewers)
  684. actual.append(obj.description)
  685. self.assertEqual(expected, actual)
  686. if __name__ == '__main__':
  687. git_cl.logging.basicConfig(
  688. level=git_cl.logging.DEBUG if '-v' in sys.argv else git_cl.logging.ERROR)
  689. unittest.main()