~svn/ubuntu/oneiric/subversion/ppa

« back to all changes in this revision

Viewing changes to subversion/tests/cmdline/svntest/main.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2006-12-13 17:57:16 UTC
  • mfrom: (1.1.6 upstream) (0.1.3 etch)
  • Revision ID: james.westby@ubuntu.com-20061213175716-2ysv6z4w5dpa2r2f
Tags: 1.4.2dfsg1-2ubuntu1
* Merge with Debian unstable; remaining changes:
  - Create pot file on build.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
#
 
3
#  main.py: a shared, automated test suite for Subversion
 
4
#
 
5
#  Subversion is a tool for revision control.
 
6
#  See http://subversion.tigris.org for more information.
 
7
#
 
8
# ====================================================================
 
9
# Copyright (c) 2000-2004 CollabNet.  All rights reserved.
 
10
#
 
11
# This software is licensed as described in the file COPYING, which
 
12
# you should have received as part of this distribution.  The terms
 
13
# are also available at http://subversion.tigris.org/license-1.html.
 
14
# If newer versions of this license are posted there, you may use a
 
15
# newer version instead, at your option.
 
16
#
 
17
######################################################################
 
18
 
 
19
import sys     # for argv[]
 
20
import os      # for popen2()
 
21
import shutil  # for rmtree()
 
22
import re
 
23
import stat    # for ST_MODE
 
24
import string  # for atof()
 
25
import copy    # for deepcopy()
 
26
import time    # for time()
 
27
import traceback # for print_exc()
 
28
 
 
29
import getopt
 
30
try:
 
31
  my_getopt = getopt.gnu_getopt
 
32
except AttributeError:
 
33
  my_getopt = getopt.getopt
 
34
 
 
35
from svntest import Failure
 
36
from svntest import Skip
 
37
from svntest import testcase
 
38
from svntest import wc
 
39
 
 
40
######################################################################
 
41
#
 
42
#  HOW TO USE THIS MODULE:
 
43
#
 
44
#  Write a new python script that
 
45
#
 
46
#     1) imports this 'svntest' package
 
47
#
 
48
#     2) contains a number of related 'test' routines.  (Each test
 
49
#        routine should take no arguments, and return None on success
 
50
#        or throw a Failure exception on failure.  Each test should
 
51
#        also contain a short docstring.)
 
52
#
 
53
#     3) places all the tests into a list that begins with None.
 
54
#
 
55
#     4) calls svntest.main.client_test() on the list.
 
56
#
 
57
#  Also, your tests will probably want to use some of the common
 
58
#  routines in the 'Utilities' section below.
 
59
#
 
60
#####################################################################
 
61
# Global stuff
 
62
 
 
63
### Grandfather in SVNTreeUnequal, which used to live here.  If you're
 
64
# ever feeling saucy, you could go through the testsuite and change
 
65
# main.SVNTreeUnequal to test.SVNTreeUnequal.
 
66
import tree
 
67
SVNTreeUnequal = tree.SVNTreeUnequal
 
68
 
 
69
class SVNProcessTerminatedBySignal(Failure):
 
70
  "Exception raised if a spawned process segfaulted, aborted, etc."
 
71
  pass
 
72
 
 
73
class SVNLineUnequal(Failure):
 
74
  "Exception raised if two lines are unequal"
 
75
  pass
 
76
 
 
77
class SVNUnmatchedError(Failure):
 
78
  "Exception raised if an expected error is not found"
 
79
  pass
 
80
 
 
81
class SVNCommitFailure(Failure):
 
82
  "Exception raised if a commit failed"
 
83
  pass
 
84
 
 
85
class SVNRepositoryCopyFailure(Failure):
 
86
  "Exception raised if unable to copy a repository"
 
87
  pass
 
88
 
 
89
class SVNRepositoryCreateFailure(Failure):
 
90
  "Exception raised if unable to create a repository"
 
91
  pass
 
92
 
 
93
# Windows specifics
 
94
if sys.platform == 'win32':
 
95
  windows = 1
 
96
  file_scheme_prefix = 'file:///'
 
97
  _exe = '.exe'
 
98
else:
 
99
  windows = 0
 
100
  file_scheme_prefix = 'file://'
 
101
  _exe = ''
 
102
 
 
103
# os.wait() specifics
 
104
try:
 
105
  from os import wait
 
106
  platform_with_os_wait = 1
 
107
except ImportError:
 
108
  platform_with_os_wait = 0
 
109
 
 
110
# The locations of the svn, svnadmin and svnlook binaries, relative to
 
111
# the only scripts that import this file right now (they live in ../).
 
112
svn_binary = os.path.abspath('../../svn/svn' + _exe)
 
113
svnadmin_binary = os.path.abspath('../../svnadmin/svnadmin' + _exe)
 
114
svnlook_binary = os.path.abspath('../../svnlook/svnlook' + _exe)
 
115
svnsync_binary = os.path.abspath('../../svnsync/svnsync' + _exe)
 
116
svnversion_binary = os.path.abspath('../../svnversion/svnversion' + _exe)
 
117
 
 
118
# Username and password used by the working copies
 
119
wc_author = 'jrandom'
 
120
wc_passwd = 'rayjandom'
 
121
 
 
122
# Username and password used by the working copies for "second user"
 
123
# scenarios
 
124
wc_author2 = 'jconstant' # use the same password as wc_author
 
125
 
 
126
# Global variable indicating if we want verbose output.
 
127
verbose_mode = 0
 
128
 
 
129
# Global variable indicating if we want test data cleaned up after success
 
130
cleanup_mode = 0
 
131
 
 
132
# Global URL to testing area.  Default to ra_local, current working dir.
 
133
test_area_url = file_scheme_prefix + os.path.abspath(os.getcwd())
 
134
if windows == 1:
 
135
  test_area_url = string.replace(test_area_url, '\\', '/')
 
136
 
 
137
# Global variable indicating the FS type for repository creations.
 
138
fs_type = None
 
139
 
 
140
# All temporary repositories and working copies are created underneath
 
141
# this dir, so there's one point at which to mount, e.g., a ramdisk.
 
142
work_dir = "svn-test-work"
 
143
 
 
144
# Where we want all the repositories and working copies to live.
 
145
# Each test will have its own!
 
146
general_repo_dir = os.path.join(work_dir, "repositories")
 
147
general_wc_dir = os.path.join(work_dir, "working_copies")
 
148
 
 
149
# A relative path that will always point to latest repository
 
150
current_repo_dir = None
 
151
current_repo_url = None
 
152
 
 
153
# temp directory in which we will create our 'pristine' local
 
154
# repository and other scratch data.  This should be removed when we
 
155
# quit and when we startup.
 
156
temp_dir = os.path.join(work_dir, 'local_tmp')
 
157
 
 
158
# (derivatives of the tmp dir.)
 
159
pristine_dir = os.path.join(temp_dir, "repos")
 
160
greek_dump_dir = os.path.join(temp_dir, "greekfiles")
 
161
config_dir = os.path.abspath(os.path.join(temp_dir, "config"))
 
162
default_config_dir = config_dir
 
163
 
 
164
 
 
165
#
 
166
# Our pristine greek-tree state.
 
167
#
 
168
# If a test wishes to create an "expected" working-copy tree, it should
 
169
# call main.greek_state.copy().  That method will return a copy of this
 
170
# State object which can then be edited.
 
171
#
 
172
_item = wc.StateItem
 
173
greek_state = wc.State('', {
 
174
  'iota'        : _item("This is the file 'iota'.\n"),
 
175
  'A'           : _item(),
 
176
  'A/mu'        : _item("This is the file 'mu'.\n"),
 
177
  'A/B'         : _item(),
 
178
  'A/B/lambda'  : _item("This is the file 'lambda'.\n"),
 
179
  'A/B/E'       : _item(),
 
180
  'A/B/E/alpha' : _item("This is the file 'alpha'.\n"),
 
181
  'A/B/E/beta'  : _item("This is the file 'beta'.\n"),
 
182
  'A/B/F'       : _item(),
 
183
  'A/C'         : _item(),
 
184
  'A/D'         : _item(),
 
185
  'A/D/gamma'   : _item("This is the file 'gamma'.\n"),
 
186
  'A/D/G'       : _item(),
 
187
  'A/D/G/pi'    : _item("This is the file 'pi'.\n"),
 
188
  'A/D/G/rho'   : _item("This is the file 'rho'.\n"),
 
189
  'A/D/G/tau'   : _item("This is the file 'tau'.\n"),
 
190
  'A/D/H'       : _item(),
 
191
  'A/D/H/chi'   : _item("This is the file 'chi'.\n"),
 
192
  'A/D/H/psi'   : _item("This is the file 'psi'.\n"),
 
193
  'A/D/H/omega' : _item("This is the file 'omega'.\n"),
 
194
  })
 
195
 
 
196
 
 
197
######################################################################
 
198
# Utilities shared by the tests
 
199
 
 
200
def get_admin_name():
 
201
  "Return name of SVN administrative subdirectory."
 
202
 
 
203
  if (windows or sys.platform == 'cygwin') \
 
204
      and os.environ.has_key('SVN_ASP_DOT_NET_HACK'):
 
205
    return '_svn'
 
206
  else:
 
207
    return '.svn'
 
208
 
 
209
def get_start_commit_hook_path(repo_dir):
 
210
  "Return the path of the start-commit-hook conf file in REPO_DIR."
 
211
 
 
212
  return os.path.join(repo_dir, "hooks", "start-commit")
 
213
 
 
214
 
 
215
def get_pre_commit_hook_path(repo_dir):
 
216
  "Return the path of the pre-commit-hook conf file in REPO_DIR."
 
217
 
 
218
  return os.path.join(repo_dir, "hooks", "pre-commit")
 
219
 
 
220
 
 
221
def get_post_commit_hook_path(repo_dir):
 
222
  "Return the path of the post-commit-hook conf file in REPO_DIR."
 
223
 
 
224
  return os.path.join(repo_dir, "hooks", "post-commit")
 
225
 
 
226
def get_pre_revprop_change_hook_path(repo_dir):
 
227
  "Return the path of the pre-revprop-change hook script in REPO_DIR."
 
228
 
 
229
  return os.path.join(repo_dir, "hooks", "pre-revprop-change")
 
230
 
 
231
def get_svnserve_conf_file_path(repo_dir):
 
232
  "Return the path of the svnserve.conf file in REPO_DIR."
 
233
 
 
234
  return os.path.join(repo_dir, "conf", "svnserve.conf")
 
235
 
 
236
# Run any binary, logging the command line (TODO: and return code)
 
237
def run_command(command, error_expected, binary_mode=0, *varargs):
 
238
  """Run COMMAND with VARARGS; return stdout, stderr as lists of lines.
 
239
  If ERROR_EXPECTED is None, any stderr also will be printed."""
 
240
 
 
241
  return run_command_stdin(command, error_expected, binary_mode,
 
242
                           None, *varargs)
 
243
 
 
244
# Run any binary, supplying input text, logging the command line
 
245
def run_command_stdin(command, error_expected, binary_mode=0,
 
246
                      stdin_lines=None, *varargs):
 
247
  """Run COMMAND with VARARGS; input STDIN_LINES (a list of strings
 
248
  which should include newline characters) to program via stdin - this
 
249
  should not be very large, as if the program outputs more than the OS
 
250
  is willing to buffer, this will deadlock, with both Python and
 
251
  COMMAND waiting to write to each other for ever.
 
252
  Return stdout, stderr as lists of lines.
 
253
  If ERROR_EXPECTED is None, any stderr also will be printed."""
 
254
 
 
255
  args = ''
 
256
  for arg in varargs:                   # build the command string
 
257
    arg = str(arg)
 
258
    if os.name != 'nt':
 
259
      arg = arg.replace('$', '\$')
 
260
    args = args + ' "' + arg + '"'
 
261
 
 
262
  # Log the command line
 
263
  if verbose_mode:
 
264
    print 'CMD:', os.path.basename(command) + args,
 
265
 
 
266
  if binary_mode:
 
267
    mode = 'b'
 
268
  else:
 
269
    mode = 't'
 
270
 
 
271
  start = time.time()
 
272
  infile, outfile, errfile = os.popen3(command + args, mode)
 
273
 
 
274
  if stdin_lines:
 
275
    map(infile.write, stdin_lines)
 
276
 
 
277
  infile.close()
 
278
 
 
279
  stdout_lines = outfile.readlines()
 
280
  stderr_lines = errfile.readlines()
 
281
 
 
282
  outfile.close()
 
283
  errfile.close()
 
284
 
 
285
  if platform_with_os_wait:
 
286
    pid, wait_code = os.wait()
 
287
 
 
288
    exit_code = int(wait_code / 256)
 
289
    exit_signal = wait_code % 256
 
290
 
 
291
    if exit_signal != 0:
 
292
      raise SVNProcessTerminatedBySignal
 
293
 
 
294
  if verbose_mode:
 
295
    stop = time.time()
 
296
    print '<TIME = %.6f>' % (stop - start)
 
297
 
 
298
  if (not error_expected) and (stderr_lines):
 
299
    map(sys.stdout.write, stderr_lines)
 
300
    raise Failure
 
301
 
 
302
  return stdout_lines, stderr_lines
 
303
 
 
304
def set_config_dir(cfgdir):
 
305
  "Set the config directory."
 
306
 
 
307
  global config_dir
 
308
  config_dir = cfgdir
 
309
 
 
310
def reset_config_dir():
 
311
  "Reset the config directory to the default value."
 
312
 
 
313
  global config_dir
 
314
  global default_config_dir
 
315
 
 
316
  config_dir = default_config_dir
 
317
 
 
318
def create_config_dir(cfgdir,
 
319
                      config_contents = '#\n',
 
320
                      server_contents = '#\n'):
 
321
  "Create config directories and files"
 
322
 
 
323
  # config file names
 
324
  cfgfile_cfg = os.path.join(cfgdir, 'config')
 
325
  cfgfile_srv = os.path.join(cfgdir, 'server')
 
326
 
 
327
  # create the directory
 
328
  if not os.path.isdir(cfgdir):
 
329
    os.makedirs(cfgdir)
 
330
 
 
331
  fd = open(cfgfile_cfg, 'w')
 
332
  fd.write(config_contents)
 
333
  fd.close()
 
334
 
 
335
  fd = open(cfgfile_srv, 'w')
 
336
  fd.write(server_contents)
 
337
  fd.close()
 
338
 
 
339
 
 
340
# For running subversion and returning the output
 
341
def run_svn(error_expected, *varargs):
 
342
  """Run svn with VARARGS; return stdout, stderr as lists of lines.
 
343
  If ERROR_EXPECTED is None, any stderr also will be printed.  If
 
344
  you're just checking that something does/doesn't come out of
 
345
  stdout/stderr, you might want to use actions.run_and_verify_svn()."""
 
346
  global config_dir
 
347
  return run_command(svn_binary, error_expected, 0,
 
348
                     *varargs + ('--config-dir', config_dir))
 
349
 
 
350
# For running svnadmin.  Ignores the output.
 
351
def run_svnadmin(*varargs):
 
352
  "Run svnadmin with VARARGS, returns stdout, stderr as list of lines."
 
353
  return run_command(svnadmin_binary, 1, 0, *varargs)
 
354
 
 
355
# For running svnlook.  Ignores the output.
 
356
def run_svnlook(*varargs):
 
357
  "Run svnlook with VARARGS, returns stdout, stderr as list of lines."
 
358
  return run_command(svnlook_binary, 1, 0, *varargs)
 
359
 
 
360
def run_svnsync(*varargs):
 
361
  "Run svnsync with VARARGS, returns stdout, stderr as list of lines."
 
362
  return run_command(svnsync_binary, 1, 0, *varargs)
 
363
 
 
364
def run_svnversion(*varargs):
 
365
  "Run svnversion with VARARGS, returns stdout, stderr as list of lines."
 
366
  return run_command(svnversion_binary, 1, 0, *varargs)
 
367
 
 
368
# Chmod recursively on a whole subtree
 
369
def chmod_tree(path, mode, mask):
 
370
  def visit(arg, dirname, names):
 
371
    mode, mask = arg
 
372
    for name in names:
 
373
      fullname = os.path.join(dirname, name)
 
374
      if not os.path.islink(fullname):
 
375
        new_mode = (os.stat(fullname)[stat.ST_MODE] & ~mask) | mode
 
376
        os.chmod(fullname, new_mode)
 
377
  os.path.walk(path, visit, (mode, mask))
 
378
 
 
379
# For clearing away working copies
 
380
def safe_rmtree(dirname, retry=0):
 
381
  "Remove the tree at DIRNAME, making it writable first"
 
382
  def rmtree(dirname):
 
383
    chmod_tree(dirname, 0666, 0666)
 
384
    shutil.rmtree(dirname)
 
385
 
 
386
  if not os.path.exists(dirname):
 
387
    return
 
388
 
 
389
  if retry:
 
390
    for delay in (0.5, 1, 2, 4):
 
391
      try:
 
392
        rmtree(dirname)
 
393
        break
 
394
      except:
 
395
        time.sleep(delay)
 
396
    else:
 
397
      rmtree(dirname)
 
398
  else:
 
399
    rmtree(dirname)
 
400
 
 
401
# For making local mods to files
 
402
def file_append(path, new_text):
 
403
  "Append NEW_TEXT to file at PATH"
 
404
 
 
405
  fp = open(path, 'a')  # open in (a)ppend mode
 
406
  fp.write(new_text)
 
407
  fp.close()
 
408
 
 
409
# For making local mods to files
 
410
def file_write(path, new_text):
 
411
  "Replace contents of file at PATH with NEW_TEXT"
 
412
 
 
413
  fp = open(path, 'w')  # open in (w)rite mode
 
414
  fp.write(new_text)
 
415
  fp.close()
 
416
 
 
417
# For creating blank new repositories
 
418
def create_repos(path):
 
419
  """Create a brand-new SVN repository at PATH.  If PATH does not yet
 
420
  exist, create it."""
 
421
 
 
422
  if not(os.path.exists(path)):
 
423
    os.makedirs(path) # this creates all the intermediate dirs, if neccessary
 
424
 
 
425
  opts = ("--bdb-txn-nosync",)
 
426
  if fs_type is not None:
 
427
    opts += ("--fs-type=" + fs_type,)
 
428
  stdout, stderr = run_command(svnadmin_binary, 1, 0, "create", path, *opts)
 
429
 
 
430
  # Skip tests if we can't create the repository.
 
431
  if stderr:
 
432
    for line in stderr:
 
433
      if line.find('Unknown FS type') != -1:
 
434
        raise Skip
 
435
    # If the FS type is known, assume the repos couldn't be created
 
436
    # (e.g. due to a missing 'svnadmin' binary).
 
437
    raise SVNRepositoryCreateFailure("".join(stderr).rstrip())
 
438
 
 
439
  # Allow unauthenticated users to write to the repos, for ra_svn testing.
 
440
  file_append(os.path.join(path, "conf", "svnserve.conf"),
 
441
              "[general]\nauth-access = write\npassword-db = passwd\n");
 
442
  file_append(os.path.join(path, "conf", "passwd"),
 
443
               "[users]\njrandom = rayjandom\njconstant = rayjandom\n");
 
444
  # make the repos world-writeable, for mod_dav_svn's sake.
 
445
  chmod_tree(path, 0666, 0666)
 
446
 
 
447
# For copying a repository
 
448
def copy_repos(src_path, dst_path, head_revision, ignore_uuid = 0):
 
449
  "Copy the repository SRC_PATH, with head revision HEAD_REVISION, to DST_PATH"
 
450
 
 
451
  # A BDB hot-backup procedure would be more efficient, but that would
 
452
  # require access to the BDB tools, and this doesn't.  Print a fake
 
453
  # pipe command so that the displayed CMDs can be run by hand
 
454
  create_repos(dst_path)
 
455
  dump_args = ' dump "' + src_path + '"'
 
456
  load_args = ' load "' + dst_path + '"'
 
457
 
 
458
  if ignore_uuid:
 
459
    load_args = load_args + " --ignore-uuid"
 
460
  if verbose_mode:
 
461
    print 'CMD:', os.path.basename(svnadmin_binary) + dump_args, \
 
462
          '|', os.path.basename(svnadmin_binary) + load_args,
 
463
  start = time.time()
 
464
  dump_in, dump_out, dump_err = os.popen3(svnadmin_binary + dump_args, 'b')
 
465
  load_in, load_out, load_err = os.popen3(svnadmin_binary + load_args, 'b')
 
466
  stop = time.time()
 
467
  if verbose_mode:
 
468
    print '<TIME = %.6f>' % (stop - start)
 
469
 
 
470
  while 1:
 
471
    data = dump_out.read(1024*1024)  # Arbitrary buffer size
 
472
    if data == "":
 
473
      break
 
474
    load_in.write(data)
 
475
  load_in.close() # Tell load we are done
 
476
 
 
477
  dump_lines = dump_err.readlines()
 
478
  load_lines = load_out.readlines()
 
479
  dump_in.close()
 
480
  dump_out.close()
 
481
  dump_err.close()
 
482
  load_out.close()
 
483
  load_err.close()
 
484
 
 
485
  dump_re = re.compile(r'^\* Dumped revision (\d+)\.\r?$')
 
486
  expect_revision = 0
 
487
  for dump_line in dump_lines:
 
488
    match = dump_re.match(dump_line)
 
489
    if not match or match.group(1) != str(expect_revision):
 
490
      print 'ERROR:  dump failed:', dump_line,
 
491
      raise SVNRepositoryCopyFailure
 
492
    expect_revision += 1
 
493
  if expect_revision != head_revision + 1:
 
494
    print 'ERROR:  dump failed; did not see revision', head_revision
 
495
    raise SVNRepositoryCopyFailure
 
496
 
 
497
  load_re = re.compile(r'^------- Committed revision (\d+) >>>\r?$')
 
498
  expect_revision = 1
 
499
  for load_line in load_lines:
 
500
    match = load_re.match(load_line)
 
501
    if match:
 
502
      if match.group(1) != str(expect_revision):
 
503
        print 'ERROR:  load failed:', load_line,
 
504
        raise SVNRepositoryCopyFailure
 
505
      expect_revision += 1
 
506
  if expect_revision != head_revision + 1:
 
507
    print 'ERROR:  load failed; did not see revision', head_revision
 
508
    raise SVNRepositoryCopyFailure
 
509
 
 
510
 
 
511
def set_repos_paths(repo_dir):
 
512
  "Set current_repo_dir and current_repo_url from a relative path to the repo."
 
513
  global current_repo_dir, current_repo_url
 
514
  current_repo_dir = repo_dir
 
515
  current_repo_url = test_area_url + '/' + repo_dir
 
516
  if windows == 1:
 
517
    current_repo_url = string.replace(current_repo_url, '\\', '/')
 
518
 
 
519
 
 
520
def canonicalize_url(input):
 
521
  "Canonicalize the url, if the scheme is unknown, returns intact input"
 
522
 
 
523
  m = re.match(r"^((file://)|((svn|svn\+ssh|http|https)(://)))", input)
 
524
  if m:
 
525
    scheme = m.group(1)
 
526
    return scheme + re.sub(r'//*', '/', input[len(scheme):])
 
527
  else:
 
528
    return input
 
529
 
 
530
 
 
531
def create_python_hook_script (hook_path, hook_script_code):
 
532
  """Create a Python hook script at HOOK_PATH with the specified
 
533
     HOOK_SCRIPT_CODE."""
 
534
 
 
535
  if sys.platform == 'win32':
 
536
    # Use an absolute path since the working directory is not guaranteed
 
537
    hook_path = os.path.abspath(hook_path)
 
538
    # Fill the python file.
 
539
    file_append ("%s.py" % hook_path, hook_script_code)
 
540
    # Fill the batch wrapper file.
 
541
    file_append ("%s.bat" % hook_path,
 
542
                 "@\"%s\" %s.py\n" % (sys.executable, hook_path))
 
543
  else:
 
544
    # For all other platforms
 
545
    file_append (hook_path, "#!%s\n%s" % (sys.executable, hook_script_code))
 
546
    os.chmod (hook_path, 0755)
 
547
 
 
548
 
 
549
def compare_unordered_output(expected, actual):
 
550
  """Compare lists of output lines for equality disregarding the
 
551
     order of the lines"""
 
552
  if len(actual) != len(expected):
 
553
    raise Failure("Length of expected output not equal to actual length")
 
554
  for aline in actual:
 
555
    try:
 
556
      i = expected.index(aline)
 
557
      expected.pop(i)
 
558
    except ValueError:
 
559
      raise Failure("Expected output does not match actual output")
 
560
 
 
561
 
 
562
######################################################################
 
563
# Sandbox handling
 
564
 
 
565
class Sandbox:
 
566
  """Manages a sandbox (one or more repository/working copy pairs) for
 
567
  a test to operate within."""
 
568
 
 
569
  dependents = None
 
570
 
 
571
  def __init__(self, module, idx):
 
572
    self._set_name("%s-%d" % (module, idx))
 
573
 
 
574
  def _set_name(self, name):
 
575
    """A convenience method for renaming a sandbox, useful when
 
576
    working with multiple repositories in the same unit test."""
 
577
    self.name = name
 
578
    self.wc_dir = os.path.join(general_wc_dir, self.name)
 
579
    self.repo_dir = os.path.join(general_repo_dir, self.name)
 
580
    self.repo_url = test_area_url + '/' + self.repo_dir
 
581
 
 
582
    # For dav tests we need a single authz file which must be present,
 
583
    # so we recreate it each time a sandbox is created with some default
 
584
    # contents.
 
585
    if self.repo_url.startswith("http"):
 
586
      # this dir doesn't exist out of the box, so we may have to make it
 
587
      if not(os.path.exists(work_dir)):
 
588
        os.makedirs(work_dir)
 
589
      self.authz_file = os.path.join(work_dir, "authz")
 
590
      fp = open(self.authz_file, "w")
 
591
      fp.write("[/]\n* = rw\n")
 
592
      fp.close()
 
593
 
 
594
    # For svnserve tests we have a per-repository authz file, and it
 
595
    # doesn't need to be there in order for things to work, so we don't
 
596
    # have any default contents.
 
597
    elif self.repo_url.startswith("svn"):
 
598
      self.authz_file = os.path.join(self.repo_dir, "conf", "authz")
 
599
 
 
600
    if windows == 1:
 
601
      self.repo_url = string.replace(self.repo_url, '\\', '/')
 
602
    self.test_paths = [self.wc_dir, self.repo_dir]
 
603
 
 
604
  def clone_dependent(self):
 
605
    """A convenience method for creating a near-duplicate of this
 
606
    sandbox, useful when working with multiple repositories in the
 
607
    same unit test.  Any necessary cleanup operations are triggered
 
608
    by cleanup of the original sandbox."""
 
609
    if not self.dependents:
 
610
      self.dependents = []
 
611
    self.dependents.append(copy.deepcopy(self))
 
612
    self.dependents[-1]._set_name("%s-%d" % (self.name, len(self.dependents)))
 
613
    return self.dependents[-1]
 
614
 
 
615
  def build(self, name = None, create_wc = True):
 
616
    if name != None:
 
617
      self._set_name(name)
 
618
    if actions.make_repo_and_wc(self, create_wc):
 
619
      raise Failure("Could not build repository and sandbox '%s'" % self.name)
 
620
 
 
621
  def add_test_path(self, path, remove=1):
 
622
    self.test_paths.append(path)
 
623
    if remove:
 
624
      safe_rmtree(path)
 
625
 
 
626
  def add_repo_path(self, suffix, remove=1):
 
627
    path = self.repo_dir + '.' + suffix
 
628
    url  = self.repo_url + '.' + suffix
 
629
    self.add_test_path(path, remove)
 
630
    return path, url
 
631
 
 
632
  def add_wc_path(self, suffix, remove=1):
 
633
    path = self.wc_dir + '.' + suffix
 
634
    self.add_test_path(path, remove)
 
635
    return path
 
636
 
 
637
  def cleanup_test_paths(self):
 
638
    "Clean up detritus from this sandbox, and any dependents."
 
639
    if self.dependents:
 
640
      # Recursively cleanup any dependent sandboxes.
 
641
      for sbox in self.dependents:
 
642
        sbox.cleanup_test_paths()
 
643
    for path in self.test_paths:
 
644
      _cleanup_test_path(path)
 
645
 
 
646
 
 
647
_deferred_test_paths = []
 
648
def _cleanup_deferred_test_paths():
 
649
  global _deferred_test_paths
 
650
  test_paths = _deferred_test_paths[:]
 
651
  _deferred_test_paths = []
 
652
  for path in test_paths:
 
653
    _cleanup_test_path(path, 1)
 
654
 
 
655
def _cleanup_test_path(path, retrying=None):
 
656
  if verbose_mode:
 
657
    if retrying:
 
658
      print "CLEANUP: RETRY:", path
 
659
    else:
 
660
      print "CLEANUP:", path
 
661
  try:
 
662
    safe_rmtree(path)
 
663
  except:
 
664
    if verbose_mode:
 
665
      print "WARNING: cleanup failed, will try again later"
 
666
    _deferred_test_paths.append(path)
 
667
 
 
668
 
 
669
class TestRunner:
 
670
  """Encapsulate a single test case (predicate), including logic for
 
671
  runing the test and test list output."""
 
672
 
 
673
  def __init__(self, func, index):
 
674
    self.pred = testcase.create_test_case(func)
 
675
    self.index = index
 
676
 
 
677
  def list(self):
 
678
    print " %2d     %-5s  %s" % (self.index,
 
679
                                 self.pred.list_mode(),
 
680
                                 self.pred.get_description())
 
681
    self.pred.check_description()
 
682
 
 
683
  def _print_name(self):
 
684
    print os.path.basename(sys.argv[0]), str(self.index) + ":", \
 
685
          self.pred.get_description()
 
686
    self.pred.check_description()
 
687
 
 
688
  def run(self):
 
689
    """Run self.pred and return the result.  The return value is
 
690
        - 0 if the test was successful
 
691
        - 1 if it errored in a way that indicates test failure
 
692
        - 2 if the test skipped
 
693
        """
 
694
    if self.pred.need_sandbox():
 
695
      # ooh! this function takes a sandbox argument
 
696
      sandbox = Sandbox(self.pred.get_sandbox_name(), self.index)
 
697
      args = (sandbox,)
 
698
    else:
 
699
      sandbox = None
 
700
      args = ()
 
701
 
 
702
    try:
 
703
      rc = self.pred.run(args)
 
704
      if rc is not None:
 
705
        print 'STYLE ERROR in',
 
706
        self._print_name()
 
707
        print 'Test driver returned a status code.'
 
708
        sys.exit(255)
 
709
      result = 0
 
710
    except Skip, ex:
 
711
      result = 2
 
712
    except Failure, ex:
 
713
      result = 1
 
714
      # We captured Failure and its subclasses. We don't want to print
 
715
      # anything for plain old Failure since that just indicates test
 
716
      # failure, rather than relevant information. However, if there
 
717
      # *is* information in the exception's arguments, then print it.
 
718
      if ex.__class__ != Failure or ex.args:
 
719
        ex_args = str(ex)
 
720
        if ex_args:
 
721
          print 'EXCEPTION: %s: %s' % (ex.__class__.__name__, ex_args)
 
722
        else:
 
723
          print 'EXCEPTION:', ex.__class__.__name__
 
724
    except KeyboardInterrupt:
 
725
      print 'Interrupted'
 
726
      sys.exit(0)
 
727
    except SystemExit, ex:
 
728
      print 'EXCEPTION: SystemExit(%d), skipping cleanup' % ex.code
 
729
      print ex.code and 'FAIL: ' or 'PASS: ',
 
730
      self._print_name()
 
731
      raise
 
732
    except:
 
733
      result = 1
 
734
      print 'UNEXPECTED EXCEPTION:'
 
735
      traceback.print_exc(file=sys.stdout)
 
736
    result = self.pred.convert_result(result)
 
737
    print self.pred.run_text(result),
 
738
    self._print_name()
 
739
    sys.stdout.flush()
 
740
    if sandbox is not None and result != 1 and cleanup_mode:
 
741
      sandbox.cleanup_test_paths()
 
742
    return result
 
743
 
 
744
 
 
745
######################################################################
 
746
# Main testing functions
 
747
 
 
748
# These two functions each take a TEST_LIST as input.  The TEST_LIST
 
749
# should be a list of test functions; each test function should take
 
750
# no arguments and return a 0 on success, non-zero on failure.
 
751
# Ideally, each test should also have a short, one-line docstring (so
 
752
# it can be displayed by the 'list' command.)
 
753
 
 
754
# Func to run one test in the list.
 
755
def run_one_test(n, test_list):
 
756
  "Run the Nth client test in TEST_LIST, return the result."
 
757
 
 
758
  if (n < 1) or (n > len(test_list) - 1):
 
759
    print "There is no test", `n` + ".\n"
 
760
    return 1
 
761
 
 
762
  # Clear the repos paths for this test
 
763
  global current_repo_dir, current_repo_url
 
764
  current_repo_dir = None
 
765
  current_repo_url = None
 
766
 
 
767
  # Run the test.
 
768
  exit_code = TestRunner(test_list[n], n).run()
 
769
  reset_config_dir()
 
770
  return exit_code
 
771
 
 
772
def _internal_run_tests(test_list, testnums):
 
773
  """Run the tests from TEST_LIST whose indices are listed in TESTNUMS."""
 
774
 
 
775
  exit_code = 0
 
776
 
 
777
  for testnum in testnums:
 
778
    # 1 is the only return code that indicates actual test failure.
 
779
    if run_one_test(testnum, test_list) == 1:
 
780
      exit_code = 1
 
781
 
 
782
  _cleanup_deferred_test_paths()
 
783
  return exit_code
 
784
 
 
785
 
 
786
# Main func.  This is the "entry point" that all the test scripts call
 
787
# to run their list of tests.
 
788
#
 
789
# This routine parses sys.argv to decide what to do.  Basic usage:
 
790
#
 
791
# test-script.py [--list] [<testnum>]...
 
792
#
 
793
# --list : Option to print the docstrings for the chosen tests
 
794
# instead of running them.
 
795
#
 
796
# [<testnum>]... : the numbers of the tests that should be run.  If no
 
797
# testnums are specified, then all tests in TEST_LIST are run.
 
798
def run_tests(test_list):
 
799
  """Main routine to run all tests in TEST_LIST.
 
800
 
 
801
  NOTE: this function does not return. It does a sys.exit() with the
 
802
        appropriate exit code.
 
803
  """
 
804
 
 
805
  global test_area_url
 
806
  global fs_type
 
807
  global verbose_mode
 
808
  global cleanup_mode
 
809
  testnums = []
 
810
  # Should the tests be listed (as opposed to executed)?
 
811
  list_tests = 0
 
812
 
 
813
  # Explicitly set this so that commands that commit but don't supply a
 
814
  # log message will fail rather than invoke an editor.
 
815
  os.environ['SVN_EDITOR'] = ''
 
816
 
 
817
  opts, args = my_getopt(sys.argv[1:], 'v',
 
818
                         ['url=', 'fs-type=', 'verbose', 'cleanup', 'list'])
 
819
 
 
820
  for arg in args:
 
821
    if arg == "list":
 
822
      # This is an old deprecated variant of the "--list" option:
 
823
      list_tests = 1
 
824
    elif arg.startswith('BASE_URL='):
 
825
      test_area_url = arg[9:]
 
826
    else:
 
827
      testnums.append(int(arg))
 
828
 
 
829
  for opt, val in opts:
 
830
    if opt == "--url":
 
831
      test_area_url = val
 
832
 
 
833
    elif opt == "--fs-type":
 
834
      fs_type = val
 
835
 
 
836
    elif opt == "-v" or opt == "--verbose":
 
837
      verbose_mode = 1
 
838
 
 
839
    elif opt == "--cleanup":
 
840
      cleanup_mode = 1
 
841
 
 
842
    elif opt == "--list":
 
843
      list_tests = 1
 
844
 
 
845
  if test_area_url[-1:] == '/': # Normalize url to have no trailing slash
 
846
    test_area_url = test_area_url[:-1]
 
847
 
 
848
  if not testnums:
 
849
    # If no test numbers were listed explicitly, include all of them:
 
850
    testnums = range(1, len(test_list))
 
851
 
 
852
  if list_tests:
 
853
    print "Test #  Mode   Test Description"
 
854
    print "------  -----  ----------------"
 
855
    for testnum in testnums:
 
856
      TestRunner(test_list[testnum], testnum).list()
 
857
 
 
858
    # done. just exit with success.
 
859
    sys.exit(0)
 
860
 
 
861
  else:
 
862
    exit_code = _internal_run_tests(test_list, testnums)
 
863
 
 
864
    # remove all scratchwork: the 'pristine' repository, greek tree, etc.
 
865
    # This ensures that an 'import' will happen the next time we run.
 
866
    safe_rmtree(temp_dir)
 
867
 
 
868
    _cleanup_deferred_test_paths()
 
869
 
 
870
    # return the appropriate exit code from the tests.
 
871
    sys.exit(exit_code)
 
872
 
 
873
 
 
874
######################################################################
 
875
# Initialization
 
876
 
 
877
# Cleanup: if a previous run crashed or interrupted the python
 
878
# interpreter, then `temp_dir' was never removed.  This can cause wonkiness.
 
879
 
 
880
safe_rmtree(temp_dir)
 
881
 
 
882
# the modules import each other, so we do this import very late, to ensure
 
883
# that the definitions in "main" have been completed.
 
884
import actions
 
885
 
 
886
 
 
887
### End of file.