blob: 5707c7be66704f70f7cc3ccb981e4ed23bb5b20f [file] [log] [blame]
[email protected]dfaecd22011-04-21 00:33:311#!/usr/bin/env python
2# Copyright (c) 2011 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
6"""Unit tests for checkout.py."""
7
8from __future__ import with_statement
9import logging
10import os
11import shutil
12import sys
13import unittest
14from xml.etree import ElementTree
15
16ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
17BASE_DIR = os.path.join(ROOT_DIR, '..')
18sys.path.insert(0, BASE_DIR)
19
20import checkout
21import patch
22import subprocess2
23from tests import fake_repos
24
25
26# pass -v to enable it.
27DEBUGGING = False
28
29# A naked patch.
30NAKED_PATCH = ("""\
31--- svn_utils_test.txt
32+++ svn_utils_test.txt
33@@ -3,6 +3,7 @@ bb
34 ccc
35 dd
36 e
37+FOO!
38 ff
39 ggg
40 hh
41""")
42
43# A patch generated from git.
44GIT_PATCH = ("""\
45diff --git a/svn_utils_test.txt b/svn_utils_test.txt
46index 0e4de76..8320059 100644
47--- a/svn_utils_test.txt
48+++ b/svn_utils_test.txt
49@@ -3,6 +3,7 @@ bb
50 ccc
51 dd
52 e
53+FOO!
54 ff
55 ggg
56 hh
57""")
58
59# A patch that will fail to apply.
60BAD_PATCH = ("""\
61diff --git a/svn_utils_test.txt b/svn_utils_test.txt
62index 0e4de76..8320059 100644
63--- a/svn_utils_test.txt
64+++ b/svn_utils_test.txt
65@@ -3,7 +3,8 @@ bb
66 ccc
67 dd
68+FOO!
69 ff
70 ggg
71 hh
72""")
73
74PATCH_ADD = ("""\
75diff --git a/new_dir/subdir/new_file b/new_dir/subdir/new_file
76new file mode 100644
77--- /dev/null
78+++ b/new_dir/subdir/new_file
79@@ -0,0 +1,2 @@
80+A new file
81+should exist.
82""")
83
84
85class FakeRepos(fake_repos.FakeReposBase):
86 def populateSvn(self):
87 """Creates a few revisions of changes files."""
88 subprocess2.check_call(
89 ['svn', 'checkout', self.svn_base, self.svn_checkout, '-q',
90 '--non-interactive', '--no-auth-cache',
91 '--username', self.USERS[0][0], '--password', self.USERS[0][1]])
92 assert os.path.isdir(os.path.join(self.svn_checkout, '.svn'))
93 fs = {}
94 fs['trunk/origin'] = 'svn@1'
95 fs['trunk/codereview.settings'] = (
96 '# Test data\n'
97 'bar: pouet\n')
98 fs['trunk/svn_utils_test.txt'] = (
99 'a\n'
100 'bb\n'
101 'ccc\n'
102 'dd\n'
103 'e\n'
104 'ff\n'
105 'ggg\n'
106 'hh\n'
107 'i\n'
108 'jj\n'
109 'kkk\n'
110 'll\n'
111 'm\n'
112 'nn\n'
113 'ooo\n'
114 'pp\n'
115 'q\n')
116 self._commit_svn(fs)
117 fs['trunk/origin'] = 'svn@2\n'
118 fs['trunk/extra'] = 'dummy\n'
119 fs['trunk/bin_file'] = '\x00'
120 self._commit_svn(fs)
121
122 def populateGit(self):
123 raise NotImplementedError()
124
125
126# pylint: disable=R0201
127class BaseTest(fake_repos.FakeReposTestBase):
128 name = 'foo'
129 FAKE_REPOS_CLASS = FakeRepos
130
131 def setUp(self):
132 # Need to enforce subversion_config first.
133 checkout.SvnMixIn.svn_config_dir = os.path.join(
134 ROOT_DIR, 'subversion_config')
135 super(BaseTest, self).setUp()
136 self._old_call = subprocess2.call
137 def redirect_call(args, **kwargs):
138 if not DEBUGGING:
139 kwargs.setdefault('stdout', subprocess2.PIPE)
140 kwargs.setdefault('stderr', subprocess2.STDOUT)
141 return self._old_call(args, **kwargs)
142 subprocess2.call = redirect_call
143 self.usr, self.pwd = self.FAKE_REPOS.USERS[0]
144 self.previous_log = None
145
146 def tearDown(self):
147 subprocess2.call = self._old_call
148 super(BaseTest, self).tearDown()
149
150 def get_patches(self):
151 return patch.PatchSet([
152 patch.FilePatchDiff(
153 'svn_utils_test.txt', GIT_PATCH, []),
154 patch.FilePatchBinary('bin_file', '\x00', []),
155 patch.FilePatchDelete('extra', False),
156 patch.FilePatchDiff('new_dir/subdir/new_file', PATCH_ADD, []),
157 ])
158
159 def get_trunk(self, modified):
160 tree = {}
161 subroot = 'trunk/'
162 for k, v in self.FAKE_REPOS.svn_revs[-1].iteritems():
163 if k.startswith(subroot):
164 f = k[len(subroot):]
165 assert f not in tree
166 tree[f] = v
167
168 if modified:
169 content_lines = tree['svn_utils_test.txt'].splitlines(True)
170 tree['svn_utils_test.txt'] = ''.join(
171 content_lines[0:5] + ['FOO!\n'] + content_lines[5:])
172 del tree['extra']
173 tree['new_dir/subdir/new_file'] = 'A new file\nshould exist.\n'
174 return tree
175
176 def _check_base(self, co, root, git, expected):
177 read_only = isinstance(co, checkout.ReadOnlyCheckout)
178 assert not read_only == bool(expected)
179 if not read_only:
180 self.FAKE_REPOS.svn_dirty = True
181
182 self.assertEquals(root, co.project_path)
183 self.assertEquals(self.previous_log['revision'], co.prepare())
184 self.assertEquals('pouet', co.get_settings('bar'))
185 self.assertTree(self.get_trunk(False), root)
186 patches = self.get_patches()
187 co.apply_patch(patches)
188 self.assertEquals(
189 ['bin_file', 'extra', 'new_dir/subdir/new_file', 'svn_utils_test.txt'],
190 sorted(patches.filenames))
191
192 if git:
193 # Hackish to verify _branches() internal function.
194 # pylint: disable=W0212
195 self.assertEquals(
196 (['master', 'working_branch'], 'working_branch'),
197 co.checkout._branches())
198
199 # Verify that the patch is applied even for read only checkout.
200 self.assertTree(self.get_trunk(True), root)
201 fake_author = self.FAKE_REPOS.USERS[1][0]
[email protected]1bf50972011-05-05 19:57:21202 revision = co.commit(u'msg', fake_author)
[email protected]dfaecd22011-04-21 00:33:31203 # Nothing changed.
204 self.assertTree(self.get_trunk(True), root)
205
206 if read_only:
207 self.assertEquals('FAKE', revision)
208 self.assertEquals(self.previous_log['revision'], co.prepare())
209 # Changes should be reverted now.
210 self.assertTree(self.get_trunk(False), root)
211 expected = self.previous_log
212 else:
213 self.assertEquals(self.previous_log['revision'] + 1, revision)
214 self.assertEquals(self.previous_log['revision'] + 1, co.prepare())
215 self.assertTree(self.get_trunk(True), root)
216 expected = expected.copy()
217 expected['msg'] = 'msg'
218 expected['revision'] = self.previous_log['revision'] + 1
219 expected.setdefault('author', fake_author)
220
221 actual = self._log()
222 self.assertEquals(expected, actual)
223
224 def _check_exception(self, co, err_msg):
225 co.prepare()
226 try:
227 co.apply_patch([patch.FilePatchDiff('svn_utils_test.txt', BAD_PATCH, [])])
228 self.fail()
229 except checkout.PatchApplicationFailed, e:
230 self.assertEquals(e.filename, 'svn_utils_test.txt')
231 self.assertEquals(e.status, err_msg)
232
233 def _log(self):
234 raise NotImplementedError()
235
[email protected]8a1396c2011-04-22 00:14:24236 def _test_process(self, co):
237 """Makes sure the process lambda is called correctly."""
238 co.prepare()
239 ps = self.get_patches()
240 results = []
241 co.apply_patch(ps, [lambda *args: results.append(args)])
242 expected = [(co, p) for p in ps.patches]
243 self.assertEquals(expected, results)
244
[email protected]dfaecd22011-04-21 00:33:31245
246class SvnBaseTest(BaseTest):
247 def setUp(self):
248 super(SvnBaseTest, self).setUp()
249 self.enabled = self.FAKE_REPOS.set_up_svn()
250 self.assertTrue(self.enabled)
251 self.svn_trunk = 'trunk'
252 self.svn_url = self.svn_base + self.svn_trunk
253 self.previous_log = self._log()
254
255 def _log(self):
256 # Don't use the local checkout in case of caching incorrency.
257 out = subprocess2.check_output(
258 ['svn', 'log', self.svn_url,
259 '--non-interactive', '--no-auth-cache',
260 '--username', self.usr, '--password', self.pwd,
261 '--with-all-revprops', '--xml',
262 '--limit', '1'])
263 logentry = ElementTree.XML(out).find('logentry')
264 if logentry == None:
265 return {'revision': 0}
266 data = {
267 'revision': int(logentry.attrib['revision']),
268 }
269 def set_item(name):
270 item = logentry.find(name)
271 if item != None:
272 data[name] = item.text
273 set_item('author')
274 set_item('msg')
275 revprops = logentry.find('revprops')
276 if revprops != None:
277 data['revprops'] = []
278 for prop in revprops.getiterator('property'):
279 data['revprops'].append((prop.attrib['name'], prop.text))
280 return data
281
282
283class SvnCheckout(SvnBaseTest):
284 def _get_co(self, read_only):
285 if read_only:
286 return checkout.ReadOnlyCheckout(
287 checkout.SvnCheckout(
288 self.root_dir, self.name, None, None, self.svn_url))
289 else:
290 return checkout.SvnCheckout(
291 self.root_dir, self.name, self.usr, self.pwd, self.svn_url)
292
293 def _check(self, read_only, expected):
294 root = os.path.join(self.root_dir, self.name)
295 self._check_base(self._get_co(read_only), root, False, expected)
296
297 def testAllRW(self):
298 expected = {
299 'author': self.FAKE_REPOS.USERS[0][0],
300 'revprops': [('realauthor', self.FAKE_REPOS.USERS[1][0])]
301 }
302 self._check(False, expected)
303
304 def testAllRO(self):
305 self._check(True, None)
306
307 def testException(self):
308 self._check_exception(
309 self._get_co(True),
[email protected]9842a0c2011-05-30 20:41:54310 'While running patch -p1 --forward --force;\n'
[email protected]dfaecd22011-04-21 00:33:31311 'patching file svn_utils_test.txt\n'
312 'Hunk #1 FAILED at 3.\n'
313 '1 out of 1 hunk FAILED -- saving rejects to file '
314 'svn_utils_test.txt.rej\n')
315
316 def testSvnProps(self):
317 co = self._get_co(False)
318 co.prepare()
319 try:
320 # svn:ignore can only be applied to directories.
321 svn_props = [('svn:ignore', 'foo')]
322 co.apply_patch(
323 [patch.FilePatchDiff('svn_utils_test.txt', NAKED_PATCH, svn_props)])
324 self.fail()
325 except checkout.PatchApplicationFailed, e:
326 self.assertEquals(e.filename, 'svn_utils_test.txt')
327 self.assertEquals(
328 e.status,
[email protected]9842a0c2011-05-30 20:41:54329 'While running svn propset svn:ignore foo svn_utils_test.txt '
330 '--non-interactive;\n'
331 'patching file svn_utils_test.txt\n'
332 'svn: Cannot set \'svn:ignore\' on a file (\'svn_utils_test.txt\')\n')
[email protected]dfaecd22011-04-21 00:33:31333 co.prepare()
334 svn_props = [('svn:eol-style', 'LF'), ('foo', 'bar')]
335 co.apply_patch(
336 [patch.FilePatchDiff('svn_utils_test.txt', NAKED_PATCH, svn_props)])
337 filepath = os.path.join(self.root_dir, self.name, 'svn_utils_test.txt')
338 # Manually verify the properties.
339 props = subprocess2.check_output(
340 ['svn', 'proplist', filepath],
341 cwd=self.root_dir).splitlines()[1:]
342 props = sorted(p.strip() for p in props)
343 expected_props = dict(svn_props)
344 self.assertEquals(sorted(expected_props.iterkeys()), props)
345 for k, v in expected_props.iteritems():
346 value = subprocess2.check_output(
347 ['svn', 'propget', '--strict', k, filepath],
348 cwd=self.root_dir).strip()
349 self.assertEquals(v, value)
350
351 def testWithRevPropsSupport(self):
352 # Add the hook that will commit in a way that removes the race condition.
353 hook = os.path.join(self.FAKE_REPOS.svn_repo, 'hooks', 'pre-commit')
354 shutil.copyfile(os.path.join(ROOT_DIR, 'sample_pre_commit_hook'), hook)
355 os.chmod(hook, 0755)
356 expected = {
357 'revprops': [('commit-bot', '[email protected]')],
358 }
359 self._check(False, expected)
360
361 def testWithRevPropsSupportNotCommitBot(self):
362 # Add the hook that will commit in a way that removes the race condition.
363 hook = os.path.join(self.FAKE_REPOS.svn_repo, 'hooks', 'pre-commit')
364 shutil.copyfile(os.path.join(ROOT_DIR, 'sample_pre_commit_hook'), hook)
365 os.chmod(hook, 0755)
366 co = checkout.SvnCheckout(
367 self.root_dir, self.name,
368 self.FAKE_REPOS.USERS[1][0], self.FAKE_REPOS.USERS[1][1],
369 self.svn_url)
370 root = os.path.join(self.root_dir, self.name)
371 expected = {
372 'author': self.FAKE_REPOS.USERS[1][0],
373 }
374 self._check_base(co, root, False, expected)
375
376 def testAutoProps(self):
377 co = self._get_co(False)
378 co.svn_config = checkout.SvnConfig(
379 os.path.join(ROOT_DIR, 'subversion_config'))
380 co.prepare()
381 patches = self.get_patches()
382 co.apply_patch(patches)
383 self.assertEquals(
384 ['bin_file', 'extra', 'new_dir/subdir/new_file', 'svn_utils_test.txt'],
385 sorted(patches.filenames))
386 # *.txt = svn:eol-style=LF in subversion_config/config.
387 out = subprocess2.check_output(
388 ['svn', 'pget', 'svn:eol-style', 'svn_utils_test.txt'],
389 cwd=co.project_path)
390 self.assertEquals('LF\n', out)
391
[email protected]8a1396c2011-04-22 00:14:24392 def testProcess(self):
393 co = checkout.SvnCheckout(
394 self.root_dir, self.name,
395 None, None,
396 self.svn_url)
397 self._test_process(co)
398
[email protected]dfaecd22011-04-21 00:33:31399
400class GitSvnCheckout(SvnBaseTest):
401 name = 'foo.git'
402
403 def _get_co(self, read_only):
404 co = checkout.GitSvnCheckout(
405 self.root_dir, self.name[:-4],
406 self.usr, self.pwd,
407 self.svn_base, self.svn_trunk)
408 if read_only:
409 co = checkout.ReadOnlyCheckout(co)
410 else:
411 # Hack to simplify testing.
412 co.checkout = co
413 return co
414
415 def _check(self, read_only, expected):
416 root = os.path.join(self.root_dir, self.name)
417 self._check_base(self._get_co(read_only), root, True, expected)
418
419 def testAllRO(self):
420 self._check(True, None)
421
422 def testAllRW(self):
423 expected = {
424 'author': self.FAKE_REPOS.USERS[0][0],
425 }
426 self._check(False, expected)
427
428 def testGitSvnPremade(self):
429 # Test premade git-svn clone. First make a git-svn clone.
430 git_svn_co = self._get_co(True)
431 revision = git_svn_co.prepare()
432 self.assertEquals(self.previous_log['revision'], revision)
433 # Then use GitSvnClone to clone it to lose the git-svn connection and verify
434 # git svn init / git svn fetch works.
435 git_svn_clone = checkout.GitSvnPremadeCheckout(
436 self.root_dir, self.name[:-4] + '2', 'trunk',
437 self.usr, self.pwd,
438 self.svn_base, self.svn_trunk, git_svn_co.project_path)
439 self.assertEquals(self.previous_log['revision'], git_svn_clone.prepare())
440
441 def testException(self):
442 self._check_exception(
443 self._get_co(True), 'fatal: corrupt patch at line 12\n')
444
445 def testSvnProps(self):
446 co = self._get_co(False)
447 co.prepare()
448 try:
449 svn_props = [('foo', 'bar')]
450 co.apply_patch(
451 [patch.FilePatchDiff('svn_utils_test.txt', NAKED_PATCH, svn_props)])
452 self.fail()
453 except patch.UnsupportedPatchFormat, e:
454 self.assertEquals(e.filename, 'svn_utils_test.txt')
455 self.assertEquals(
456 e.status,
457 'Cannot apply svn property foo to file svn_utils_test.txt.')
458 co.prepare()
459 # svn:eol-style is ignored.
460 svn_props = [('svn:eol-style', 'LF')]
461 co.apply_patch(
462 [patch.FilePatchDiff('svn_utils_test.txt', NAKED_PATCH, svn_props)])
463
[email protected]8a1396c2011-04-22 00:14:24464 def testProcess(self):
465 co = checkout.SvnCheckout(
466 self.root_dir, self.name,
467 None, None,
468 self.svn_url)
469 self._test_process(co)
470
[email protected]dfaecd22011-04-21 00:33:31471
472class RawCheckout(SvnBaseTest):
473 def setUp(self):
474 super(RawCheckout, self).setUp()
475 # Use a svn checkout as the base.
476 self.base_co = checkout.SvnCheckout(
477 self.root_dir, self.name, None, None, self.svn_url)
478 self.base_co.prepare()
479
480 def _get_co(self, read_only):
481 co = checkout.RawCheckout(self.root_dir, self.name)
482 if read_only:
483 return checkout.ReadOnlyCheckout(co)
484 return co
485
486 def _check(self, read_only):
487 root = os.path.join(self.root_dir, self.name)
488 co = self._get_co(read_only)
489
490 # A copy of BaseTest._check_base()
491 self.assertEquals(root, co.project_path)
492 self.assertEquals(None, co.prepare())
493 self.assertEquals('pouet', co.get_settings('bar'))
494 self.assertTree(self.get_trunk(False), root)
495 patches = self.get_patches()
496 co.apply_patch(patches)
497 self.assertEquals(
498 ['bin_file', 'extra', 'new_dir/subdir/new_file', 'svn_utils_test.txt'],
499 sorted(patches.filenames))
500
501 # Verify that the patch is applied even for read only checkout.
502 self.assertTree(self.get_trunk(True), root)
503 if read_only:
[email protected]1bf50972011-05-05 19:57:21504 revision = co.commit(u'msg', self.FAKE_REPOS.USERS[1][0])
[email protected]dfaecd22011-04-21 00:33:31505 self.assertEquals('FAKE', revision)
506 else:
507 try:
[email protected]1bf50972011-05-05 19:57:21508 co.commit(u'msg', self.FAKE_REPOS.USERS[1][0])
[email protected]dfaecd22011-04-21 00:33:31509 self.fail()
510 except NotImplementedError:
511 pass
512 self.assertTree(self.get_trunk(True), root)
513 # Verify that prepare() is a no-op.
514 self.assertEquals(None, co.prepare())
515 self.assertTree(self.get_trunk(True), root)
516
517 def testAllRW(self):
518 self._check(False)
519
520 def testAllRO(self):
521 self._check(True)
522
523 def testException(self):
524 self._check_exception(
525 self._get_co(True),
526 'patching file svn_utils_test.txt\n'
527 'Hunk #1 FAILED at 3.\n'
528 '1 out of 1 hunk FAILED -- saving rejects to file '
529 'svn_utils_test.txt.rej\n')
530
[email protected]8a1396c2011-04-22 00:14:24531 def testProcess(self):
532 co = checkout.SvnCheckout(
533 self.root_dir, self.name,
534 None, None,
535 self.svn_url)
536 self._test_process(co)
537
[email protected]dfaecd22011-04-21 00:33:31538
539if __name__ == '__main__':
540 if '-v' in sys.argv:
541 DEBUGGING = True
542 logging.basicConfig(
543 level=logging.DEBUG,
544 format='%(levelname)5s %(filename)15s(%(lineno)3d): %(message)s')
545 else:
546 logging.basicConfig(
547 level=logging.ERROR,
548 format='%(levelname)5s %(filename)15s(%(lineno)3d): %(message)s')
549 unittest.main()