1#!/usr/bin/env python 2# 3# svnsync_tests.py: Tests SVNSync's repository mirroring capabilities. 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 27# General modules 28import sys, os 29 30# Test suite-specific modules 31import re 32 33# Our testing module 34import svntest 35from svntest.verify import SVNUnexpectedStdout, SVNUnexpectedStderr 36from svntest.verify import SVNExpectedStderr 37from svntest.verify import AnyOutput 38from svntest.main import server_has_partial_replay 39 40# (abbreviation) 41Skip = svntest.testcase.Skip_deco 42SkipUnless = svntest.testcase.SkipUnless_deco 43XFail = svntest.testcase.XFail_deco 44Issues = svntest.testcase.Issues_deco 45Issue = svntest.testcase.Issue_deco 46Wimp = svntest.testcase.Wimp_deco 47Item = svntest.wc.StateItem 48 49###################################################################### 50# Helper routines 51 52 53def run_sync(url, source_url=None, 54 source_prop_encoding=None, 55 expected_output=AnyOutput, expected_error=[]): 56 "Synchronize the mirror repository with the master" 57 if source_url is not None: 58 args = ["synchronize", url, source_url] 59 else: # Allow testing of old source-URL-less syntax 60 args = ["synchronize", url] 61 if source_prop_encoding: 62 args.append("--source-prop-encoding") 63 args.append(source_prop_encoding) 64 65 # Normal expected output is of the form: 66 # ['Transmitting file data .......\n', # optional 67 # 'Committed revision 1.\n', 68 # 'Copied properties for revision 1.\n', ...] 69 svntest.actions.run_and_verify_svnsync(expected_output, expected_error, 70 *args) 71 72def run_copy_revprops(url, source_url, 73 source_prop_encoding=None, 74 expected_output=AnyOutput, expected_error=[]): 75 "Copy revprops to the mirror repository from the master" 76 args = ["copy-revprops", url, source_url] 77 if source_prop_encoding: 78 args.append("--source-prop-encoding") 79 args.append(source_prop_encoding) 80 81 # Normal expected output is of the form: 82 # ['Copied properties for revision 1.\n', ...] 83 svntest.actions.run_and_verify_svnsync(expected_output, expected_error, 84 *args) 85 86def run_init(dst_url, src_url, source_prop_encoding=None): 87 "Initialize the mirror repository from the master" 88 args = ["initialize", dst_url, src_url] 89 if source_prop_encoding: 90 args.append("--source-prop-encoding") 91 args.append(source_prop_encoding) 92 93 expected_output = ['Copied properties for revision 0.\n'] 94 svntest.actions.run_and_verify_svnsync(expected_output, [], *args) 95 96def run_info(url, expected_output=AnyOutput, expected_error=[]): 97 "Print synchronization information of the repository" 98 # Normal expected output is of the form: 99 # ['From URL: http://....\n', 100 # 'From UUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX\n', 101 # 'Last Merged Revision: XXX\n'] 102 svntest.actions.run_and_verify_svnsync(expected_output, expected_error, 103 "info", url) 104 105 106def setup_and_sync(sbox, dump_file_contents, subdir=None, 107 bypass_prop_validation=False, source_prop_encoding=None, 108 is_src_ra_local=None, is_dest_ra_local=None): 109 """Create a repository for SBOX, load it with DUMP_FILE_CONTENTS, then create a mirror repository and sync it with SBOX. If is_src_ra_local or is_dest_ra_local is True, then run_init, run_sync, and run_copy_revprops will use the file:// scheme for the source and destination URLs. Return the mirror sandbox.""" 110 111 # Create the empty master repository. 112 sbox.build(create_wc=False, empty=True) 113 114 # Load the repository from DUMP_FILE_PATH. 115 svntest.actions.run_and_verify_load(sbox.repo_dir, dump_file_contents, 116 bypass_prop_validation) 117 118 # Create the empty destination repository. 119 dest_sbox = sbox.clone_dependent() 120 dest_sbox.build(create_wc=False, empty=True) 121 122 # Setup the mirror repository. Feed it the UUID of the source repository. 123 exit_code, output, errput = svntest.main.run_svnlook("uuid", sbox.repo_dir) 124 svntest.actions.run_and_verify_svnadmin2(None, None, 0, 125 'setuuid', dest_sbox.repo_dir, 126 output[0][:-1]) 127 128 # Create the revprop-change hook for this test 129 svntest.actions.enable_revprop_changes(dest_sbox.repo_dir) 130 131 repo_url = sbox.repo_url 132 cwd = os.getcwd() 133 if is_src_ra_local: 134 repo_url = sbox.file_protocol_repo_url() 135 136 if subdir: 137 repo_url = repo_url + subdir 138 139 dest_repo_url = dest_sbox.repo_url 140 if is_dest_ra_local: 141 dest_repo_url = dest_sbox.file_protocol_repo_url() 142 run_init(dest_repo_url, repo_url, source_prop_encoding) 143 144 run_sync(dest_repo_url, repo_url, 145 source_prop_encoding=source_prop_encoding) 146 run_copy_revprops(dest_repo_url, repo_url, 147 source_prop_encoding=source_prop_encoding) 148 149 return dest_sbox 150 151def verify_mirror(dest_sbox, exp_dump_file_contents): 152 """Compare the contents of the mirror repository in DEST_SBOX with 153 EXP_DUMP_FILE_CONTENTS, by comparing the parsed dump stream content. 154 155 First remove svnsync rev-props from the DEST_SBOX repository. 156 """ 157 158 # Remove some SVNSync-specific housekeeping properties from the 159 # mirror repository in preparation for the comparison dump. 160 for prop_name in ("svn:sync-from-url", "svn:sync-from-uuid", 161 "svn:sync-last-merged-rev"): 162 svntest.actions.run_and_verify_svn( 163 None, [], "propdel", "--revprop", "-r", "0", 164 prop_name, dest_sbox.repo_url) 165 166 # Create a dump file from the mirror repository. 167 dest_dump = svntest.actions.run_and_verify_dump(dest_sbox.repo_dir) 168 169 svntest.verify.compare_dump_files( 170 None, None, exp_dump_file_contents, dest_dump) 171 172def run_test(sbox, dump_file_name, subdir=None, exp_dump_file_name=None, 173 bypass_prop_validation=False, source_prop_encoding=None, 174 is_src_ra_local=None, is_dest_ra_local=None): 175 176 """Load a dump file, sync repositories, and compare contents with the original 177or another dump file.""" 178 179 # This directory contains all the dump files 180 svnsync_tests_dir = os.path.join(os.path.dirname(sys.argv[0]), 181 'svnsync_tests_data') 182 183 # Load the specified dump file into the master repository. 184 master_dumpfile_contents = open(os.path.join(svnsync_tests_dir, 185 dump_file_name), 186 'rb').readlines() 187 188 dest_sbox = setup_and_sync(sbox, master_dumpfile_contents, subdir, 189 bypass_prop_validation, source_prop_encoding, 190 is_src_ra_local, is_dest_ra_local) 191 192 # Compare the dump produced by the mirror repository with either the original 193 # dump file (used to create the master repository) or another specified dump 194 # file. 195 if exp_dump_file_name: 196 exp_dump_file_contents = open(os.path.join(svnsync_tests_dir, 197 exp_dump_file_name), 'rb').readlines() 198 else: 199 exp_dump_file_contents = master_dumpfile_contents 200 201 verify_mirror(dest_sbox, exp_dump_file_contents) 202 203 204 205###################################################################### 206# Tests 207 208#---------------------------------------------------------------------- 209 210def copy_and_modify(sbox): 211 "copy and modify" 212 run_test(sbox, "copy-and-modify.dump") 213 214#---------------------------------------------------------------------- 215 216def copy_from_previous_version_and_modify(sbox): 217 "copy from previous version and modify" 218 run_test(sbox, "copy-from-previous-version-and-modify.dump") 219 220#---------------------------------------------------------------------- 221 222def copy_from_previous_version(sbox): 223 "copy from previous version" 224 run_test(sbox, "copy-from-previous-version.dump") 225 226#---------------------------------------------------------------------- 227 228def modified_in_place(sbox): 229 "modified in place" 230 run_test(sbox, "modified-in-place.dump") 231 232#---------------------------------------------------------------------- 233 234def tag_empty_trunk(sbox): 235 "tag empty trunk" 236 run_test(sbox, "tag-empty-trunk.dump") 237 238#---------------------------------------------------------------------- 239 240def tag_trunk_with_dir(sbox): 241 "tag trunk containing a sub-directory" 242 run_test(sbox, "tag-trunk-with-dir.dump") 243 244#---------------------------------------------------------------------- 245 246def tag_trunk_with_file(sbox): 247 "tag trunk containing a file" 248 run_test(sbox, "tag-trunk-with-file.dump") 249 250#---------------------------------------------------------------------- 251 252def tag_trunk_with_file2(sbox): 253 "tag trunk containing a file (#2)" 254 run_test(sbox, "tag-trunk-with-file2.dump") 255 256#---------------------------------------------------------------------- 257 258def tag_with_modified_file(sbox): 259 "tag with a modified file" 260 run_test(sbox, "tag-with-modified-file.dump") 261 262#---------------------------------------------------------------------- 263 264def dir_prop_change(sbox): 265 "directory property changes" 266 run_test(sbox, "dir-prop-change.dump") 267 268#---------------------------------------------------------------------- 269 270def file_dir_file(sbox): 271 "files and dirs mixed together" 272 run_test(sbox, "file-dir-file.dump") 273 274#---------------------------------------------------------------------- 275 276def copy_parent_modify_prop(sbox): 277 "copy parent and modify prop" 278 run_test(sbox, "copy-parent-modify-prop.dump") 279 280#---------------------------------------------------------------------- 281 282def detect_meddling(sbox): 283 "detect non-svnsync commits in destination" 284 285 sbox.build("svnsync-meddling") 286 287 dest_sbox = sbox.clone_dependent() 288 dest_sbox.build(create_wc=False, empty=True) 289 290 # Make our own destination checkout (have to do it ourself because 291 # it is not greek). 292 293 svntest.main.safe_rmtree(dest_sbox.wc_dir) 294 svntest.actions.run_and_verify_svn(None, 295 [], 296 'co', 297 dest_sbox.repo_url, 298 dest_sbox.wc_dir) 299 300 svntest.actions.enable_revprop_changes(dest_sbox.repo_dir) 301 302 run_init(dest_sbox.repo_url, sbox.repo_url) 303 run_sync(dest_sbox.repo_url) 304 305 svntest.actions.run_and_verify_svn(None, 306 [], 307 'up', 308 dest_sbox.wc_dir) 309 310 # Commit some change to the destination, which should be detected by svnsync 311 svntest.main.file_append(os.path.join(dest_sbox.wc_dir, 'A', 'B', 'lambda'), 312 'new lambda text') 313 svntest.actions.run_and_verify_svn(None, 314 [], 315 'ci', 316 '-m', 'msg', 317 dest_sbox.wc_dir) 318 319 expected_error = r".*Destination HEAD \(2\) is not the last merged revision \(1\).*" 320 run_sync(dest_sbox.repo_url, None, 321 expected_output=[], expected_error=expected_error) 322 323def url_encoding(sbox): 324 "test url encoding issues" 325 run_test(sbox, "url-encoding-bug.dump") 326 327 328# A test for copying revisions that lack a property that already exists 329# on the destination rev as part of the commit (i.e. svn:author in this 330# case, but svn:date would also work). 331def no_author(sbox): 332 "test copying revs with no svn:author revprops" 333 run_test(sbox, "no-author.dump") 334 335def copy_revprops(sbox): 336 "test copying revprops other than svn:*" 337 run_test(sbox, "revprops.dump") 338 339@SkipUnless(server_has_partial_replay) 340def only_trunk(sbox): 341 "test syncing subdirectories" 342 run_test(sbox, "svnsync-trunk-only.dump", "/trunk", 343 "svnsync-trunk-only.expected.dump") 344 345@SkipUnless(server_has_partial_replay) 346def only_trunk_A_with_changes(sbox): 347 "test syncing subdirectories with changes on root" 348 run_test(sbox, "svnsync-trunk-A-changes.dump", "/trunk/A", 349 "svnsync-trunk-A-changes.expected.dump") 350 351# test for issue #2904 352@Issue(2904) 353def move_and_modify_in_the_same_revision(sbox): 354 "test move parent and modify child file in same rev" 355 run_test(sbox, "svnsync-move-and-modify.dump") 356 357def info_synchronized(sbox): 358 "test info cmd on a synchronized repo" 359 360 sbox.build("svnsync-info-syncd", False) 361 362 # Get the UUID of the source repository. 363 exit_code, output, errput = svntest.main.run_svnlook("uuid", sbox.repo_dir) 364 src_uuid = output[0].strip() 365 366 dest_sbox = sbox.clone_dependent() 367 dest_sbox.build(create_wc=False, empty=True) 368 369 svntest.actions.enable_revprop_changes(dest_sbox.repo_dir) 370 run_init(dest_sbox.repo_url, sbox.repo_url) 371 run_sync(dest_sbox.repo_url) 372 373 expected_out = ['Source URL: %s\n' % sbox.repo_url, 374 'Source Repository UUID: %s\n' % src_uuid, 375 'Last Merged Revision: 1\n', 376 ] 377 svntest.actions.run_and_verify_svnsync(expected_out, [], 378 "info", dest_sbox.repo_url) 379 380def info_not_synchronized(sbox): 381 "test info cmd on an un-synchronized repo" 382 383 sbox.build("svnsync-info-not-syncd", False) 384 385 run_info(sbox.repo_url, 386 [], ".*Repository '%s' is not initialized.*" % sbox.repo_url) 387 388#---------------------------------------------------------------------- 389 390def copy_bad_line_endings(sbox): 391 "copy with inconsistent line endings in svn:* props" 392 run_test(sbox, "copy-bad-line-endings.dump", 393 exp_dump_file_name="copy-bad-line-endings.expected.dump", 394 bypass_prop_validation=True) 395 396def copy_bad_line_endings2(sbox): 397 "copy with non-LF line endings in svn:* props" 398 run_test(sbox, "copy-bad-line-endings2.dump", 399 exp_dump_file_name="copy-bad-line-endings2.expected.dump", 400 bypass_prop_validation=True) 401 402def copy_bad_encoding(sbox): 403 "copy and reencode non-UTF-8 svn:* props" 404 run_test(sbox, "copy-bad-encoding.dump", 405 exp_dump_file_name="copy-bad-encoding.expected.dump", 406 bypass_prop_validation=True, source_prop_encoding="ISO-8859-3") 407 408#---------------------------------------------------------------------- 409 410def delete_svn_props(sbox): 411 "copy with svn:* prop deletions" 412 run_test(sbox, "delete-svn-props.dump") 413 414@Issue(3438) 415def commit_a_copy_of_root(sbox): 416 "commit a copy of root causes sync to fail" 417 #Testcase for issue 3438. 418 run_test(sbox, "repo-with-copy-of-root-dir.dump") 419 420 421# issue #3641 'svnsync fails to partially copy a repository'. 422# This currently fails because while replacements with history 423# within copies are handled, replacements without history inside 424# copies cause the sync to fail: 425# 426# >svnsync synchronize %TEST_REPOS_ROOT_URL%/svnsync_tests-29-1 427# %TEST_REPOS_ROOT_URL%/svnsync_tests-29/trunk/H 428# Transmitting file data ...\..\..\subversion\svnsync\main.c:1444: (apr_err=160013) 429# ..\..\..\subversion\svnsync\main.c:1391: (apr_err=160013) 430# ..\..\..\subversion\libsvn_ra\ra_loader.c:1168: (apr_err=160013) 431# ..\..\..\subversion\libsvn_delta\path_driver.c:254: (apr_err=160013) 432# ..\..\..\subversion\libsvn_repos\replay.c:480: (apr_err=160013) 433# ..\..\..\subversion\libsvn_repos\replay.c:276: (apr_err=160013) 434# ..\..\..\subversion\libsvn_repos\replay.c:290: (apr_err=160013) 435# ..\..\..\subversion\libsvn_fs_base\tree.c:1258: (apr_err=160013) 436# ..\..\..\subversion\libsvn_fs_base\tree.c:1258: (apr_err=160013) 437# ..\..\..\subversion\libsvn_fs_base\tree.c:1236: (apr_err=160013) 438# ..\..\..\subversion\libsvn_fs_base\tree.c:931: (apr_err=160013) 439# ..\..\..\subversion\libsvn_fs_base\tree.c:742: (apr_err=160013) 440# svnsync: File not found: revision 4, path '/trunk/H/Z/B/lambda' 441# 442# See also http://svn.haxx.se/dev/archive-2010-11/0411.shtml and 443# 444# 445# Note: For those who may poke around this test in the future, r3 of 446# delete-revprops.dump was created with the following svnmucc command: 447# 448# svnmucc.exe -mm cp head %ROOT_URL%/trunk/A %ROOT_URL%/trunk/H 449# rm %ROOT_URL%/trunk/H/B 450# cp head %ROOT_URL%/trunk/X %ROOT_URL%/trunk/B 451# 452# r4 was created with this svnmucc command: 453# 454# svnmucc.exe -mm cp head %ROOT_URL%/trunk/A %ROOT_URL%/trunk/H/Z 455# rm %ROOT_URL%/trunk/H/Z/B 456# mkdir %ROOT_URL%/trunk/H/Z/B 457@Issue(3641) 458def descend_into_replace(sbox): 459 "descending into replaced dir looks in src" 460 run_test(sbox, "descend-into-replace.dump", subdir='/trunk/H', 461 exp_dump_file_name = "descend-into-replace.expected.dump") 462 463# issue #3728 464@Issue(3728) 465def delete_revprops(sbox): 466 "copy-revprops with removals" 467 svnsync_tests_dir = os.path.join(os.path.dirname(sys.argv[0]), 468 'svnsync_tests_data') 469 initial_contents = open(os.path.join(svnsync_tests_dir, 470 "delete-revprops.dump"), 471 'rb').readlines() 472 expected_contents = open(os.path.join(svnsync_tests_dir, 473 "delete-revprops.expected.dump"), 474 'rb').readlines() 475 476 # Create the initial repos and mirror, and sync 'em. 477 dest_sbox = setup_and_sync(sbox, initial_contents) 478 479 # Now remove a revprop from r1 of the source, and run 'svnsync 480 # copy-revprops' to re-sync 'em. 481 svntest.actions.enable_revprop_changes(sbox.repo_dir) 482 exit_code, out, err = svntest.main.run_svn(None, 483 'pdel', 484 '-r', '1', 485 '--revprop', 486 'issue-id', 487 sbox.repo_url) 488 if err: 489 raise SVNUnexpectedStderr(err) 490 run_copy_revprops(dest_sbox.repo_url, sbox.repo_url) 491 492 # Does the result look as we expected? 493 verify_mirror(dest_sbox, expected_contents) 494 495@Issue(3870) 496@SkipUnless(svntest.main.is_posix_os) 497def fd_leak_sync_from_serf_to_local(sbox): 498 "fd leak during sync from serf to local" 499 import resource 500 resource.setrlimit(resource.RLIMIT_NOFILE, (128, 128)) 501 run_test(sbox, "largemods.dump", is_src_ra_local=None, is_dest_ra_local=True) 502 503#---------------------------------------------------------------------- 504 505@Issue(4476) 506def mergeinfo_contains_r0(sbox): 507 "mergeinfo contains r0" 508 509 def make_node_record(node_name, mi): 510 """Return a dumpfile node-record for adding a (directory) node named 511 NODE_NAME with mergeinfo MI. Return it as a list of newline-terminated 512 lines. 513 """ 514 headers_tmpl = """\ 515Node-path: %s 516Node-kind: dir 517Node-action: add 518Prop-content-length: %d 519Content-length: %d 520""" 521 content_tmpl = """\ 522K 13 523svn:mergeinfo 524V %d 525%s 526PROPS-END 527""" 528 content = content_tmpl % (len(mi), mi) 529 headers = headers_tmpl % (node_name, len(content), len(content)) 530 record = (headers + '\n' + content + '\n\n').encode() 531 return record.splitlines(True) 532 533 # The test case mergeinfo (before, after) syncing, separated here with 534 # spaces instead of newlines 535 test_mi = [ 536 ("", ""), # unchanged 537 ("/a:1", "/a:1"), 538 ("/a:1 /b:1*,2","/a:1 /b:1*,2"), 539 ("/:0:1", "/:0:1"), # unchanged; colon-zero in filename 540 ("/a:0", ""), # dropped entirely 541 ("/a:0*", ""), 542 ("/a:0 /b:0*", ""), 543 ("/a:1 /b:0", "/a:1"), # one kept, one dropped 544 ("/a:0 /b:1", "/b:1"), 545 ("/a:0,1 /b:1", "/a:1 /b:1"), # one kept, one changed 546 ("/a:1 /b:0,1", "/a:1 /b:1"), 547 ("/a:0,1 /b:0*,1 /c:0,2 /d:0-1 /e:0-1,3 /f:0-2 /g:0-3", 548 "/a:1 /b:1 /c:2 /d:1 /e:1,3 /f:1-2 /g:1-3"), # all changed 549 ("/a:0:0-1", "/a:0:1"), # changed; colon-zero in filename 550 ] 551 552 # Get the constant prefix for each dumpfile 553 dump_file_name = "mergeinfo-contains-r0.dump" 554 svnsync_tests_dir = os.path.join(os.path.dirname(sys.argv[0]), 555 'svnsync_tests_data') 556 dump_in = open(os.path.join(svnsync_tests_dir, dump_file_name), 557 'rb').readlines() 558 dump_out = list(dump_in) # duplicate the list 559 560 # Add dumpfile node records containing the test mergeinfo 561 for n, mi in enumerate(test_mi): 562 node_name = "D" + str(n) 563 564 mi_in = mi[0].replace(' ', '\n') 565 mi_out = mi[1].replace(' ', '\n') 566 dump_in.extend(make_node_record(node_name, mi_in)) 567 dump_out.extend(make_node_record(node_name, mi_out)) 568 569 # Run the sync 570 dest_sbox = setup_and_sync(sbox, dump_in, bypass_prop_validation=True) 571 572 # Compare the dump produced by the mirror repository with expected 573 verify_mirror(dest_sbox, dump_out) 574 575def up_to_date_sync(sbox): 576 """sync that does nothing""" 577 578 # An up-to-date mirror. 579 sbox.build(create_wc=False) 580 dest_sbox = sbox.clone_dependent() 581 dest_sbox.build(create_wc=False, empty=True) 582 svntest.actions.enable_revprop_changes(dest_sbox.repo_dir) 583 run_init(dest_sbox.repo_url, sbox.repo_url) 584 run_sync(dest_sbox.repo_url) 585 586 # Another sync should be a no-op 587 svntest.actions.run_and_verify_svnsync([], [], 588 "synchronize", dest_sbox.repo_url) 589 590 591######################################################################## 592# Run the tests 593 594 595# list all tests here, starting with None: 596test_list = [ None, 597 copy_and_modify, 598 copy_from_previous_version_and_modify, 599 copy_from_previous_version, 600 modified_in_place, 601 tag_empty_trunk, 602 tag_trunk_with_dir, 603 tag_trunk_with_file, 604 tag_trunk_with_file2, 605 tag_with_modified_file, 606 dir_prop_change, 607 file_dir_file, 608 copy_parent_modify_prop, 609 detect_meddling, 610 url_encoding, 611 no_author, 612 copy_revprops, 613 only_trunk, 614 only_trunk_A_with_changes, 615 move_and_modify_in_the_same_revision, 616 info_synchronized, 617 info_not_synchronized, 618 copy_bad_line_endings, 619 copy_bad_line_endings2, 620 copy_bad_encoding, 621 delete_svn_props, 622 commit_a_copy_of_root, 623 descend_into_replace, 624 delete_revprops, 625 fd_leak_sync_from_serf_to_local, # calls setrlimit 626 mergeinfo_contains_r0, 627 up_to_date_sync, 628 ] 629 630if __name__ == '__main__': 631 svntest.main.run_tests(test_list) 632 # NOTREACHED 633 634 635### End of file. 636