<commit_before>#
#  main.py: a shared, automated test suite for Subversion
#
#  Subversion is a tool for revision control.
#  See http://subversion.tigris.org for more information.
#
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
######################################################################

import sys     # for argv[]
import os
import shutil  # for rmtree()
import re
import stat    # for ST_MODE
import subprocess
import copy    # for deepcopy()
import time    # for time()
import traceback # for print_exc()
import threading
import optparse # for argument parsing
import xml
import urllib

try:
  # Python >=3.0
  import queue
  from urllib.parse import quote as urllib_parse_quote
  from urllib.parse import unquote as urllib_parse_unquote
except ImportError:
  # Python <3.0
  import Queue as queue
  from urllib import quote as urllib_parse_quote
  from urllib import unquote as urllib_parse_unquote

import svntest
from svntest import Failure
from svntest import Skip


######################################################################
#
#  HOW TO USE THIS MODULE:
#
#  Write a new python script that
#
#     1) imports this 'svntest' package
#
#     2) contains a number of related 'test' routines.  (Each test
#        routine should take no arguments, and return None on success
#        or throw a Failure exception on failure.  Each test should
#        also contain a short docstring.)
#
#     3) places all the tests into a list that begins with None.
#
#     4) calls svntest.main.client_test() on the list.
#
#  Also, your tests will probably want to use some of the common
#  routines in the 'Utilities' section below.
#
#####################################################################
# Global stuff

default_num_threads = 5

class SVNProcessTerminatedBySignal(Failure):
  "Exception raised if a spawned process segfaulted, aborted, etc."
  pass

class SVNLineUnequal(Failure):
  "Exception raised if two lines are unequal"
  pass

class SVNUnmatchedError(Failure):
  "Exception raised if an expected error is not found"
  pass

class SVNCommitFailure(Failure):
  "Exception raised if a commit failed"
  pass

class SVNRepositoryCopyFailure(Failure):
  "Exception raised if unable to copy a repository"
  pass

class SVNRepositoryCreateFailure(Failure):
  "Exception raised if unable to create a repository"
  pass

# Windows specifics
if sys.platform == 'win32':
  windows = True
  file_scheme_prefix = 'file:///'
  _exe = '.exe'
  _bat = '.bat'
  os.environ['SVN_DBG_STACKTRACES_TO_STDERR'] = 'y'
else:
  windows = False
  file_scheme_prefix = 'file://'
  _exe = ''
  _bat = ''

# The location of our mock svneditor script.
if windows:
  svneditor_script = os.path.join(sys.path[0], 'svneditor.bat')
else:
  svneditor_script = os.path.join(sys.path[0], 'svneditor.py')

# Username and password used by the working copies
wc_author = 'jrandom'
wc_passwd = '<PASSWORD>'

# Username and password used by the working copies for "second user"
# scenarios
wc_author2 = 'jconstant' # use the same password as wc_author

# Set C locale for command line programs
os.environ['LC_ALL'] = 'C'

# This function mimics the Python 2.3 urllib function of the same name.
def pathname2url(path):
  """Convert the pathname PATH from the local syntax for a path to the form
  used in the path component of a URL. This does not produce a complete URL.
  The return value will already be quoted using the quote() function."""

  # Don't leave ':' in file://C%3A/ escaped as our canonicalization
  # rules will replace this with a ':' on input.
  return urllib_parse_quote(path.replace('\\', '/')).replace('%3A', ':')

# This function mimics the Python 2.3 urllib function of the same name.
def url2pathname(path):
  """Convert the path component PATH from an encoded URL to the local syntax
  for a path. This does not accept a complete URL. This function uses
  unquote() to decode PATH."""
  return os.path.normpath(urllib_parse_unquote(path))

######################################################################
# The locations of the svn, svnadmin and svnlook binaries, relative to
# the only scripts that import this file right now (they live in ../).
# Use --bin to override these defaults.
svn_binary = os.path.abspath('../../svn/svn' + _exe)
svnadmin_binary = os.path.abspath('../../svnadmin/svnadmin' + _exe)
svnlook_binary = os.path.abspath('../../svnlook/svnlook' + _exe)
svnrdump_binary = os.path.abspath('../../svnrdump/svnrdump' + _exe)
svnsync_binary = os.path.abspath('../../svnsync/svnsync' + _exe)
svnversion_binary = os.path.abspath('../../svnversion/svnversion' + _exe)
svndumpfilter_binary = os.path.abspath('../../svndumpfilter/svndumpfilter' + \
                                       _exe)
entriesdump_binary = os.path.abspath('entries-dump' + _exe)
atomic_ra_revprop_change_binary = os.path.abspath('atomic-ra-revprop-change' + \
                                                  _exe)

# Location to the pristine repository, will be calculated from test_area_url
# when we know what the user specified for --url.
pristine_greek_repos_url = None

# Global variable to track all of our options
options = None

# End of command-line-set global variables.
######################################################################

# All temporary repositories and working copies are created underneath
# this dir, so there's one point at which to mount, e.g., a ramdisk.
work_dir = "svn-test-work"

# Constant for the merge info property.
SVN_PROP_MERGEINFO = "svn:mergeinfo"

# Where we want all the repositories and working copies to live.
# Each test will have its own!
general_repo_dir = os.path.join(work_dir, "repositories")
general_wc_dir = os.path.join(work_dir, "working_copies")

# temp directory in which we will create our 'pristine' local
# repository and other scratch data.  This should be removed when we
# quit and when we startup.
temp_dir = os.path.join(work_dir, 'local_tmp')

# (derivatives of the tmp dir.)
pristine_greek_repos_dir = os.path.join(temp_dir, "repos")
greek_dump_dir = os.path.join(temp_dir, "greekfiles")
default_config_dir = os.path.abspath(os.path.join(temp_dir, "config"))

#
# Our pristine greek-tree state.
#
# If a test wishes to create an "expected" working-copy tree, it should
# call main.greek_state.copy().  That method will return a copy of this
# State object which can then be edited.
#
_item = svntest.wc.StateItem
greek_state = svntest.wc.State('', {
  'iota'        : _item("This is the file 'iota'.\n"),
  'A'           : _item(),
  'A/mu'        : _item("This is the file 'mu'.\n"),
  'A/B'         : _item(),
  'A/B/lambda'  : _item("This is the file 'lambda'.\n"),
  'A/B/E'       : _item(),
  'A/B/E/alpha' : _item("This is the file 'alpha'.\n"),
  'A/B/E/beta'  : _item("This is the file 'beta'.\n"),
  'A/B/F'       : _item(),
  'A/C'         : _item(),
  'A/D'         : _item(),
  'A/D/gamma'   : _item("This is the file 'gamma'.\n"),
  'A/D/G'       : _item(),
  'A/D/G/pi'    : _item("This is the file 'pi'.\n"),
  'A/D/G/rho'   : _item("This is the file 'rho'.\n"),
  'A/D/G/tau'   : _item("This is the file 'tau'.\n"),
  'A/D/H'       : _item(),
  'A/D/H/chi'   : _item("This is the file 'chi'.\n"),
  'A/D/H/psi'   : _item("This is the file 'psi'.\n"),
  'A/D/H/omega' : _item("This is the file 'omega'.\n"),
  })


######################################################################
# Utilities shared by the tests
def wrap_ex(func):
  "Wrap a function, catch, print and ignore exceptions"
  def w(*args, **kwds):
    try:
      return func(*args, **kwds)
    except Failure, ex:
      if ex.__class__ != Failure or ex.args:
        ex_args = str(ex)
        if ex_args:
          print('EXCEPTION: %s: %s' % (ex.__class__.__name__, ex_args))
        else:
          print('EXCEPTION: %s' % ex.__class__.__name__)
  return w

def setup_development_mode():
  "Wraps functions in module actions"
  l = [ 'run_and_verify_svn',
        'run_and_verify_svnversion',
        'run_and_verify_load',
        'run_and_verify_dump',
        'run_and_verify_checkout',
        'run_and_verify_export',
        'run_and_verify_update',
        'run_and_verify_merge',
        'run_and_verify_switch',
        'run_and_verify_commit',
        'run_and_verify_unquiet_status',
        'run_and_verify_status',
        'run_and_verify_diff_summarize',
        'run_and_verify_diff_summarize_xml',
        'run_and_validate_lock']

  for func in l:
    setattr(svntest.actions, func, wrap_ex(getattr(svntest.actions, func)))

def get_admin_name():
  "Return name of SVN administrative subdirectory."

  if (windows or sys.platform == 'cygwin') \
      and 'SVN_ASP_DOT_NET_HACK' in os.environ:
    return '_svn'
  else:
    return '.svn'

def wc_is_singledb(wcpath):
  """Temporary function that checks whether a working copy directory looks
  like it is part of a single-db working copy."""

  pristine = os.path.join(wcpath, get_admin_name(), 'pristine')
  if not os.path.exists(pristine):
    return True

  # Now we must be looking at a multi-db WC dir or the root dir of a
  # single-DB WC.  Sharded 'pristine' dir => single-db, else => multi-db.
  for name in os.listdir(pristine):
    if len(name) == 2:
      return True
    elif len(name) == 40:
      return False

  return False

def get_start_commit_hook_path(repo_dir):
  "Return the path of the start-commit-hook conf file in REPO_DIR."

  return os.path.join(repo_dir, "hooks", "start-commit")


def get_pre_commit_hook_path(repo_dir):
  "Return the path of the pre-commit-hook conf file in REPO_DIR."

  return os.path.join(repo_dir, "hooks", "pre-commit")


def get_post_commit_hook_path(repo_dir):
  "Return the path of the post-commit-hook conf file in REPO_DIR."

  return os.path.join(repo_dir, "hooks", "post-commit")

def get_pre_revprop_change_hook_path(repo_dir):
  "Return the path of the pre-revprop-change hook script in REPO_DIR."

  return os.path.join(repo_dir, "hooks", "pre-revprop-change")

def get_svnserve_conf_file_path(repo_dir):
  "Return the path of the svnserve.conf file in REPO_DIR."

  return os.path.join(repo_dir, "conf", "svnserve.conf")

def get_fsfs_conf_file_path(repo_dir):
  "Return the path of the fsfs.conf file in REPO_DIR."

  return os.path.join(repo_dir, "db", "fsfs.conf")

def get_fsfs_format_file_path(repo_dir):
  "Return the path of the format file in REPO_DIR."

  return os.path.join(repo_dir, "db", "format")

# Run any binary, logging the command line and return code
def run_command(command, error_expected, binary_mode=0, *varargs):
  """Run COMMAND with VARARGS. Return exit code as int; stdout, stderr
  as lists of lines (including line terminators).  See run_command_stdin()
  for details.  If ERROR_EXPECTED is None, any stderr output will be
  printed and any stderr output or a non-zero exit code will raise an
  exception."""

  return run_command_stdin(command, error_expected, 0, binary_mode,
                           None, *varargs)

# A regular expression that matches arguments that are trivially safe
# to pass on a command line without quoting on any supported operating
# system:
_safe_arg_re = re.compile(r'^[A-Za-z\d\.\_\/\-\:\@]+$')

def _quote_arg(arg):
  """Quote ARG for a command line.

  Return a quoted version of the string ARG, or just ARG if it contains
  only universally harmless characters.

  WARNING: This function cannot handle arbitrary command-line
  arguments: it is just good enough for what we need here."""

  arg = str(arg)
  if _safe_arg_re.match(arg):
    return arg

  if windows:
    # Note: subprocess.list2cmdline is Windows-specific.
    return subprocess.list2cmdline([arg])
  else:
    # Quoting suitable for most Unix shells.
    return "'" + arg.replace("'", "'\\''") + "'"

def open_pipe(command, bufsize=0, stdin=None, stdout=None, stderr=None):
  """Opens a subprocess.Popen pipe to COMMAND using STDIN,
  STDOUT, and STDERR.  BUFSIZE is passed to subprocess.Popen's
  argument of the same name.

  Returns (infile, outfile, errfile, waiter); waiter
  should be passed to wait_on_pipe."""
  command = [str(x) for x in command]

  # On Windows subprocess.Popen() won't accept a Python script as
  # a valid program to execute, rather it wants the Python executable.
  if (sys.platform == 'win32') and (command[0].endswith('.py')):
    command.insert(0, sys.executable)

  command_string = command[0] + ' ' + ' '.join(map(_quote_arg, command[1:]))

  if not stdin:
    stdin = subprocess.PIPE
  if not stdout:
    stdout = subprocess.PIPE
  if not stderr:
    stderr = subprocess.PIPE

  p = subprocess.Popen(command,
                       bufsize,
                       stdin=stdin,
                       stdout=stdout,
                       stderr=stderr,
                       close_fds=not windows)
  return p.stdin, p.stdout, p.stderr, (p, command_string)

def wait_on_pipe(waiter, binary_mode, stdin=None):
  """WAITER is (KID, COMMAND_STRING).  Wait for KID (opened with open_pipe)
  to finish, dying if it does.  If KID fails, create an error message
  containing any stdout and stderr from the kid.  Show COMMAND_STRING in
  diagnostic messages.  Normalize Windows line endings of stdout and stderr
  if not BINARY_MODE.  Return KID's exit code as int; stdout, stderr as
  lists of lines (including line terminators)."""
  if waiter is None:
    return

  kid, command_string = waiter
  stdout, stderr = kid.communicate(stdin)
  exit_code = kid.returncode

  # Normalize Windows line endings if in text mode.
  if windows and not binary_mode:
    stdout = stdout.replace('\r\n', '\n')
    stderr = stderr.replace('\r\n', '\n')

  # Convert output strings to lists.
  stdout_lines = stdout.splitlines(True)
  stderr_lines = stderr.splitlines(True)

  if exit_code < 0:
    if not windows:
      exit_signal = os.WTERMSIG(-exit_code)
    else:
      exit_signal = exit_code

    if stdout_lines is not None:
      sys.stdout.write("".join(stdout_lines))
      sys.stdout.flush()
    if stderr_lines is not None:
      sys.stderr.write("".join(stderr_lines))
      sys.stderr.flush()
    if options.verbose:
      # show the whole path to make it easier to start a debugger
      sys.stderr.write("CMD: %s terminated by signal %d\n"
                       % (command_string, exit_signal))
      sys.stderr.flush()
    raise SVNProcessTerminatedBySignal
  else:
    if exit_code and options.verbose:
      sys.stderr.write("CMD: %s exited with %d\n"
                       % (command_string, exit_code))
      sys.stderr.flush()
    return stdout_lines, stderr_lines, exit_code

def spawn_process(command, bufsize=0, binary_mode=0, stdin_lines=None,
                  *varargs):
  """Run any binary, supplying input text, logging the command line.
  BUFSIZE dictates the pipe buffer size used in communication with the
  subprocess: 0 means unbuffered, 1 means line buffered, any other
  positive value means use a buffer of (approximately) that size.
  A negative bufsize means to use the system default, which usually
  means fully buffered. The default value for bufsize is 0 (unbuffered).
  Normalize Windows line endings of stdout and stderr if not BINARY_MODE.
  Return exit code as int; stdout, stderr as lists of lines (including
  line terminators)."""
  if stdin_lines and not isinstance(stdin_lines, list):
    raise TypeError("stdin_lines should have list type")

  # Log the command line
  if options.verbose and not command.endswith('.py'):
    sys.stdout.write('CMD: %s %s\n' % (os.path.basename(command),
                                      ' '.join([_quote_arg(x) for x in varargs])))
    sys.stdout.flush()

  infile, outfile, errfile, kid = open_pipe([command] + list(varargs), bufsize)

  if stdin_lines:
    for x in stdin_lines:
      infile.write(x)

  stdout_lines, stderr_lines, exit_code = wait_on_pipe(kid, binary_mode)
  infile.close()

  outfile.close()
  errfile.close()

  return exit_code, stdout_lines, stderr_lines

def run_command_stdin(command, error_expected, bufsize=0, binary_mode=0,
                      stdin_lines=None, *varargs):
  """Run COMMAND with VARARGS; input STDIN_LINES (a list of strings
  which should include newline characters) to program via stdin - this
  should not be very large, as if the program outputs more than the OS
  is willing to buffer, this will deadlock, with both Python and
  COMMAND waiting to write to each other for ever.  For tests where this
  is a problem, setting BUFSIZE to a sufficiently large value will prevent
  the deadlock, see spawn_process().
  Normalize Windows line endings of stdout and stderr if not BINARY_MODE.
  Return exit code as int; stdout, stderr as lists of lines (including
  line terminators).
  If ERROR_EXPECTED is None, any stderr output will be printed and any
  stderr output or a non-zero exit code will raise an exception."""

  if options.verbose:
    start = time.time()

  exit_code, stdout_lines, stderr_lines = spawn_process(command,
                                                        bufsize,
                                                        binary_mode,
                                                        stdin_lines,
                                                        *varargs)

  if options.verbose:
    stop = time.time()
    print('<TIME = %.6f>' % (stop - start))
    for x in stdout_lines:
      sys.stdout.write(x)
    for x in stderr_lines:
      sys.stdout.write(x)

  if (not error_expected) and ((stderr_lines) or (exit_code != 0)):
    if not options.verbose:
      for x in stderr_lines:
        sys.stdout.write(x)
    raise Failure

  return exit_code, \
         [line for line in stdout_lines if not line.startswith("DBG:")], \
         stderr_lines

def create_config_dir(cfgdir, config_contents=None, server_contents=None):
  "Create config directories and files"

  # config file names
  cfgfile_cfg = os.path.join(cfgdir, 'config')
  cfgfile_srv = os.path.join(cfgdir, 'servers')

  # create the directory
  if not os.path.isdir(cfgdir):
    os.makedirs(cfgdir)

  # define default config file contents if none provided
  if config_contents is None:
    config_contents = """
#
[auth]
password-stores =

[miscellany]
interactive-conflicts = false
"""

  # define default server file contents if none provided
  if server_contents is None:
    http_library_str = ""
    if options.http_library:
      http_library_str = "http-library=%s" % (options.http_library)
    server_contents = """
#
[global]
%s
store-plaintext-passwords=yes
store-passwords=yes
""" % (http_library_str)

  file_write(cfgfile_cfg, config_contents)
  file_write(cfgfile_srv, server_contents)

def _with_config_dir(args):
  if '--config-dir' in args:
    return args
  else:
    return args + ('--config-dir', default_config_dir)

def _with_auth(args):
  assert '--password' not in args
  args = args + ('--password', wc_passwd,
                 '--no-auth-cache' )
  if '--username' in args:
    return args
  else:
    return args + ('--username', wc_author )

# For running subversion and returning the output
def run_svn(error_expected, *varargs):
  """Run svn with VARARGS; return exit code as int; stdout, stderr as
  lists of lines (including line terminators).  If ERROR_EXPECTED is
  None, any stderr output will be printed and any stderr output or a
  non-zero exit code will raise an exception.  If
  you're just checking that something does/doesn't come out of
  stdout/stderr, you might want to use actions.run_and_verify_svn()."""
  return run_command(svn_binary, error_expected, 0,
                     *(_with_auth(_with_config_dir(varargs))))

# For running svnadmin.  Ignores the output.
def run_svnadmin(*varargs):
  """Run svnadmin with VARARGS, returns exit code as int; stdout, stderr as
  list of lines (including line terminators)."""
  return run_command(svnadmin_binary, 1, 0, *varargs)

# For running svnlook.  Ignores the output.
def run_svnlook(*varargs):
  """Run svnlook with VARARGS, returns exit code as int; stdout, stderr as
  list of lines (including line terminators)."""
  return run_command(svnlook_binary, 1, 0, *varargs)

def run_svnrdump(stdin_input, *varargs):
  """Run svnrdump with VARARGS, returns exit code as int; stdout, stderr as
  list of lines (including line terminators).  Use binary mode for output."""
  if stdin_input:
    return run_command_stdin(svnrdump_binary, 0, 1, 1, stdin_input,
                             *(_with_auth(_with_config_dir(varargs))))
  else:
    return run_command(svnrdump_binary, 1, 1,
                       *(_with_auth(_with_config_dir(varargs))))

def run_svnsync(*varargs):
  """Run svnsync with VARARGS, returns exit code as int; stdout, stderr as
  list of lines (including line terminators)."""
  return run_command(svnsync_binary, 1, 0, *(_with_config_dir(varargs)))

def run_svnversion(*varargs):
  """Run svnversion with VARARGS, returns exit code as int; stdout, stderr
  as list of lines (including line terminators)."""
  return run_command(svnversion_binary, 1, 0, *varargs)

def run_entriesdump(path):
  """Run the entries-dump helper, returning a dict of Entry objects."""
  # use spawn_process rather than run_command to avoid copying all the data
  # to stdout in verbose mode.
  exit_code, stdout_lines, stderr_lines = spawn_process(entriesdump_binary,
                                                        0, 0, None, path)
  if exit_code or stderr_lines:
    ### report on this? or continue to just skip it?
    return None

  class Entry(object):
    pass
  entries = { }
  exec(''.join([line for line in stdout_lines if not line.startswith("DBG:")]))
  return entries

def run_entriesdump_subdirs(path):
  """Run the entries-dump helper, returning a list of directory names."""
  # use spawn_process rather than run_command to avoid copying all the data
  # to stdout in verbose mode.
  exit_code, stdout_lines, stderr_lines = spawn_process(entriesdump_binary,
                                                        0, 0, None, '--subdirs', path)
  return [line.strip() for line in stdout_lines if not line.startswith("DBG:")]

def run_atomic_ra_revprop_change(url, revision, propname, skel, want_error):
  """Run the atomic-ra-revprop-change helper, returning its exit code, stdout,
  and stderr.  For HTTP, default HTTP library is used."""
  # use spawn_process rather than run_command to avoid copying all the data
  # to stdout in verbose mode.
  #exit_code, stdout_lines, stderr_lines = spawn_process(entriesdump_binary,
  #                                                      0, 0, None, path)

  # This passes HTTP_LIBRARY in addition to our params.
  return run_command(atomic_ra_revprop_change_binary, True, False,
                     url, revision, propname, skel,
                     options.http_library, want_error and 1 or 0)


# Chmod recursively on a whole subtree
def chmod_tree(path, mode, mask):
  for dirpath, dirs, files in os.walk(path):
    for name in dirs + files:
      fullname = os.path.join(dirpath, name)
      if not os.path.islink(fullname):
        new_mode = (os.stat(fullname)[stat.ST_MODE] & ~mask) | mode
        os.chmod(fullname, new_mode)

# For clearing away working copies
def safe_rmtree(dirname, retry=0):
  "Remove the tree at DIRNAME, making it writable first"
  def rmtree(dirname):
    chmod_tree(dirname, 0666, 0666)
    shutil.rmtree(dirname)

  if not os.path.exists(dirname):
    return

  if retry:
    for delay in (0.5, 1, 2, 4):
      try:
        rmtree(dirname)
        break
      except:
        time.sleep(delay)
    else:
      rmtree(dirname)<commit_msg>* subversion/tests/cmdline/svntest/main.py   (youngest): New helper.<commit_after>#
#  main.py: a shared, automated test suite for Subversion
#
#  Subversion is a tool for revision control.
#  See http://subversion.tigris.org for more information.
#
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
######################################################################

import sys     # for argv[]
import os
import shutil  # for rmtree()
import re
import stat    # for ST_MODE
import subprocess
import copy    # for deepcopy()
import time    # for time()
import traceback # for print_exc()
import threading
import optparse # for argument parsing
import xml
import urllib

try:
  # Python >=3.0
  import queue
  from urllib.parse import quote as urllib_parse_quote
  from urllib.parse import unquote as urllib_parse_unquote
except ImportError:
  # Python <3.0
  import Queue as queue
  from urllib import quote as urllib_parse_quote
  from urllib import unquote as urllib_parse_unquote

import svntest
from svntest import Failure
from svntest import Skip


######################################################################
#
#  HOW TO USE THIS MODULE:
#
#  Write a new python script that
#
#     1) imports this 'svntest' package
#
#     2) contains a number of related 'test' routines.  (Each test
#        routine should take no arguments, and return None on success
#        or throw a Failure exception on failure.  Each test should
#        also contain a short docstring.)
#
#     3) places all the tests into a list that begins with None.
#
#     4) calls svntest.main.client_test() on the list.
#
#  Also, your tests will probably want to use some of the common
#  routines in the 'Utilities' section below.
#
#####################################################################
# Global stuff

default_num_threads = 5

class SVNProcessTerminatedBySignal(Failure):
  "Exception raised if a spawned process segfaulted, aborted, etc."
  pass

class SVNLineUnequal(Failure):
  "Exception raised if two lines are unequal"
  pass

class SVNUnmatchedError(Failure):
  "Exception raised if an expected error is not found"
  pass

class SVNCommitFailure(Failure):
  "Exception raised if a commit failed"
  pass

class SVNRepositoryCopyFailure(Failure):
  "Exception raised if unable to copy a repository"
  pass

class SVNRepositoryCreateFailure(Failure):
  "Exception raised if unable to create a repository"
  pass

# Windows specifics
if sys.platform == 'win32':
  windows = True
  file_scheme_prefix = 'file:///'
  _exe = '.exe'
  _bat = '.bat'
  os.environ['SVN_DBG_STACKTRACES_TO_STDERR'] = 'y'
else:
  windows = False
  file_scheme_prefix = 'file://'
  _exe = ''
  _bat = ''

# The location of our mock svneditor script.
if windows:
  svneditor_script = os.path.join(sys.path[0], 'svneditor.bat')
else:
  svneditor_script = os.path.join(sys.path[0], 'svneditor.py')

# Username and password used by the working copies
wc_author = 'jrandom'
wc_passwd = '<PASSWORD>'

# Username and password used by the working copies for "second user"
# scenarios
wc_author2 = 'jconstant' # use the same password as wc_author

# Set C locale for command line programs
os.environ['LC_ALL'] = 'C'

# This function mimics the Python 2.3 urllib function of the same name.
def pathname2url(path):
  """Convert the pathname PATH from the local syntax for a path to the form
  used in the path component of a URL. This does not produce a complete URL.
  The return value will already be quoted using the quote() function."""

  # Don't leave ':' in file://C%3A/ escaped as our canonicalization
  # rules will replace this with a ':' on input.
  return urllib_parse_quote(path.replace('\\', '/')).replace('%3A', ':')

# This function mimics the Python 2.3 urllib function of the same name.
def url2pathname(path):
  """Convert the path component PATH from an encoded URL to the local syntax
  for a path. This does not accept a complete URL. This function uses
  unquote() to decode PATH."""
  return os.path.normpath(urllib_parse_unquote(path))

######################################################################
# The locations of the svn, svnadmin and svnlook binaries, relative to
# the only scripts that import this file right now (they live in ../).
# Use --bin to override these defaults.
svn_binary = os.path.abspath('../../svn/svn' + _exe)
svnadmin_binary = os.path.abspath('../../svnadmin/svnadmin' + _exe)
svnlook_binary = os.path.abspath('../../svnlook/svnlook' + _exe)
svnrdump_binary = os.path.abspath('../../svnrdump/svnrdump' + _exe)
svnsync_binary = os.path.abspath('../../svnsync/svnsync' + _exe)
svnversion_binary = os.path.abspath('../../svnversion/svnversion' + _exe)
svndumpfilter_binary = os.path.abspath('../../svndumpfilter/svndumpfilter' + \
                                       _exe)
entriesdump_binary = os.path.abspath('entries-dump' + _exe)
atomic_ra_revprop_change_binary = os.path.abspath('atomic-ra-revprop-change' + \
                                                  _exe)

# Location to the pristine repository, will be calculated from test_area_url
# when we know what the user specified for --url.
pristine_greek_repos_url = None

# Global variable to track all of our options
options = None

# End of command-line-set global variables.
######################################################################

# All temporary repositories and working copies are created underneath
# this dir, so there's one point at which to mount, e.g., a ramdisk.
work_dir = "svn-test-work"

# Constant for the merge info property.
SVN_PROP_MERGEINFO = "svn:mergeinfo"

# Where we want all the repositories and working copies to live.
# Each test will have its own!
general_repo_dir = os.path.join(work_dir, "repositories")
general_wc_dir = os.path.join(work_dir, "working_copies")

# temp directory in which we will create our 'pristine' local
# repository and other scratch data.  This should be removed when we
# quit and when we startup.
temp_dir = os.path.join(work_dir, 'local_tmp')

# (derivatives of the tmp dir.)
pristine_greek_repos_dir = os.path.join(temp_dir, "repos")
greek_dump_dir = os.path.join(temp_dir, "greekfiles")
default_config_dir = os.path.abspath(os.path.join(temp_dir, "config"))

#
# Our pristine greek-tree state.
#
# If a test wishes to create an "expected" working-copy tree, it should
# call main.greek_state.copy().  That method will return a copy of this
# State object which can then be edited.
#
_item = svntest.wc.StateItem
greek_state = svntest.wc.State('', {
  'iota'        : _item("This is the file 'iota'.\n"),
  'A'           : _item(),
  'A/mu'        : _item("This is the file 'mu'.\n"),
  'A/B'         : _item(),
  'A/B/lambda'  : _item("This is the file 'lambda'.\n"),
  'A/B/E'       : _item(),
  'A/B/E/alpha' : _item("This is the file 'alpha'.\n"),
  'A/B/E/beta'  : _item("This is the file 'beta'.\n"),
  'A/B/F'       : _item(),
  'A/C'         : _item(),
  'A/D'         : _item(),
  'A/D/gamma'   : _item("This is the file 'gamma'.\n"),
  'A/D/G'       : _item(),
  'A/D/G/pi'    : _item("This is the file 'pi'.\n"),
  'A/D/G/rho'   : _item("This is the file 'rho'.\n"),
  'A/D/G/tau'   : _item("This is the file 'tau'.\n"),
  'A/D/H'       : _item(),
  'A/D/H/chi'   : _item("This is the file 'chi'.\n"),
  'A/D/H/psi'   : _item("This is the file 'psi'.\n"),
  'A/D/H/omega' : _item("This is the file 'omega'.\n"),
  })


######################################################################
# Utilities shared by the tests
def wrap_ex(func):
  "Wrap a function, catch, print and ignore exceptions"
  def w(*args, **kwds):
    try:
      return func(*args, **kwds)
    except Failure, ex:
      if ex.__class__ != Failure or ex.args:
        ex_args = str(ex)
        if ex_args:
          print('EXCEPTION: %s: %s' % (ex.__class__.__name__, ex_args))
        else:
          print('EXCEPTION: %s' % ex.__class__.__name__)
  return w

def setup_development_mode():
  "Wraps functions in module actions"
  l = [ 'run_and_verify_svn',
        'run_and_verify_svnversion',
        'run_and_verify_load',
        'run_and_verify_dump',
        'run_and_verify_checkout',
        'run_and_verify_export',
        'run_and_verify_update',
        'run_and_verify_merge',
        'run_and_verify_switch',
        'run_and_verify_commit',
        'run_and_verify_unquiet_status',
        'run_and_verify_status',
        'run_and_verify_diff_summarize',
        'run_and_verify_diff_summarize_xml',
        'run_and_validate_lock']

  for func in l:
    setattr(svntest.actions, func, wrap_ex(getattr(svntest.actions, func)))

def get_admin_name():
  "Return name of SVN administrative subdirectory."

  if (windows or sys.platform == 'cygwin') \
      and 'SVN_ASP_DOT_NET_HACK' in os.environ:
    return '_svn'
  else:
    return '.svn'

def wc_is_singledb(wcpath):
  """Temporary function that checks whether a working copy directory looks
  like it is part of a single-db working copy."""

  pristine = os.path.join(wcpath, get_admin_name(), 'pristine')
  if not os.path.exists(pristine):
    return True

  # Now we must be looking at a multi-db WC dir or the root dir of a
  # single-DB WC.  Sharded 'pristine' dir => single-db, else => multi-db.
  for name in os.listdir(pristine):
    if len(name) == 2:
      return True
    elif len(name) == 40:
      return False

  return False

def get_start_commit_hook_path(repo_dir):
  "Return the path of the start-commit-hook conf file in REPO_DIR."

  return os.path.join(repo_dir, "hooks", "start-commit")


def get_pre_commit_hook_path(repo_dir):
  "Return the path of the pre-commit-hook conf file in REPO_DIR."

  return os.path.join(repo_dir, "hooks", "pre-commit")


def get_post_commit_hook_path(repo_dir):
  "Return the path of the post-commit-hook conf file in REPO_DIR."

  return os.path.join(repo_dir, "hooks", "post-commit")

def get_pre_revprop_change_hook_path(repo_dir):
  "Return the path of the pre-revprop-change hook script in REPO_DIR."

  return os.path.join(repo_dir, "hooks", "pre-revprop-change")

def get_svnserve_conf_file_path(repo_dir):
  "Return the path of the svnserve.conf file in REPO_DIR."

  return os.path.join(repo_dir, "conf", "svnserve.conf")

def get_fsfs_conf_file_path(repo_dir):
  "Return the path of the fsfs.conf file in REPO_DIR."

  return os.path.join(repo_dir, "db", "fsfs.conf")

def get_fsfs_format_file_path(repo_dir):
  "Return the path of the format file in REPO_DIR."

  return os.path.join(repo_dir, "db", "format")

# Run any binary, logging the command line and return code
def run_command(command, error_expected, binary_mode=0, *varargs):
  """Run COMMAND with VARARGS. Return exit code as int; stdout, stderr
  as lists of lines (including line terminators).  See run_command_stdin()
  for details.  If ERROR_EXPECTED is None, any stderr output will be
  printed and any stderr output or a non-zero exit code will raise an
  exception."""

  return run_command_stdin(command, error_expected, 0, binary_mode,
                           None, *varargs)

# A regular expression that matches arguments that are trivially safe
# to pass on a command line without quoting on any supported operating
# system:
_safe_arg_re = re.compile(r'^[A-Za-z\d\.\_\/\-\:\@]+$')

def _quote_arg(arg):
  """Quote ARG for a command line.

  Return a quoted version of the string ARG, or just ARG if it contains
  only universally harmless characters.

  WARNING: This function cannot handle arbitrary command-line
  arguments: it is just good enough for what we need here."""

  arg = str(arg)
  if _safe_arg_re.match(arg):
    return arg

  if windows:
    # Note: subprocess.list2cmdline is Windows-specific.
    return subprocess.list2cmdline([arg])
  else:
    # Quoting suitable for most Unix shells.
    return "'" + arg.replace("'", "'\\''") + "'"

def open_pipe(command, bufsize=0, stdin=None, stdout=None, stderr=None):
  """Opens a subprocess.Popen pipe to COMMAND using STDIN,
  STDOUT, and STDERR.  BUFSIZE is passed to subprocess.Popen's
  argument of the same name.

  Returns (infile, outfile, errfile, waiter); waiter
  should be passed to wait_on_pipe."""
  command = [str(x) for x in command]

  # On Windows subprocess.Popen() won't accept a Python script as
  # a valid program to execute, rather it wants the Python executable.
  if (sys.platform == 'win32') and (command[0].endswith('.py')):
    command.insert(0, sys.executable)

  command_string = command[0] + ' ' + ' '.join(map(_quote_arg, command[1:]))

  if not stdin:
    stdin = subprocess.PIPE
  if not stdout:
    stdout = subprocess.PIPE
  if not stderr:
    stderr = subprocess.PIPE

  p = subprocess.Popen(command,
                       bufsize,
                       stdin=stdin,
                       stdout=stdout,
                       stderr=stderr,
                       close_fds=not windows)
  return p.stdin, p.stdout, p.stderr, (p, command_string)

def wait_on_pipe(waiter, binary_mode, stdin=None):
  """WAITER is (KID, COMMAND_STRING).  Wait for KID (opened with open_pipe)
  to finish, dying if it does.  If KID fails, create an error message
  containing any stdout and stderr from the kid.  Show COMMAND_STRING in
  diagnostic messages.  Normalize Windows line endings of stdout and stderr
  if not BINARY_MODE.  Return KID's exit code as int; stdout, stderr as
  lists of lines (including line terminators)."""
  if waiter is None:
    return

  kid, command_string = waiter
  stdout, stderr = kid.communicate(stdin)
  exit_code = kid.returncode

  # Normalize Windows line endings if in text mode.
  if windows and not binary_mode:
    stdout = stdout.replace('\r\n', '\n')
    stderr = stderr.replace('\r\n', '\n')

  # Convert output strings to lists.
  stdout_lines = stdout.splitlines(True)
  stderr_lines = stderr.splitlines(True)

  if exit_code < 0:
    if not windows:
      exit_signal = os.WTERMSIG(-exit_code)
    else:
      exit_signal = exit_code

    if stdout_lines is not None:
      sys.stdout.write("".join(stdout_lines))
      sys.stdout.flush()
    if stderr_lines is not None:
      sys.stderr.write("".join(stderr_lines))
      sys.stderr.flush()
    if options.verbose:
      # show the whole path to make it easier to start a debugger
      sys.stderr.write("CMD: %s terminated by signal %d\n"
                       % (command_string, exit_signal))
      sys.stderr.flush()
    raise SVNProcessTerminatedBySignal
  else:
    if exit_code and options.verbose:
      sys.stderr.write("CMD: %s exited with %d\n"
                       % (command_string, exit_code))
      sys.stderr.flush()
    return stdout_lines, stderr_lines, exit_code

def spawn_process(command, bufsize=0, binary_mode=0, stdin_lines=None,
                  *varargs):
  """Run any binary, supplying input text, logging the command line.
  BUFSIZE dictates the pipe buffer size used in communication with the
  subprocess: 0 means unbuffered, 1 means line buffered, any other
  positive value means use a buffer of (approximately) that size.
  A negative bufsize means to use the system default, which usually
  means fully buffered. The default value for bufsize is 0 (unbuffered).
  Normalize Windows line endings of stdout and stderr if not BINARY_MODE.
  Return exit code as int; stdout, stderr as lists of lines (including
  line terminators)."""
  if stdin_lines and not isinstance(stdin_lines, list):
    raise TypeError("stdin_lines should have list type")

  # Log the command line
  if options.verbose and not command.endswith('.py'):
    sys.stdout.write('CMD: %s %s\n' % (os.path.basename(command),
                                      ' '.join([_quote_arg(x) for x in varargs])))
    sys.stdout.flush()

  infile, outfile, errfile, kid = open_pipe([command] + list(varargs), bufsize)

  if stdin_lines:
    for x in stdin_lines:
      infile.write(x)

  stdout_lines, stderr_lines, exit_code = wait_on_pipe(kid, binary_mode)
  infile.close()

  outfile.close()
  errfile.close()

  return exit_code, stdout_lines, stderr_lines

def run_command_stdin(command, error_expected, bufsize=0, binary_mode=0,
                      stdin_lines=None, *varargs):
  """Run COMMAND with VARARGS; input STDIN_LINES (a list of strings
  which should include newline characters) to program via stdin - this
  should not be very large, as if the program outputs more than the OS
  is willing to buffer, this will deadlock, with both Python and
  COMMAND waiting to write to each other for ever.  For tests where this
  is a problem, setting BUFSIZE to a sufficiently large value will prevent
  the deadlock, see spawn_process().
  Normalize Windows line endings of stdout and stderr if not BINARY_MODE.
  Return exit code as int; stdout, stderr as lists of lines (including
  line terminators).
  If ERROR_EXPECTED is None, any stderr output will be printed and any
  stderr output or a non-zero exit code will raise an exception."""

  if options.verbose:
    start = time.time()

  exit_code, stdout_lines, stderr_lines = spawn_process(command,
                                                        bufsize,
                                                        binary_mode,
                                                        stdin_lines,
                                                        *varargs)

  if options.verbose:
    stop = time.time()
    print('<TIME = %.6f>' % (stop - start))
    for x in stdout_lines:
      sys.stdout.write(x)
    for x in stderr_lines:
      sys.stdout.write(x)

  if (not error_expected) and ((stderr_lines) or (exit_code != 0)):
    if not options.verbose:
      for x in stderr_lines:
        sys.stdout.write(x)
    raise Failure

  return exit_code, \
         [line for line in stdout_lines if not line.startswith("DBG:")], \
         stderr_lines

def create_config_dir(cfgdir, config_contents=None, server_contents=None):
  "Create config directories and files"

  # config file names
  cfgfile_cfg = os.path.join(cfgdir, 'config')
  cfgfile_srv = os.path.join(cfgdir, 'servers')

  # create the directory
  if not os.path.isdir(cfgdir):
    os.makedirs(cfgdir)

  # define default config file contents if none provided
  if config_contents is None:
    config_contents = """
#
[auth]
password-stores =

[miscellany]
interactive-conflicts = false
"""

  # define default server file contents if none provided
  if server_contents is None:
    http_library_str = ""
    if options.http_library:
      http_library_str = "http-library=%s" % (options.http_library)
    server_contents = """
#
[global]
%s
store-plaintext-passwords=yes
store-passwords=yes
""" % (http_library_str)

  file_write(cfgfile_cfg, config_contents)
  file_write(cfgfile_srv, server_contents)

def _with_config_dir(args):
  if '--config-dir' in args:
    return args
  else:
    return args + ('--config-dir', default_config_dir)

def _with_auth(args):
  assert '--password' not in args
  args = args + ('--password', wc_passwd,
                 '--no-auth-cache' )
  if '--username' in args:
    return args
  else:
    return args + ('--username', wc_author )

# For running subversion and returning the output
def run_svn(error_expected, *varargs):
  """Run svn with VARARGS; return exit code as int; stdout, stderr as
  lists of lines (including line terminators).  If ERROR_EXPECTED is
  None, any stderr output will be printed and any stderr output or a
  non-zero exit code will raise an exception.  If
  you're just checking that something does/doesn't come out of
  stdout/stderr, you might want to use actions.run_and_verify_svn()."""
  return run_command(svn_binary, error_expected, 0,
                     *(_with_auth(_with_config_dir(varargs))))

# For running svnadmin.  Ignores the output.
def run_svnadmin(*varargs):
  """Run svnadmin with VARARGS, returns exit code as int; stdout, stderr as
  list of lines (including line terminators)."""
  return run_command(svnadmin_binary, 1, 0, *varargs)

# For running svnlook.  Ignores the output.
def run_svnlook(*varargs):
  """Run svnlook with VARARGS, returns exit code as int; stdout, stderr as
  list of lines (including line terminators)."""
  return run_command(svnlook_binary, 1, 0, *varargs)

def run_svnrdump(stdin_input, *varargs):
  """Run svnrdump with VARARGS, returns exit code as int; stdout, stderr as
  list of lines (including line terminators).  Use binary mode for output."""
  if stdin_input:
    return run_command_stdin(svnrdump_binary, 0, 1, 1, stdin_input,
                             *(_with_auth(_with_config_dir(varargs))))
  else:
    return run_command(svnrdump_binary, 1, 1,
                       *(_with_auth(_with_config_dir(varargs))))

def run_svnsync(*varargs):
  """Run svnsync with VARARGS, returns exit code as int; stdout, stderr as
  list of lines (including line terminators)."""
  return run_command(svnsync_binary, 1, 0, *(_with_config_dir(varargs)))

def run_svnversion(*varargs):
  """Run svnversion with VARARGS, returns exit code as int; stdout, stderr
  as list of lines (including line terminators)."""
  return run_command(svnversion_binary, 1, 0, *varargs)

def run_entriesdump(path):
  """Run the entries-dump helper, returning a dict of Entry objects."""
  # use spawn_process rather than run_command to avoid copying all the data
  # to stdout in verbose mode.
  exit_code, stdout_lines, stderr_lines = spawn_process(entriesdump_binary,
                                                        0, 0, None, path)
  if exit_code or stderr_lines:
    ### report on this? or continue to just skip it?
    return None

  class Entry(object):
    pass
  entries = { }
  exec(''.join([line for line in stdout_lines if not line.startswith("DBG:")]))
  return entries

def run_entriesdump_subdirs(path):
  """Run the entries-dump helper, returning a list of directory names."""
  # use spawn_process rather than run_command to avoid copying all the data
  # to stdout in verbose mode.
  exit_code, stdout_lines, stderr_lines = spawn_process(entriesdump_binary,
                                                        0, 0, None, '--subdirs', path)
  return [line.strip() for line in stdout_lines if not line.startswith("DBG:")]

def run_atomic_ra_revprop_change(url, revision, propname, skel, want_error):
  """Run the atomic-ra-revprop-change helper, returning its exit code, stdout,
  and stderr.  For HTTP, default HTTP library is used."""
  # use spawn_process rather than run_command to avoid copying all the data
  # to stdout in verbose mode.
  #exit_code, stdout_lines, stderr_lines = spawn_process(entriesdump_binary,
  #                                                      0, 0, None, path)

  # This passes HTTP_LIBRARY in addition to our params.
  return run_command(atomic_ra_revprop_change_binary, True, False,
                     url, revision, propname, skel,
                     options.http_library, want_error and 1 or 0)


def youngest(repos_path):
  "run 'svnlook youngest' on REPOS_PATH, returns revision as int"
  exit_code, stdout_lines, stderr_lines = run_command(svnlook_binary, None, 0, 
                                                      'youngest', repos_path)
  if exit_code or stderr_lines:
    raise Failure("Unexpected failure of 'svnlook youngest':\n%s" % stderr_lines)
  if len(stdout_lines) != 1:
    raise Failure("Wrong output from 'svnlook youngest':\n%s" % stdout_lines)
  return int(stdout_lines[0].rstrip())

# Chmod recursively on a whole subtree
def chmod_tree(path, mode, mask):
  for dirpath, dirs, files in os.walk(path):
    for name in dirs + files:
      fullname = os.path.join(dirpath, name)
      if not os.path.islink(fullname):
        new_mode = (os.stat(fullname)[stat.ST_MODE] & ~mask) | mode
        os.chmod(fullname, new_mode)

# For clearing away working copies
def safe_rmtree(dirname, retry=0):
  "Remove the tree at DIRNAME, making it writable first"
  def rmtree(dirname):
    chmod_tree(dirname, 0666, 0666)
    shutil.rmtree(dirname)

  if not os.path.exists(dirname):
    return

  if retry:
    for delay in (0.5, 1, 2, 4):
      try:
        rmtree(dirname)
        break
      except:
        time.sleep(delay)
    else:
      rmtree(dirname)
{'fraction_non_alphanumeric': 0.11725618897127345, 'fraction_numerical': 0.0035452372270167337, 'mean_word_length': 3.2961705831157526, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 38, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7378800', 'n_tokens_mistral': 15760, 'n_tokens_neox': 15221, 'n_words': 5113}