OLD | NEW |
(Empty) | |
| 1 #!/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 |
| 8 from __future__ import with_statement |
| 9 import logging |
| 10 import os |
| 11 import shutil |
| 12 import sys |
| 13 import unittest |
| 14 from xml.etree import ElementTree |
| 15 |
| 16 ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) |
| 17 BASE_DIR = os.path.join(ROOT_DIR, '..') |
| 18 sys.path.insert(0, BASE_DIR) |
| 19 |
| 20 import checkout |
| 21 import patch |
| 22 import subprocess2 |
| 23 from tests import fake_repos |
| 24 |
| 25 |
| 26 # pass -v to enable it. |
| 27 DEBUGGING = False |
| 28 |
| 29 # A naked patch. |
| 30 NAKED_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. |
| 44 GIT_PATCH = ("""\ |
| 45 diff --git a/svn_utils_test.txt b/svn_utils_test.txt |
| 46 index 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. |
| 60 BAD_PATCH = ("""\ |
| 61 diff --git a/svn_utils_test.txt b/svn_utils_test.txt |
| 62 index 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 |
| 74 PATCH_ADD = ("""\ |
| 75 diff --git a/new_dir/subdir/new_file b/new_dir/subdir/new_file |
| 76 new 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 |
| 85 class 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 |
| 127 class 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] |
| 202 revision = co.commit('msg', fake_author) |
| 203 # 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 |
| 236 |
| 237 class SvnBaseTest(BaseTest): |
| 238 def setUp(self): |
| 239 super(SvnBaseTest, self).setUp() |
| 240 self.enabled = self.FAKE_REPOS.set_up_svn() |
| 241 self.assertTrue(self.enabled) |
| 242 self.svn_trunk = 'trunk' |
| 243 self.svn_url = self.svn_base + self.svn_trunk |
| 244 self.previous_log = self._log() |
| 245 |
| 246 def _log(self): |
| 247 # Don't use the local checkout in case of caching incorrency. |
| 248 out = subprocess2.check_output( |
| 249 ['svn', 'log', self.svn_url, |
| 250 '--non-interactive', '--no-auth-cache', |
| 251 '--username', self.usr, '--password', self.pwd, |
| 252 '--with-all-revprops', '--xml', |
| 253 '--limit', '1']) |
| 254 logentry = ElementTree.XML(out).find('logentry') |
| 255 if logentry == None: |
| 256 return {'revision': 0} |
| 257 data = { |
| 258 'revision': int(logentry.attrib['revision']), |
| 259 } |
| 260 def set_item(name): |
| 261 item = logentry.find(name) |
| 262 if item != None: |
| 263 data[name] = item.text |
| 264 set_item('author') |
| 265 set_item('msg') |
| 266 revprops = logentry.find('revprops') |
| 267 if revprops != None: |
| 268 data['revprops'] = [] |
| 269 for prop in revprops.getiterator('property'): |
| 270 data['revprops'].append((prop.attrib['name'], prop.text)) |
| 271 return data |
| 272 |
| 273 |
| 274 class SvnCheckout(SvnBaseTest): |
| 275 def _get_co(self, read_only): |
| 276 if read_only: |
| 277 return checkout.ReadOnlyCheckout( |
| 278 checkout.SvnCheckout( |
| 279 self.root_dir, self.name, None, None, self.svn_url)) |
| 280 else: |
| 281 return checkout.SvnCheckout( |
| 282 self.root_dir, self.name, self.usr, self.pwd, self.svn_url) |
| 283 |
| 284 def _check(self, read_only, expected): |
| 285 root = os.path.join(self.root_dir, self.name) |
| 286 self._check_base(self._get_co(read_only), root, False, expected) |
| 287 |
| 288 def testAllRW(self): |
| 289 expected = { |
| 290 'author': self.FAKE_REPOS.USERS[0][0], |
| 291 'revprops': [('realauthor', self.FAKE_REPOS.USERS[1][0])] |
| 292 } |
| 293 self._check(False, expected) |
| 294 |
| 295 def testAllRO(self): |
| 296 self._check(True, None) |
| 297 |
| 298 def testException(self): |
| 299 self._check_exception( |
| 300 self._get_co(True), |
| 301 'patching file svn_utils_test.txt\n' |
| 302 'Hunk #1 FAILED at 3.\n' |
| 303 '1 out of 1 hunk FAILED -- saving rejects to file ' |
| 304 'svn_utils_test.txt.rej\n') |
| 305 |
| 306 def testSvnProps(self): |
| 307 co = self._get_co(False) |
| 308 co.prepare() |
| 309 try: |
| 310 # svn:ignore can only be applied to directories. |
| 311 svn_props = [('svn:ignore', 'foo')] |
| 312 co.apply_patch( |
| 313 [patch.FilePatchDiff('svn_utils_test.txt', NAKED_PATCH, svn_props)]) |
| 314 self.fail() |
| 315 except checkout.PatchApplicationFailed, e: |
| 316 self.assertEquals(e.filename, 'svn_utils_test.txt') |
| 317 self.assertEquals( |
| 318 e.status, |
| 319 "patching file svn_utils_test.txt\n" |
| 320 "svn: Cannot set 'svn:ignore' on a file ('svn_utils_test.txt')\n") |
| 321 co.prepare() |
| 322 svn_props = [('svn:eol-style', 'LF'), ('foo', 'bar')] |
| 323 co.apply_patch( |
| 324 [patch.FilePatchDiff('svn_utils_test.txt', NAKED_PATCH, svn_props)]) |
| 325 filepath = os.path.join(self.root_dir, self.name, 'svn_utils_test.txt') |
| 326 # Manually verify the properties. |
| 327 props = subprocess2.check_output( |
| 328 ['svn', 'proplist', filepath], |
| 329 cwd=self.root_dir).splitlines()[1:] |
| 330 props = sorted(p.strip() for p in props) |
| 331 expected_props = dict(svn_props) |
| 332 self.assertEquals(sorted(expected_props.iterkeys()), props) |
| 333 for k, v in expected_props.iteritems(): |
| 334 value = subprocess2.check_output( |
| 335 ['svn', 'propget', '--strict', k, filepath], |
| 336 cwd=self.root_dir).strip() |
| 337 self.assertEquals(v, value) |
| 338 |
| 339 def testWithRevPropsSupport(self): |
| 340 # Add the hook that will commit in a way that removes the race condition. |
| 341 hook = os.path.join(self.FAKE_REPOS.svn_repo, 'hooks', 'pre-commit') |
| 342 shutil.copyfile(os.path.join(ROOT_DIR, 'sample_pre_commit_hook'), hook) |
| 343 os.chmod(hook, 0755) |
| 344 expected = { |
| 345 'revprops': [('commit-bot', 'user1@example.com')], |
| 346 } |
| 347 self._check(False, expected) |
| 348 |
| 349 def testWithRevPropsSupportNotCommitBot(self): |
| 350 # Add the hook that will commit in a way that removes the race condition. |
| 351 hook = os.path.join(self.FAKE_REPOS.svn_repo, 'hooks', 'pre-commit') |
| 352 shutil.copyfile(os.path.join(ROOT_DIR, 'sample_pre_commit_hook'), hook) |
| 353 os.chmod(hook, 0755) |
| 354 co = checkout.SvnCheckout( |
| 355 self.root_dir, self.name, |
| 356 self.FAKE_REPOS.USERS[1][0], self.FAKE_REPOS.USERS[1][1], |
| 357 self.svn_url) |
| 358 root = os.path.join(self.root_dir, self.name) |
| 359 expected = { |
| 360 'author': self.FAKE_REPOS.USERS[1][0], |
| 361 } |
| 362 self._check_base(co, root, False, expected) |
| 363 |
| 364 def testAutoProps(self): |
| 365 co = self._get_co(False) |
| 366 co.svn_config = checkout.SvnConfig( |
| 367 os.path.join(ROOT_DIR, 'subversion_config')) |
| 368 co.prepare() |
| 369 patches = self.get_patches() |
| 370 co.apply_patch(patches) |
| 371 self.assertEquals( |
| 372 ['bin_file', 'extra', 'new_dir/subdir/new_file', 'svn_utils_test.txt'], |
| 373 sorted(patches.filenames)) |
| 374 # *.txt = svn:eol-style=LF in subversion_config/config. |
| 375 out = subprocess2.check_output( |
| 376 ['svn', 'pget', 'svn:eol-style', 'svn_utils_test.txt'], |
| 377 cwd=co.project_path) |
| 378 self.assertEquals('LF\n', out) |
| 379 |
| 380 |
| 381 class GitSvnCheckout(SvnBaseTest): |
| 382 name = 'foo.git' |
| 383 |
| 384 def _get_co(self, read_only): |
| 385 co = checkout.GitSvnCheckout( |
| 386 self.root_dir, self.name[:-4], |
| 387 self.usr, self.pwd, |
| 388 self.svn_base, self.svn_trunk) |
| 389 if read_only: |
| 390 co = checkout.ReadOnlyCheckout(co) |
| 391 else: |
| 392 # Hack to simplify testing. |
| 393 co.checkout = co |
| 394 return co |
| 395 |
| 396 def _check(self, read_only, expected): |
| 397 root = os.path.join(self.root_dir, self.name) |
| 398 self._check_base(self._get_co(read_only), root, True, expected) |
| 399 |
| 400 def testAllRO(self): |
| 401 self._check(True, None) |
| 402 |
| 403 def testAllRW(self): |
| 404 expected = { |
| 405 'author': self.FAKE_REPOS.USERS[0][0], |
| 406 } |
| 407 self._check(False, expected) |
| 408 |
| 409 def testGitSvnPremade(self): |
| 410 # Test premade git-svn clone. First make a git-svn clone. |
| 411 git_svn_co = self._get_co(True) |
| 412 revision = git_svn_co.prepare() |
| 413 self.assertEquals(self.previous_log['revision'], revision) |
| 414 # Then use GitSvnClone to clone it to lose the git-svn connection and verify |
| 415 # git svn init / git svn fetch works. |
| 416 git_svn_clone = checkout.GitSvnPremadeCheckout( |
| 417 self.root_dir, self.name[:-4] + '2', 'trunk', |
| 418 self.usr, self.pwd, |
| 419 self.svn_base, self.svn_trunk, git_svn_co.project_path) |
| 420 self.assertEquals(self.previous_log['revision'], git_svn_clone.prepare()) |
| 421 |
| 422 def testException(self): |
| 423 self._check_exception( |
| 424 self._get_co(True), 'fatal: corrupt patch at line 12\n') |
| 425 |
| 426 def testSvnProps(self): |
| 427 co = self._get_co(False) |
| 428 co.prepare() |
| 429 try: |
| 430 svn_props = [('foo', 'bar')] |
| 431 co.apply_patch( |
| 432 [patch.FilePatchDiff('svn_utils_test.txt', NAKED_PATCH, svn_props)]) |
| 433 self.fail() |
| 434 except patch.UnsupportedPatchFormat, e: |
| 435 self.assertEquals(e.filename, 'svn_utils_test.txt') |
| 436 self.assertEquals( |
| 437 e.status, |
| 438 'Cannot apply svn property foo to file svn_utils_test.txt.') |
| 439 co.prepare() |
| 440 # svn:eol-style is ignored. |
| 441 svn_props = [('svn:eol-style', 'LF')] |
| 442 co.apply_patch( |
| 443 [patch.FilePatchDiff('svn_utils_test.txt', NAKED_PATCH, svn_props)]) |
| 444 |
| 445 |
| 446 class RawCheckout(SvnBaseTest): |
| 447 def setUp(self): |
| 448 super(RawCheckout, self).setUp() |
| 449 # Use a svn checkout as the base. |
| 450 self.base_co = checkout.SvnCheckout( |
| 451 self.root_dir, self.name, None, None, self.svn_url) |
| 452 self.base_co.prepare() |
| 453 |
| 454 def _get_co(self, read_only): |
| 455 co = checkout.RawCheckout(self.root_dir, self.name) |
| 456 if read_only: |
| 457 return checkout.ReadOnlyCheckout(co) |
| 458 return co |
| 459 |
| 460 def _check(self, read_only): |
| 461 root = os.path.join(self.root_dir, self.name) |
| 462 co = self._get_co(read_only) |
| 463 |
| 464 # A copy of BaseTest._check_base() |
| 465 self.assertEquals(root, co.project_path) |
| 466 self.assertEquals(None, co.prepare()) |
| 467 self.assertEquals('pouet', co.get_settings('bar')) |
| 468 self.assertTree(self.get_trunk(False), root) |
| 469 patches = self.get_patches() |
| 470 co.apply_patch(patches) |
| 471 self.assertEquals( |
| 472 ['bin_file', 'extra', 'new_dir/subdir/new_file', 'svn_utils_test.txt'], |
| 473 sorted(patches.filenames)) |
| 474 |
| 475 # Verify that the patch is applied even for read only checkout. |
| 476 self.assertTree(self.get_trunk(True), root) |
| 477 if read_only: |
| 478 revision = co.commit('msg', self.FAKE_REPOS.USERS[1][0]) |
| 479 self.assertEquals('FAKE', revision) |
| 480 else: |
| 481 try: |
| 482 co.commit('msg', self.FAKE_REPOS.USERS[1][0]) |
| 483 self.fail() |
| 484 except NotImplementedError: |
| 485 pass |
| 486 self.assertTree(self.get_trunk(True), root) |
| 487 # Verify that prepare() is a no-op. |
| 488 self.assertEquals(None, co.prepare()) |
| 489 self.assertTree(self.get_trunk(True), root) |
| 490 |
| 491 def testAllRW(self): |
| 492 self._check(False) |
| 493 |
| 494 def testAllRO(self): |
| 495 self._check(True) |
| 496 |
| 497 def testException(self): |
| 498 self._check_exception( |
| 499 self._get_co(True), |
| 500 'patching file svn_utils_test.txt\n' |
| 501 'Hunk #1 FAILED at 3.\n' |
| 502 '1 out of 1 hunk FAILED -- saving rejects to file ' |
| 503 'svn_utils_test.txt.rej\n') |
| 504 |
| 505 |
| 506 if __name__ == '__main__': |
| 507 if '-v' in sys.argv: |
| 508 DEBUGGING = True |
| 509 logging.basicConfig( |
| 510 level=logging.DEBUG, |
| 511 format='%(levelname)5s %(filename)15s(%(lineno)3d): %(message)s') |
| 512 else: |
| 513 logging.basicConfig( |
| 514 level=logging.ERROR, |
| 515 format='%(levelname)5s %(filename)15s(%(lineno)3d): %(message)s') |
| 516 unittest.main() |
OLD | NEW |