blob: 3dc1b45dee4e0118ef7a6a3f29ffec735b064452 [file] [log] [blame]
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001#!/usr/bin/env python
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Unit tests for git_cl.py."""
7
8import os
maruel@chromium.orga3353652011-11-30 14:26:57 +00009import StringIO
maruel@chromium.orgddd59412011-11-30 14:20:38 +000010import sys
11import unittest
12
13sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
14
15from testing_support.auto_stub import TestCase
16
17import git_cl
18import subprocess2
19
20
maruel@chromium.org2e72bb12012-01-17 15:18:35 +000021class PresubmitMock(object):
22 def __init__(self, *args, **kwargs):
23 self.reviewers = []
24 @staticmethod
25 def should_continue():
26 return True
27
28
29class RietveldMock(object):
30 def __init__(self, *args, **kwargs):
31 pass
32 @staticmethod
33 def get_description(issue):
34 return 'Issue: %d' % issue
35
36
37class WatchlistsMock(object):
38 def __init__(self, _):
39 pass
40 @staticmethod
41 def GetWatchersForPaths(_):
42 return ['joe@example.com']
43
44
maruel@chromium.orgddd59412011-11-30 14:20:38 +000045class TestGitCl(TestCase):
46 def setUp(self):
47 super(TestGitCl, self).setUp()
48 self.calls = []
49 self._calls_done = 0
maruel@chromium.org2e72bb12012-01-17 15:18:35 +000050 self.mock(subprocess2, 'call', self._mocked_call)
51 self.mock(subprocess2, 'check_call', self._mocked_call)
52 self.mock(subprocess2, 'check_output', self._mocked_call)
53 self.mock(subprocess2, 'communicate', self._mocked_call)
54 self.mock(subprocess2, 'Popen', self._mocked_call)
maruel@chromium.orgddd59412011-11-30 14:20:38 +000055 self.mock(git_cl, 'FindCodereviewSettingsFile', lambda: '')
maruel@chromium.org2e72bb12012-01-17 15:18:35 +000056 self.mock(git_cl, 'ask_for_data', self._mocked_call)
57 self.mock(git_cl.breakpad, 'post', self._mocked_call)
58 self.mock(git_cl.breakpad, 'SendStack', self._mocked_call)
maruel@chromium.orgddd59412011-11-30 14:20:38 +000059 self.mock(git_cl.presubmit_support, 'DoPresubmitChecks', PresubmitMock)
maruel@chromium.orgddd59412011-11-30 14:20:38 +000060 self.mock(git_cl.rietveld, 'Rietveld', RietveldMock)
maruel@chromium.orgddd59412011-11-30 14:20:38 +000061 self.mock(git_cl.upload, 'RealMain', self.fail)
maruel@chromium.orgddd59412011-11-30 14:20:38 +000062 self.mock(git_cl.watchlists, 'Watchlists', WatchlistsMock)
63 # It's important to reset settings to not have inter-tests interference.
64 git_cl.settings = None
65
66 def tearDown(self):
67 if not self.has_failed():
68 self.assertEquals([], self.calls)
69 super(TestGitCl, self).tearDown()
70
maruel@chromium.org2e72bb12012-01-17 15:18:35 +000071 def _mocked_call(self, *args, **kwargs):
72 self.assertTrue(
73 self.calls,
74 '@%d Expected: <Missing> Actual: %r' % (self._calls_done, args))
75 expected_args, result = self.calls.pop(0)
76 self.assertEquals(
77 expected_args,
78 args,
79 '@%d Expected: %r Actual: %r' % (
80 self._calls_done, expected_args, args))
81 self._calls_done += 1
82 return result
83
maruel@chromium.orga3353652011-11-30 14:26:57 +000084 @classmethod
85 def _upload_calls(cls):
86 return cls._git_base_calls() + cls._git_upload_calls()
87
maruel@chromium.orgddd59412011-11-30 14:20:38 +000088 @staticmethod
maruel@chromium.orga3353652011-11-30 14:26:57 +000089 def _git_base_calls():
maruel@chromium.orgddd59412011-11-30 14:20:38 +000090 return [
maruel@chromium.org2e72bb12012-01-17 15:18:35 +000091 ((['git', 'update-index', '--refresh', '-q'],), ''),
92 ((['git', 'diff-index', 'HEAD'],), ''),
93 ((['git', 'config', 'rietveld.server'],), 'codereview.example.com'),
94 ((['git', 'symbolic-ref', 'HEAD'],), 'master'),
95 ((['git', 'config', 'branch.master.merge'],), 'master'),
96 ((['git', 'config', 'branch.master.remote'],), 'origin'),
97 ((['git', 'rev-parse', '--show-cdup'],), ''),
98 ((['git', 'rev-parse', 'HEAD'],), '12345'),
99 ((['git', 'diff', '--name-status', '-r', 'master...', '.'],),
100 'M\t.gitignore\n'),
101 ((['git', 'rev-parse', '--git-dir'],), '.git'),
102 ((['git', 'config', 'branch.master.rietveldissue'],), ''),
103 ((['git', 'config', 'branch.master.rietveldpatchset'],), ''),
104 ((['git', 'log', '--pretty=format:%s%n%n%b', 'master...'],), 'foo'),
105 ((['git', 'config', 'user.email'],), 'me@example.com'),
106 ((['git', 'diff', '--no-ext-diff', '--stat', '-M', 'master...'],),
107 '+dat'),
108 ((['git', 'log', '--pretty=format:%s\n\n%b', 'master..'],), 'desc\n'),
maruel@chromium.orga3353652011-11-30 14:26:57 +0000109 ]
110
111 @staticmethod
112 def _git_upload_calls():
113 return [
maruel@chromium.org2e72bb12012-01-17 15:18:35 +0000114 ((['git', 'config', 'rietveld.cc'],), ''),
115 ((['git', 'config', '--get-regexp', '^svn-remote\\.'],),
116 (('', None), 0)),
117 ((['git', 'rev-parse', '--show-cdup'],), ''),
118 ((['git', 'svn', 'info'],), ''),
119 ((['git', 'config', 'branch.master.rietveldissue', '1'],), ''),
120 ((['git', 'config', 'branch.master.rietveldserver',
121 'https://codereview.example.com'],), ''),
122 ((['git', 'config', 'branch.master.rietveldpatchset', '2'],), ''),
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000123 ]
124
maruel@chromium.org2e72bb12012-01-17 15:18:35 +0000125 @classmethod
126 def _dcommit_calls_1(cls):
127 return [
128 ((['git', 'config', '--get-regexp', '^svn-remote\\.'],),
129 ((('svn-remote.svn.url svn://svn.chromium.org/chrome\n'
130 'svn-remote.svn.fetch trunk/src:refs/remotes/origin/master'),
131 None),
132 0)),
133 ((['git', 'config', 'rietveld.server'],), 'codereview.example.com'),
134 ((['git', 'symbolic-ref', 'HEAD'],), 'refs/heads/working'),
135 ((['git', 'config', 'branch.working.merge'],), 'refs/heads/master'),
136 ((['git', 'config', 'branch.working.remote'],), 'origin'),
137 ((['git', 'update-index', '--refresh', '-q'],), ''),
138 ((['git', 'diff-index', 'HEAD'],), ''),
139 ((['git', 'rev-list', '^refs/heads/working',
140 'refs/remotes/origin/master'],),
141 ''),
142 ((['git', 'log', '--grep=^git-svn-id:', '-1', '--pretty=format:%H'],),
143 '3fc18b62c4966193eb435baabe2d18a3810ec82e'),
144 ((['git', 'rev-list', '^3fc18b62c4966193eb435baabe2d18a3810ec82e',
145 'refs/remotes/origin/master'],), ''),
146 ]
147
148 @classmethod
149 def _dcommit_calls_normal(cls):
150 return [
151 ((['git', 'rev-parse', '--show-cdup'],), ''),
152 ((['git', 'rev-parse', 'HEAD'],),
153 '00ff397798ea57439712ed7e04ab96e13969ef40'),
154 ((['git', 'diff', '--name-status', '-r', 'refs/remotes/origin/master...',
155 '.'],),
156 'M\tPRESUBMIT.py'),
157 ((['git', 'rev-parse', '--git-dir'],), '.git'),
158 ((['git', 'config', 'branch.working.rietveldissue'],), '12345'),
159 ((['git', 'config', 'branch.working.rietveldserver'],),
160 'codereview.example.com'),
161 ((['git', 'config', 'branch.working.rietveldpatchset'],), '31137'),
162 ((['git', 'config', 'user.email'],), 'author@example.com'),
163 ((['git', 'config', 'rietveld.tree-status-url'],), ''),
164 ]
165
166 @classmethod
167 def _dcommit_calls_bypassed(cls):
168 return [
169 ((['git', 'rev-parse', '--git-dir'],), '.git'),
170 ((['git', 'config', 'branch.working.rietveldissue'],), '12345'),
171 ((['git', 'config', 'branch.working.rietveldserver'],),
172 'codereview.example.com'),
173 (('GitClHooksBypassedCommit',
174 'Issue https://codereview.example.com/12345 bypassed hook when '
175 'committing'), None),
176 ]
177
178 @classmethod
179 def _dcommit_calls_3(cls):
180 return [
181 ((['git', 'diff', '--stat', 'refs/remotes/origin/master',
182 'refs/heads/working'],),
183 (' PRESUBMIT.py | 2 +-\n'
184 ' 1 files changed, 1 insertions(+), 1 deletions(-)\n')),
185 (('About to commit; enter to confirm.',), None),
186 ((['git', 'show-ref', '--quiet', '--verify',
187 'refs/heads/git-cl-commit'],),
188 (('', None), 0)),
189 ((['git', 'branch', '-D', 'git-cl-commit'],), ''),
190 ((['git', 'rev-parse', '--show-cdup'],), '\n'),
191 ((['git', 'checkout', '-q', '-b', 'git-cl-commit'],), ''),
192 ((['git', 'reset', '--soft', 'refs/remotes/origin/master'],), ''),
193 ((['git', 'commit', '-m',
194 'Issue: 12345\n\nReview URL: https://codereview.example.com/12345'],),
195 ''),
196 ((['git', 'svn', 'dcommit', '--no-rebase', '--rmdir'],), (('', None), 0)),
197 ((['git', 'checkout', '-q', 'working'],), ''),
198 ((['git', 'branch', '-D', 'git-cl-commit'],), ''),
199 ]
200
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000201 @staticmethod
202 def _cmd_line(description, args):
203 """Returns the upload command line passed to upload.RealMain()."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000204 return [
205 'upload', '--assume_yes', '--server',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000206 'https://codereview.example.com',
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000207 '--message', description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000208 ] + args + [
209 '--cc', 'joe@example.com',
210 'master...'
211 ]
212
213 def _run_reviewer_test(
214 self,
215 upload_args,
216 expected_description,
217 returned_description,
218 final_description,
219 reviewers):
220 """Generic reviewer test framework."""
221 self.calls = self._upload_calls()
222 def RunEditor(desc, _):
223 self.assertEquals(
224 '# Enter a description of the change.\n'
225 '# This will displayed on the codereview site.\n'
226 '# The first line will also be used as the subject of the review.\n' +
227 expected_description,
228 desc)
229 return returned_description
230 self.mock(git_cl.gclient_utils, 'RunEditor', RunEditor)
231 def check_upload(args):
232 self.assertEquals(self._cmd_line(final_description, reviewers), args)
233 return 1, 2
234 self.mock(git_cl.upload, 'RealMain', check_upload)
235 git_cl.main(['upload'] + upload_args)
236
237 def test_no_reviewer(self):
238 self._run_reviewer_test(
239 [],
240 'desc\n\nBUG=\nTEST=\n',
241 '# Blah blah comment.\ndesc\n\nBUG=\nTEST=\n',
242 'desc\n\nBUG=\nTEST=\n',
243 [])
244
245 def test_reviewers_cmd_line(self):
246 # Reviewer is passed as-is
247 description = 'desc\n\nR=foo@example.com\nBUG=\nTEST=\n'
248 self._run_reviewer_test(
249 ['-r' 'foo@example.com'],
250 description,
251 '\n%s\n' % description,
252 description,
253 ['--reviewers', 'foo@example.com'])
254
255 def test_reviewer_tbr_overriden(self):
256 # Reviewer is overriden with TBR
257 # Also verifies the regexp work without a trailing LF
258 description = 'Foo Bar\nTBR=reviewer@example.com\n'
259 self._run_reviewer_test(
260 ['-r' 'foo@example.com'],
261 'desc\n\nR=foo@example.com\nBUG=\nTEST=\n',
262 description.strip('\n'),
263 description,
264 ['--reviewers', 'reviewer@example.com'])
265
266 def test_reviewer_multiple(self):
267 # Handles multiple R= or TBR= lines.
268 description = (
269 'Foo Bar\nTBR=reviewer@example.com\nBUG=\nR=another@example.com\n')
270 self._run_reviewer_test(
271 [],
272 'desc\n\nBUG=\nTEST=\n',
273 description,
274 description,
275 ['--reviewers', 'reviewer@example.com,another@example.com'])
276
maruel@chromium.orga3353652011-11-30 14:26:57 +0000277 def test_reviewer_send_mail(self):
278 # --send-mail can be used without -r if R= is used
279 description = 'Foo Bar\nR=reviewer@example.com\n'
280 self._run_reviewer_test(
281 ['--send-mail'],
282 'desc\n\nBUG=\nTEST=\n',
283 description.strip('\n'),
284 description,
285 ['--reviewers', 'reviewer@example.com', '--send_mail'])
286
287 def test_reviewer_send_mail_no_rev(self):
288 # Fails without a reviewer.
289 class FileMock(object):
290 buf = StringIO.StringIO()
291 def write(self, content):
292 self.buf.write(content)
293
294 mock = FileMock()
295 try:
296 self.calls = self._git_base_calls()
297 def RunEditor(desc, _):
298 return desc
299 self.mock(git_cl.gclient_utils, 'RunEditor', RunEditor)
300 self.mock(sys, 'stderr', mock)
301 git_cl.main(['upload', '--send-mail'])
302 self.fail()
303 except SystemExit:
304 self.assertEquals(
305 'Must specify reviewers to send email.\n', mock.buf.getvalue())
306
maruel@chromium.org2e72bb12012-01-17 15:18:35 +0000307 def test_dcommit(self):
308 self.calls = (
309 self._dcommit_calls_1() +
310 self._dcommit_calls_normal() +
311 self._dcommit_calls_3())
312 git_cl.main(['dcommit'])
313
314 def test_dcommit_bypass_hooks(self):
315 self.calls = (
316 self._dcommit_calls_1() +
317 self._dcommit_calls_bypassed() +
318 self._dcommit_calls_3())
319 git_cl.main(['dcommit', '--bypass-hooks'])
320
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000321
322if __name__ == '__main__':
323 unittest.main()