1#!/usr/bin/env python 2# 3# svnmucc_tests.py: tests of svnmucc 4# 5# Subversion is a tool for revision control. 6# See http://subversion.apache.org for more information. 7# 8# ==================================================================== 9# Licensed to the Apache Software Foundation (ASF) under one 10# or more contributor license agreements. See the NOTICE file 11# distributed with this work for additional information 12# regarding copyright ownership. The ASF licenses this file 13# to you under the Apache License, Version 2.0 (the 14# "License"); you may not use this file except in compliance 15# with the License. You may obtain a copy of the License at 16# 17# http://www.apache.org/licenses/LICENSE-2.0 18# 19# Unless required by applicable law or agreed to in writing, 20# software distributed under the License is distributed on an 21# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 22# KIND, either express or implied. See the License for the 23# specific language governing permissions and limitations 24# under the License. 25###################################################################### 26 27import svntest 28import re 29 30XFail = svntest.testcase.XFail_deco 31Issues = svntest.testcase.Issues_deco 32Issue = svntest.testcase.Issue_deco 33 34###################################################################### 35 36@Issues(3895,3953) 37def reject_bogus_mergeinfo(sbox): 38 "reject bogus mergeinfo" 39 40 sbox.build(create_wc=False) 41 42 expected_error = ".*(E200020.*Invalid revision|E175002.*PROPPATCH)" 43 44 # At present this tests the server, but if we ever make svnmucc 45 # validate the mergeinfo up front then it will only test the client 46 svntest.actions.run_and_verify_svnmucc([], expected_error, 47 'propset', 'svn:mergeinfo', '/B:0', 48 '-m', 'log msg', 49 sbox.repo_url + '/A') 50 51_svnmucc_re = re.compile(b'^(r[0-9]+) committed by jrandom at (.*)$') 52_log_re = re.compile('^ ([ADRM] /[^\(]+($| \(from .*:[0-9]+\)$))') 53_err_re = re.compile('^svnmucc: (.*)$') 54 55def test_svnmucc(repo_url, expected_path_changes, *varargs): 56 """Run svnmucc with the list of SVNMUCC_ARGS arguments. Verify that 57 its run results in a new commit with 'svn log -rHEAD' changed paths 58 that match the list of EXPECTED_PATH_CHANGES.""" 59 60 # First, run svnmucc. 61 exit_code, outlines, errlines = svntest.main.run_svnmucc('-U', repo_url, 62 *varargs) 63 if errlines: 64 raise svntest.main.SVNCommitFailure(str(errlines)) 65 if len(outlines) != 1 or not _svnmucc_re.match(outlines[0]): 66 raise svntest.main.SVNLineUnequal(str(outlines)) 67 68 # Now, run 'svn log -vq -rHEAD' 69 changed_paths = [] 70 exit_code, outlines, errlines = \ 71 svntest.main.run_svn(None, 'log', '-vqrHEAD', repo_url) 72 if errlines: 73 raise svntest.Failure("Unable to verify commit with 'svn log': %s" 74 % (str(errlines))) 75 for line in outlines: 76 match = _log_re.match(line) 77 if match: 78 changed_paths.append(match.group(1).rstrip('\n\r')) 79 80 expected_path_changes.sort() 81 changed_paths.sort() 82 if changed_paths != expected_path_changes: 83 raise svntest.Failure("Logged path changes differ from expectations\n" 84 " expected: %s\n" 85 " actual: %s" % (str(expected_path_changes), 86 str(changed_paths))) 87 88def xtest_svnmucc(repo_url, expected_errors, *varargs): 89 """Run svnmucc with the list of SVNMUCC_ARGS arguments. Verify that 90 its run results match the list of EXPECTED_ERRORS.""" 91 92 # First, run svnmucc. 93 exit_code, outlines, errlines = svntest.main.run_svnmucc('-U', repo_url, 94 *varargs) 95 errors = [] 96 for line in errlines: 97 match = _err_re.match(line) 98 if match: 99 errors.append(line.rstrip('\n\r')) 100 if errors != expected_errors: 101 raise svntest.main.SVNUnmatchedError(str(errors)) 102 103 104def basic_svnmucc(sbox): 105 "basic svnmucc tests" 106 107 sbox.build() 108 empty_file = sbox.ospath('empty') 109 file = sbox.ospath('file') 110 svntest.main.file_append(empty_file, '') 111 svntest.main.file_append(file, 'file') 112 113 # revision 2 114 test_svnmucc(sbox.repo_url, 115 ['A /foo' 116 ], # --------- 117 '-m', 'log msg', 118 'mkdir', 'foo') 119 120 # revision 3 121 test_svnmucc(sbox.repo_url, 122 ['A /z.c', 123 ], # --------- 124 '-m', 'log msg', 125 'put', empty_file, 'z.c') 126 127 # revision 4 128 test_svnmucc(sbox.repo_url, 129 ['A /foo/z.c (from /z.c:3)', 130 'A /foo/bar (from /foo:3)', 131 ], # --------- 132 '-m', 'log msg', 133 'cp', '3', 'z.c', 'foo/z.c', 134 'cp', '3', 'foo', 'foo/bar') 135 136 # revision 5 137 test_svnmucc(sbox.repo_url, 138 ['A /zig (from /foo:4)', 139 'D /zig/bar', 140 'D /foo', 141 'A /zig/zag (from /foo:4)', 142 ], # --------- 143 '-m', 'log msg', 144 'cp', '4', 'foo', 'zig', 145 'rm', 'zig/bar', 146 'mv', 'foo', 'zig/zag') 147 148 # revision 6 149 test_svnmucc(sbox.repo_url, 150 ['D /z.c', 151 'A /zig/zag/bar/y.c (from /z.c:5)', 152 'A /zig/zag/bar/x.c (from /z.c:3)', 153 ], # --------- 154 '-m', 'log msg', 155 'mv', 'z.c', 'zig/zag/bar/y.c', 156 'cp', '3', 'z.c', 'zig/zag/bar/x.c') 157 158 # revision 7 159 test_svnmucc(sbox.repo_url, 160 ['D /zig/zag/bar/y.c', 161 'A /zig/zag/bar/y y.c (from /zig/zag/bar/y.c:6)', 162 'A /zig/zag/bar/y%20y.c (from /zig/zag/bar/y.c:6)', 163 ], # --------- 164 '-m', 'log msg', 165 'mv', 'zig/zag/bar/y.c', 'zig/zag/bar/y%20y.c', 166 'cp', 'HEAD', 'zig/zag/bar/y.c', 'zig/zag/bar/y%2520y.c') 167 168 # revision 8 169 test_svnmucc(sbox.repo_url, 170 ['D /zig/zag/bar/y y.c', 171 'A /zig/zag/bar/z z1.c (from /zig/zag/bar/y y.c:7)', 172 'A /zig/zag/bar/z%20z.c (from /zig/zag/bar/y%20y.c:7)', 173 'A /zig/zag/bar/z z2.c (from /zig/zag/bar/y y.c:7)', 174 ], #--------- 175 '-m', 'log msg', 176 'mv', 'zig/zag/bar/y%20y.c', 'zig/zag/bar/z z1.c', 177 'cp', 'HEAD', 'zig/zag/bar/y%2520y.c', 'zig/zag/bar/z%2520z.c', 178 'cp', 'HEAD', 'zig/zag/bar/y y.c', 'zig/zag/bar/z z2.c') 179 180 181 # revision 9 182 test_svnmucc(sbox.repo_url, 183 ['D /zig/zag', 184 'A /zig/foo (from /zig/zag:8)', 185 'D /zig/foo/bar/z%20z.c', 186 'D /zig/foo/bar/z z2.c', 187 'R /zig/foo/bar/z z1.c (from /zig/zag/bar/x.c:6)', 188 ], #--------- 189 '-m', 'log msg', 190 'mv', 'zig/zag', 'zig/foo', 191 'rm', 'zig/foo/bar/z z1.c', 192 'rm', 'zig/foo/bar/z%20z2.c', 193 'rm', 'zig/foo/bar/z%2520z.c', 194 'cp', '6', 'zig/zag/bar/x.c', 'zig/foo/bar/z%20z1.c') 195 196 # revision 10 197 test_svnmucc(sbox.repo_url, 198 ['R /zig/foo/bar (from /zig/z.c:9)', 199 ], #--------- 200 '-m', 'log msg', 201 'rm', 'zig/foo/bar', 202 'cp', '9', 'zig/z.c', 'zig/foo/bar') 203 204 # revision 11 205 test_svnmucc(sbox.repo_url, 206 ['R /zig/foo/bar (from /zig/foo/bar:9)', 207 'D /zig/foo/bar/z z1.c', 208 ], #--------- 209 '-m', 'log msg', 210 'rm', 'zig/foo/bar', 211 'cp', '9', 'zig/foo/bar', 'zig/foo/bar', 212 'rm', 'zig/foo/bar/z%20z1.c') 213 214 # revision 12 215 test_svnmucc(sbox.repo_url, 216 ['R /zig/foo (from /zig/foo/bar:11)', 217 ], #--------- 218 '-m', 'log msg', 219 'rm', 'zig/foo', 220 'cp', 'head', 'zig/foo/bar', 'zig/foo') 221 222 # revision 13 223 test_svnmucc(sbox.repo_url, 224 ['D /zig', 225 'A /foo (from /foo:4)', 226 'A /foo/foo (from /foo:4)', 227 'A /foo/foo/foo (from /foo:4)', 228 'D /foo/foo/bar', 229 'R /foo/foo/foo/bar (from /foo:4)', 230 ], #--------- 231 '-m', 'log msg', 232 'rm', 'zig', 233 'cp', '4', 'foo', 'foo', 234 'cp', '4', 'foo', 'foo/foo', 235 'cp', '4', 'foo', 'foo/foo/foo', 236 'rm', 'foo/foo/bar', 237 'rm', 'foo/foo/foo/bar', 238 'cp', '4', 'foo', 'foo/foo/foo/bar') 239 240 # revision 14 241 test_svnmucc(sbox.repo_url, 242 ['A /boozle (from /foo:4)', 243 'A /boozle/buz', 244 'A /boozle/buz/nuz', 245 ], #--------- 246 '-m', 'log msg', 247 'cp', '4', 'foo', 'boozle', 248 'mkdir', 'boozle/buz', 249 'mkdir', 'boozle/buz/nuz') 250 251 # revision 15 252 test_svnmucc(sbox.repo_url, 253 ['A /boozle/buz/svnmucc-test.py', 254 'A /boozle/guz (from /boozle/buz:14)', 255 'A /boozle/guz/svnmucc-test.py', 256 ], #--------- 257 '-m', 'log msg', 258 'put', empty_file, 'boozle/buz/svnmucc-test.py', 259 'cp', '14', 'boozle/buz', 'boozle/guz', 260 'put', empty_file, 'boozle/guz/svnmucc-test.py') 261 262 # revision 16 263 test_svnmucc(sbox.repo_url, 264 ['M /boozle/buz/svnmucc-test.py', 265 'R /boozle/guz/svnmucc-test.py', 266 ], #--------- 267 '-m', 'log msg', 268 'put', empty_file, 'boozle/buz/svnmucc-test.py', 269 'rm', 'boozle/guz/svnmucc-test.py', 270 'put', empty_file, 'boozle/guz/svnmucc-test.py') 271 272 # revision 17 273 test_svnmucc(sbox.repo_url, 274 ['R /foo/bar (from /foo/foo:16)' 275 ], #--------- 276 '-m', 'log msg', 277 'rm', 'foo/bar', 278 'cp', '16', 'foo/foo', 'foo/bar', 279 'propset', 'testprop', 'true', 'foo/bar') 280 281 # revision 18 282 test_svnmucc(sbox.repo_url, 283 ['M /foo/bar' 284 ], #--------- 285 '-m', 'log msg', 286 'propdel', 'testprop', 'foo/bar') 287 288 # revision 19 289 test_svnmucc(sbox.repo_url, 290 ['M /foo/z.c', 291 'M /foo/foo', 292 ], #--------- 293 '-m', 'log msg', 294 'propset', 'testprop', 'true', 'foo/z.c', 295 'propset', 'testprop', 'true', 'foo/foo') 296 297 # revision 20 298 test_svnmucc(sbox.repo_url, 299 ['M /foo/z.c', 300 'M /foo/foo', 301 ], #--------- 302 '-m', 'log msg', 303 'propsetf', 'testprop', empty_file, 'foo/z.c', 304 'propsetf', 'testprop', empty_file, 'foo/foo') 305 306 # revision 21 307 test_svnmucc(sbox.repo_url, 308 ['M /foo/z.c', 309 ], #--------- 310 '-m', 'log msg', 311 'propset', 'testprop', 'false', 'foo/z.c', 312 'put', file, 'foo/z.c') 313 314 # Expected missing revision error 315 xtest_svnmucc(sbox.repo_url, 316 ["svnmucc: E200004: 'a' is not a revision" 317 ], #--------- 318 '-m', 'log msg', 319 'cp', 'a', 'b') 320 321 # Expected cannot be younger error 322 xtest_svnmucc(sbox.repo_url, 323 ['svnmucc: E160006: No such revision 42', 324 ], #--------- 325 '-m', 'log msg', 326 'cp', '42', 'a', 'b') 327 328 # Expected already exists error 329 xtest_svnmucc(sbox.repo_url, 330 ["svnmucc: E160020: Path 'foo' already exists", 331 ], #--------- 332 '-m', 'log msg', 333 'cp', '17', 'a', 'foo') 334 335 # Expected copy_src already exists error 336 xtest_svnmucc(sbox.repo_url, 337 ["svnmucc: E160020: Path 'a/bar' already exists", 338 ], #--------- 339 '-m', 'log msg', 340 'cp', '17', 'foo', 'a', 341 'cp', '17', 'foo/foo', 'a/bar') 342 343 # Expected not found error 344 xtest_svnmucc(sbox.repo_url, 345 ["svnmucc: E160013: Path 'a' not found in revision 17", 346 ], #--------- 347 '-m', 'log msg', 348 'cp', '17', 'a', 'b') 349 350 351def propset_root_internal(sbox, target): 352 ## propset on ^/ 353 svntest.actions.run_and_verify_svnmucc(None, [], 354 '-m', 'log msg', 355 'propset', 'foo', 'bar', 356 target) 357 svntest.actions.run_and_verify_svn('bar', [], 358 'propget', '--no-newline', 'foo', 359 target) 360 361 ## propdel on ^/ 362 svntest.actions.run_and_verify_svnmucc(None, [], 363 '-m', 'log msg', 364 'propdel', 'foo', 365 target) 366 svntest.actions.run_and_verify_svn([], 367 '.*W200017: Property.*not found', 368 'propget', '--no-newline', 'foo', 369 target) 370 371@Issues(3663) 372def propset_root(sbox): 373 "propset/propdel on repos root" 374 375 sbox.build(create_wc=False) 376 propset_root_internal(sbox, sbox.repo_url) 377 propset_root_internal(sbox, sbox.repo_url + '/iota') 378 379 380def too_many_log_messages(sbox): 381 "test log message mutual exclusivity checks" 382 383 sbox.build() # would use read-only=True, but need a place to stuff msg_file 384 msg_file = sbox.ospath('svnmucc_msg') 385 svntest.main.file_append(msg_file, 'some log message') 386 err_msg = ["svnmucc: E205000: --message (-m), --file (-F), and " 387 "--with-revprop=svn:log are mutually exclusive"] 388 389 xtest_svnmucc(sbox.repo_url, err_msg, 390 '--non-interactive', 391 '-m', 'log msg', 392 '-F', msg_file, 393 'mkdir', 'A/subdir') 394 xtest_svnmucc(sbox.repo_url, err_msg, 395 '--non-interactive', 396 '-m', 'log msg', 397 '--with-revprop', 'svn:log=proppy log message', 398 'mkdir', 'A/subdir') 399 xtest_svnmucc(sbox.repo_url, err_msg, 400 '--non-interactive', 401 '-F', msg_file, 402 '--with-revprop', 'svn:log=proppy log message', 403 'mkdir', 'A/subdir') 404 xtest_svnmucc(sbox.repo_url, err_msg, 405 '--non-interactive', 406 '-m', 'log msg', 407 '-F', msg_file, 408 '--with-revprop', 'svn:log=proppy log message', 409 'mkdir', 'A/subdir') 410 411@Issues(3418) 412def no_log_msg_non_interactive(sbox): 413 "test non-interactive without a log message" 414 415 sbox.build(create_wc=False) 416 xtest_svnmucc(sbox.repo_url, 417 ["svnmucc: E205001: Cannot invoke editor to get log message " 418 "when non-interactive" 419 ], #--------- 420 '--non-interactive', 421 'mkdir', 'A/subdir') 422 423 424def nested_replaces(sbox): 425 "nested replaces" 426 427 sbox.build(create_wc=False) 428 repo_url = sbox.repo_url 429 svntest.actions.run_and_verify_svnmucc(None, [], 430 '-U', repo_url, '-m', 'r2: create tree', 431 'rm', 'A', 432 'rm', 'iota', 433 'mkdir', 'A', 'mkdir', 'A/B', 'mkdir', 'A/B/C', 434 'mkdir', 'M', 'mkdir', 'M/N', 'mkdir', 'M/N/O', 435 'mkdir', 'X', 'mkdir', 'X/Y', 'mkdir', 'X/Y/Z') 436 svntest.actions.run_and_verify_svnmucc(None, [], 437 '-U', repo_url, '-m', 'r3: nested replaces', 438 *(""" 439rm A rm M rm X 440cp HEAD X/Y/Z A cp HEAD A/B/C M cp HEAD M/N/O X 441cp HEAD A/B A/B cp HEAD M/N M/N cp HEAD X/Y X/Y 442rm A/B/C rm M/N/O rm X/Y/Z 443cp HEAD X A/B/C cp HEAD A M/N/O cp HEAD M X/Y/Z 444rm A/B/C/Y 445 """.split())) 446 447 # ### TODO: need a smarter run_and_verify_log() that verifies copyfrom 448 excaped = svntest.main.ensure_list(map(re.escape, [ 449 ' R /A (from /X/Y/Z:2)', 450 ' A /A/B (from /A/B:2)', 451 ' R /A/B/C (from /X:2)', 452 ' R /M (from /A/B/C:2)', 453 ' A /M/N (from /M/N:2)', 454 ' R /M/N/O (from /A:2)', 455 ' R /X (from /M/N/O:2)', 456 ' A /X/Y (from /X/Y:2)', 457 ' R /X/Y/Z (from /M:2)', 458 ' D /A/B/C/Y', 459 ])) 460 expected_output = svntest.verify.UnorderedRegexListOutput(excaped 461 + ['^--*', '^r3.*', '^--*', '^Changed paths:',]) 462 svntest.actions.run_and_verify_svn(expected_output, [], 463 'log', '-qvr3', repo_url) 464 465 466def prohibited_deletes_and_moves(sbox): 467 "test prohibited delete and move operations" 468 469 # These action sequences were allowed in 1.8.13, but are prohibited in 1.9.x 470 # and later. Most of them probably indicate an inadvertent user mistake. 471 # See dev@, 2015-05-11, "Re: Issue 4579 / svnmucc fails to process certain 472 # deletes", <http://svn.haxx.se/dev/archive-2015-05/0038.shtml> 473 474 sbox.build(read_only = True) 475 svntest.main.file_write(sbox.ospath('file'), "New contents") 476 477 xtest_svnmucc(sbox.repo_url, 478 ["svnmucc: E200009: Can't delete node at 'iota'", 479 ], #--------- 480 '-m', 'r2: modify and delete /iota', 481 'put', sbox.ospath('file'), 'iota', 482 'rm', 'iota') 483 484 xtest_svnmucc(sbox.repo_url, 485 ["svnmucc: E200009: Can't delete node at 'iota'", 486 ], #--------- 487 '-m', 'r2: propset and delete /iota', 488 'propset', 'prop', 'val', 'iota', 489 'rm', 'iota') 490 491 xtest_svnmucc(sbox.repo_url, 492 ["svnmucc: E160013: Can't delete node at 'iota' as it does " 493 "not exist", 494 ], #--------- 495 '-m', 'r2: delete and delete /iota', 496 'rm', 'iota', 497 'rm', 'iota') 498 499 # Subversion 1.8.13 used to move /iota without applying the text change. 500 xtest_svnmucc(sbox.repo_url, 501 ["svnmucc: E200009: Can't delete node at 'iota'", 502 ], #--------- 503 '-m', 'r2: modify and move /iota', 504 'put', sbox.ospath('file'), 'iota', 505 'mv', 'iota', 'iota2') 506 507 # Subversion 1.8.13 used to move /A without applying the inner remove. 508 xtest_svnmucc(sbox.repo_url, 509 ["svnmucc: E200009: Can't delete node at 'A'", 510 ], #--------- 511 '-m', 'r2: delete /A/B and move /A', 512 'rm', 'A/B', 513 'mv', 'A', 'A1') 514 515def svnmucc_type_errors(sbox): 516 "test type errors" 517 518 sbox.build(read_only=True) 519 520 sbox.simple_append('file', 'New contents') 521 522 xtest_svnmucc(sbox.repo_url, 523 ["svnmucc: E160016: Can't operate on 'B' " 524 "because 'A' is not a directory"], 525 '-m', '', 526 'put', sbox.ospath('file'), 'A', 527 'mkdir', 'A/B', 528 'propset', 'iota', 'iota', 'iota') 529 530 xtest_svnmucc(sbox.repo_url, 531 ["svnmucc: E200009: Can't delete node at 'A'"], 532 '-m', '', 533 'mkdir', 'A/Z', 534 'put', sbox.ospath('file'), 'A') 535 536 xtest_svnmucc(sbox.repo_url, 537 ["svnmucc: E160020: Path 'Z' already exists, or was created " 538 "by an earlier operation"], 539 '-m', '', 540 'mkdir', 'A/Z', 541 'put', sbox.ospath('file'), 'A/Z') 542 543def svnmucc_propset_and_put(sbox): 544 "propset and put" 545 546 sbox.build() 547 548 sbox.simple_append('file', 'New contents') 549 550 # First in the sane order: put, then propset 551 xtest_svnmucc(sbox.repo_url, 552 [], 553 '-m', '', 554 'put', sbox.ospath('file'), 't1', 555 'propset', 't1', 't1', 't1') 556 557 # And now in an impossible order: propset, then put 558 xtest_svnmucc(sbox.repo_url, 559 ["svnmucc: E200009: Can't set properties at not existing 't2'"], 560 '-m', '', 561 'propset', 't2', 't2', 't2', 562 'put', sbox.ospath('file'), 't2') 563 564 # And if the target already exists (dir) 565 xtest_svnmucc(sbox.repo_url, 566 ["svnmucc: E200009: Can't delete node at 'A'"], 567 '-m', '', 568 'propset', 'A', 'A', 'A', 569 'put', sbox.ospath('file'), 'A') 570 571 # And if the target already exists (file) # fixed in r1702467 572 xtest_svnmucc(sbox.repo_url, 573 [], 574 '-m', '', 575 'propset', 'iota', 'iota', 'iota', 576 'put', sbox.ospath('file'), 'iota') 577 578 # Put same file twice (non existing) 579 xtest_svnmucc(sbox.repo_url, 580 ["svnmucc: E160020: Path 't3' already exists, or was created " 581 "by an earlier operation"], 582 '-m', '', 583 'put', sbox.ospath('file'), 't3', 584 'put', sbox.ospath('file'), 't3') 585 586 # Put same file twice (existing) 587 xtest_svnmucc(sbox.repo_url, 588 ["svnmucc: E200009: Can't update file at 't1'"], 589 '-m', '', 590 'put', sbox.ospath('file'), 't1', 591 'put', sbox.ospath('file'), 't1') 592 593 594###################################################################### 595 596test_list = [ None, 597 reject_bogus_mergeinfo, 598 basic_svnmucc, 599 propset_root, 600 too_many_log_messages, 601 no_log_msg_non_interactive, 602 nested_replaces, 603 prohibited_deletes_and_moves, 604 svnmucc_type_errors, 605 svnmucc_propset_and_put, 606 ] 607 608if __name__ == '__main__': 609 svntest.main.run_tests(test_list) 610