You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by st...@apache.org on 2013/02/04 21:48:13 UTC

svn commit: r1442344 [34/39] - in /subversion/branches/fsfs-format7: ./ build/ build/ac-macros/ build/generator/ build/generator/templates/ build/win32/ contrib/client-side/emacs/ contrib/server-side/fsfsfixer/fixer/ contrib/server-side/svncutter/ doc/...

Modified: subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/main.py
URL: http://svn.apache.org/viewvc/subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/main.py?rev=1442344&r1=1442343&r2=1442344&view=diff
==============================================================================
--- subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/main.py (original)
+++ subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/main.py Mon Feb  4 20:48:05 2013
@@ -157,6 +157,15 @@ atomic_ra_revprop_change_binary = os.pat
 wc_lock_tester_binary = os.path.abspath('../libsvn_wc/wc-lock-tester' + _exe)
 wc_incomplete_tester_binary = os.path.abspath('../libsvn_wc/wc-incomplete-tester' + _exe)
 
+######################################################################
+# The location of svnauthz binary, relative to the only scripts that
+# import this file right now (they live in ../).
+# Use --tools to overide these defaults.
+svnauthz_binary = os.path.abspath('../../../tools/server-side/svnauthz' + _exe)
+svnauthz_validate_binary = os.path.abspath(
+    '../../../tools/server-side/svnauthz-validate' + _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
@@ -343,7 +352,7 @@ def filter_dbg(lines):
   return included
 
 # Run any binary, logging the command line and return code
-def run_command(command, error_expected, binary_mode=0, *varargs):
+def run_command(command, error_expected, binary_mode=False, *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
@@ -451,17 +460,20 @@ def wait_on_pipe(waiter, binary_mode, st
       logger.info("CMD: %s exited with %d" % (command_string, exit_code))
     return stdout_lines, stderr_lines, exit_code
 
-def spawn_process(command, bufsize=-1, binary_mode=0, stdin_lines=None,
+def spawn_process(command, bufsize=-1, binary_mode=False, 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).
+  subprocess: quoting from subprocess.Popen(), "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."
+
   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)."""
+  line terminators).
+  """
   if stdin_lines and not isinstance(stdin_lines, list):
     raise TypeError("stdin_lines should have list type")
 
@@ -484,7 +496,7 @@ def spawn_process(command, bufsize=-1, b
 
   return exit_code, stdout_lines, stderr_lines
 
-def run_command_stdin(command, error_expected, bufsize=-1, binary_mode=0,
+def run_command_stdin(command, error_expected, bufsize=-1, binary_mode=False,
                       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
@@ -651,13 +663,6 @@ def _with_auth(args):
   else:
     return args + ('--username', wc_author )
 
-def _with_log_message(args):
-
-  if '-m' in args or '--message' in args or '-F' in args:
-    return args
-  else:
-    return args + ('--message', 'default log message')
-
 # 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
@@ -666,53 +671,63 @@ def run_svn(error_expected, *varargs):
   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,
+  return run_command(svn_binary, error_expected, False,
                      *(_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)
+  return run_command(svnadmin_binary, 1, False, *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)
+  return run_command(svnlook_binary, 1, False, *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, 1, 1, 1, stdin_input,
+    return run_command_stdin(svnrdump_binary, 1, 1, True, stdin_input,
                              *(_with_auth(_with_config_dir(varargs))))
   else:
-    return run_command(svnrdump_binary, 1, 1,
+    return run_command(svnrdump_binary, 1, True,
                        *(_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)))
+  return run_command(svnsync_binary, 1, False, *(_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)
+  return run_command(svnversion_binary, 1, False, *varargs)
 
 def run_svnmucc(*varargs):
   """Run svnmucc with VARARGS, returns exit code as int; stdout, stderr as
   list of lines (including line terminators).  Use binary mode for output."""
-  return run_command(svnmucc_binary, 1, 1,
-                     *(_with_auth(_with_config_dir(_with_log_message(varargs)))))
+  return run_command(svnmucc_binary, 1, True,
+                     *(_with_auth(_with_config_dir(varargs))))
+
+def run_svnauthz(*varargs):
+  """Run svnauthz with VARARGS, returns exit code as int; stdout, stderr
+  as list of lines (including line terminators)."""
+  return run_command(svnauthz_binary, 1, False, *varargs)
+
+def run_svnauthz_validate(*varargs):
+  """Run svnauthz-validate with VARARGS, returns exit code as int; stdout,
+  stderr as list of lines (including line terminators)."""
+  return run_command(svnauthz_validate_binary, 1, False, *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)
+                                                        0, False, None, path)
   if exit_code or stderr_lines:
     ### report on this? or continue to just skip it?
     return None
@@ -728,16 +743,33 @@ def run_entriesdump_subdirs(path):
   # 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)
+                                                        0, False, None, '--subdirs', path)
   return map(lambda line: line.strip(), filter_dbg(stdout_lines))
 
+def run_entriesdump_tree(path):
+  """Run the entries-dump helper, returning a dict of 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, False, None,
+                                                        '--tree-dump', path)
+  if exit_code or stderr_lines:
+    ### report on this? or continue to just skip it?
+    return None
+
+  class Entry(object):
+    pass
+  dirs = { }
+  exec(''.join(filter_dbg(stdout_lines)))
+  return dirs
+
 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)
+  #                                                      0, False, None, path)
 
   # This passes HTTP_LIBRARY in addition to our params.
   return run_command(atomic_ra_revprop_change_binary, True, False,
@@ -759,7 +791,7 @@ def run_wc_incomplete_tester(wc_dir, rev
 
 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,
+  exit_code, stdout_lines, stderr_lines = run_command(svnlook_binary, None, False,
                                                       'youngest', repos_path)
   if exit_code or stderr_lines:
     raise Failure("Unexpected failure of 'svnlook youngest':\n%s" % stderr_lines)
@@ -839,7 +871,7 @@ def create_repos(path, minor_version = N
   opts += ("--compatible-version=1.%d" % (minor_version),)
   if options.fs_type is not None:
     opts += ("--fs-type=" + options.fs_type,)
-  exit_code, stdout, stderr = run_command(svnadmin_binary, 1, 0, "create",
+  exit_code, stdout, stderr = run_command(svnadmin_binary, 1, False, "create",
                                           path, *opts)
 
   # Skip tests if we can't create the repository.
@@ -1033,6 +1065,19 @@ def write_restrictive_svnserve_conf(repo
     fp.write("password-db = passwd\n")
   fp.close()
 
+def write_restrictive_svnserve_conf_with_groups(repo_dir,
+                                                anon_access="none"):
+  "Create a restrictive configuration with groups stored in a separate file."
+
+  fp = open(get_svnserve_conf_file_path(repo_dir), 'w')
+  fp.write("[general]\nanon-access = %s\nauth-access = write\n"
+           "authz-db = authz\ngroups-db = groups\n" % anon_access)
+  if options.enable_sasl:
+    fp.write("realm = svntest\n[sasl]\nuse-sasl = true\n");
+  else:
+    fp.write("password-db = passwd\n")
+  fp.close()
+
 # Warning: because mod_dav_svn uses one shared authz file for all
 # repositories, you *cannot* use write_authz_file in any test that
 # might be run in parallel.
@@ -1068,6 +1113,18 @@ an appropriate list of mappings.
     fp.write("[%s%s]\n%s\n" % (prefix, p, r))
   fp.close()
 
+# See the warning about parallel test execution in write_authz_file
+# method description.
+def write_groups_file(sbox, groups):
+  """Write a groups file to SBOX, appropriate for the RA method used,
+with group contents set to GROUPS."""
+  fp = open(sbox.groups_file, 'w')
+  fp.write("[groups]\n")
+  if groups:
+    for p, r in groups.items():
+      fp.write("%s = %s\n" % (p, r))
+  fp.close()
+
 def use_editor(func):
   os.environ['SVN_EDITOR'] = svneditor_script
   os.environ['SVN_MERGE'] = svneditor_script
@@ -1234,6 +1291,17 @@ def server_enforces_date_syntax():
 def server_has_atomic_revprop():
   return options.server_minor_version >= 7
 
+def is_plaintext_password_storage_disabled():
+  try:
+    predicate = re.compile("^WARNING: Plaintext password storage is enabled!")
+    code, out, err = run_svn(False, "--version")
+    for line in out:
+      if predicate.match(line):
+        return False
+  except:
+    return False
+  return True
+
 ######################################################################
 
 
@@ -1292,17 +1360,10 @@ class TestSpawningThread(threading.Threa
     if options.http_proxy:
       args.append('--http-proxy=' + options.http_proxy)
 
-    result, stdout_lines, stderr_lines = spawn_process(command, 0, 0, None,
+    result, stdout_lines, stderr_lines = spawn_process(command, 0, False, None,
                                                        *args)
     self.results.append((index, result, stdout_lines, stderr_lines))
 
-    if result != 1:
-      sys.stdout.write('.')
-    else:
-      sys.stdout.write('F')
-
-    sys.stdout.flush()
-
 class TestRunner:
   """Encapsulate a single test case (predicate), including logic for
   runing the test and test list output."""
@@ -1542,9 +1603,6 @@ def _internal_run_tests(test_list, testn
       results += t.results
     results.sort()
 
-    # terminate the line of dots
-    print("")
-
     # all tests are finished, find out the result and print the logs.
     for (index, result, stdout_lines, stderr_lines) in results:
       if stdout_lines:
@@ -1646,6 +1704,8 @@ def _create_parser():
                     help='Path to SSL server certificate.')
   parser.add_option('--http-proxy', action='store',
                     help='Use the HTTP Proxy at hostname:port.')
+  parser.add_option('--tools-bin', action='store', dest='tools_bin',
+                    help='Use the svn tools installed in this path')
 
   # most of the defaults are None, but some are other values, set them here
   parser.set_defaults(
@@ -1786,6 +1846,8 @@ def execute_tests(test_list, serial_only
   global svndumpfilter_binary
   global svnversion_binary
   global svnmucc_binary
+  global svnauthz_binary
+  global svnauthz_validate_binary
   global options
 
   if test_name:
@@ -1903,6 +1965,11 @@ def execute_tests(test_list, serial_only
       svnversion_binary = os.path.join(options.svn_bin, 'svnversion' + _exe)
       svnmucc_binary = os.path.join(options.svn_bin, 'svnmucc' + _exe)
 
+  if options.tools_bin:
+    svnauthz_binary = os.path.join(options.tools_bin, 'svnauthz' + _exe)
+    svnauthz_validate_binary = os.path.join(options.tools_bin,
+                                            'svnauthz-validate' + _exe)
+
   ######################################################################
 
   # Cleanup: if a previous run crashed or interrupted the python

Modified: subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/sandbox.py
URL: http://svn.apache.org/viewvc/subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/sandbox.py?rev=1442344&r1=1442343&r2=1442344&view=diff
==============================================================================
--- subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/sandbox.py (original)
+++ subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/sandbox.py Mon Feb  4 20:48:05 2013
@@ -78,12 +78,14 @@ class Sandbox:
       tmp_authz_file = os.path.join(svntest.main.work_dir, "authz-" + self.name)
       open(tmp_authz_file, 'w').write("[/]\n* = rw\n")
       shutil.move(tmp_authz_file, self.authz_file)
+      self.groups_file = os.path.join(svntest.main.work_dir, "groups")
 
     # For svnserve tests we have a per-repository authz file, and it
     # doesn't need to be there in order for things to work, so we don't
     # have any default contents.
     elif self.repo_url.startswith("svn"):
       self.authz_file = os.path.join(self.repo_dir, "conf", "authz")
+      self.groups_file = os.path.join(self.repo_dir, "conf", "groups")
 
   def clone_dependent(self, copy_wc=False):
     """A convenience method for creating a near-duplicate of this
@@ -231,8 +233,8 @@ class Sandbox:
       target = self.ospath(target)
     if message is None:
       message = svntest.main.make_log_msg()
-    svntest.main.run_svn(False, 'commit', '-m', message,
-                         target)
+    svntest.actions.run_and_verify_commit(self.wc_dir, None, None, [],
+                                          '-m', message, target)
 
   def simple_rm(self, *targets):
     """Schedule TARGETS for deletion.
@@ -318,6 +320,13 @@ class Sandbox:
       # '*' is evaluated on Windows
       self.simple_propset('svn:special', 'X', target)
 
+  def simple_add_text(self, text, *targets):
+    """Create files containing TEXT as TARGETS"""
+    assert len(targets) > 0
+    for target in targets:
+       svntest.main.file_write(self.ospath(target), text, mode='wb')
+    self.simple_add(*targets)
+
   def simple_copy(self, source, dest):
     """Copy SOURCE to DEST in the WC.
        SOURCE and DEST are relpaths relative to the WC."""

Modified: subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/tree.py
URL: http://svn.apache.org/viewvc/subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/tree.py?rev=1442344&r1=1442343&r2=1442344&view=diff
==============================================================================
--- subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/tree.py (original)
+++ subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/tree.py Mon Feb  4 20:48:05 2013
@@ -253,6 +253,10 @@ class SVNTreeNode:
     # remove the subtree path, skip this node if necessary.
     if path.startswith(subtree):
       path = path[len(subtree):]
+    elif path + os.sep == subtree:
+      # Many callers set subtree to 'some-path' + os.sep. Don't skip the
+      # root node in that case.
+      path = ''
     else:
       return 0
 
@@ -834,10 +838,11 @@ def build_tree_from_commit(lines):
 #             IFF columns non-empty.
 #
 
-def build_tree_from_status(lines):
+def build_tree_from_status(lines, wc_dir_name=None):
   "Return a tree derived by parsing the output LINES from 'st -vuq'."
 
-  return svntest.wc.State.from_status(lines).old_tree()
+  return svntest.wc.State.from_status(lines,
+                                       wc_dir_name=wc_dir_name).old_tree()
 
 
 # Parse merge "skipped" output

Modified: subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/verify.py
URL: http://svn.apache.org/viewvc/subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/verify.py?rev=1442344&r1=1442343&r2=1442344&view=diff
==============================================================================
--- subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/verify.py (original)
+++ subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/verify.py Mon Feb  4 20:48:05 2013
@@ -215,6 +215,7 @@ class ExpectedOutput:
 
 
 class AnyOutput(ExpectedOutput):
+  """Matches any non-empty output."""
   def __init__(self):
     ExpectedOutput.__init__(self, None, False)
 

Modified: subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/wc.py
URL: http://svn.apache.org/viewvc/subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/wc.py?rev=1442344&r1=1442343&r2=1442344&view=diff
==============================================================================
--- subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/wc.py (original)
+++ subversion/branches/fsfs-format7/subversion/tests/cmdline/svntest/wc.py Mon Feb  4 20:48:05 2013
@@ -94,7 +94,13 @@ _re_parse_status = re.compile('^([?!MACD
                               '((?P<wc_rev>\d+|-|\?) +(\d|-|\?)+ +(\S+) +)?'
                               '(?P<path>.+)$')
 
-_re_parse_skipped = re.compile("^Skipped[^']* '(.+)'( --.*)?\n")
+_re_parse_status_ex = re.compile('^      ('
+               '(  \> moved (from (?P<moved_from>.+)|to (?P<moved_to>.*)))'
+              '|(  \> swapped places with (?P<swapped_with>.+).*)'
+              '|(\>   (?P<tc>.+))'
+  ')$')
+
+_re_parse_skipped = re.compile("^(Skipped[^']*) '(.+)'( --.*)?\n")
 
 _re_parse_summarize = re.compile("^([MAD ][M ])      (.+)\n")
 
@@ -139,10 +145,11 @@ class State:
     "Import state items from a State object, reparent the items to PARENT."
     assert isinstance(state, State)
 
-    if parent and parent[-1] != '/':
-      parent += '/'
     for path, item in state.desc.items():
-      path = parent + path
+      if path == '':
+        path = parent
+      else:
+        path = parent + '/' + path
       self.desc[path] = item
 
   def remove(self, *paths):
@@ -254,8 +261,11 @@ class State:
 
     base = to_relpath(os.path.normpath(self.wc_dir))
 
+    # TODO: We should probably normalize the paths in moved_from and moved_to.
+
     desc = dict([(repos_join(base, path), item)
                  for path, item in self.desc.items()])
+
     return State('', desc)
 
   def compare(self, other):
@@ -327,8 +337,16 @@ class State:
       if item.status:
         # If this is an unversioned tree-conflict, remove it.
         # These are only in their parents' THIS_DIR, they don't have entries.
-        if item.status[0] in '!?' and item.treeconflict == 'C':
+        if item.status[0] in '!?' and item.treeconflict == 'C' and \
+                                      item.entry_status is None:
+          del self.desc[path]
+        # Normal externals are not stored in the parent wc, drop the root
+        # and everything in these working copies
+        elif item.status == 'X ' or item.prev_status == 'X ':
           del self.desc[path]
+          for p, i in self.desc.copy().items():
+            if p.startswith(path + '/'):
+              del self.desc[p]
         else:
           # when reading the entry structures, we don't examine for text or
           # property mods, so clear those flags. we also do not examine the
@@ -347,6 +365,9 @@ class State:
           if item.entry_status is not None:
             item.status = item.entry_status
             item.entry_status = None
+          if item.entry_copied is not None:
+            item.copied = item.entry_copied
+            item.entry_copied = None
       if item.writelocked:
         # we don't contact the repository, so our only information is what
         # is in the working copy. 'K' means we have one and it matches the
@@ -362,6 +383,11 @@ class State:
           item.writelocked = 'K'
         elif item.writelocked == 'O':
           item.writelocked = None
+      item.moved_from = None
+      item.moved_to = None
+      if path == '':
+        item.switched = None
+      item.treeconflict = None
 
   def old_tree(self):
     "Return an old-style tree (for compatibility purposes)."
@@ -397,7 +423,7 @@ class State:
     return not self.__eq__(other)
 
   @classmethod
-  def from_status(cls, lines):
+  def from_status(cls, lines, wc_dir_name=None):
     """Create a State object from 'svn status' output."""
 
     def not_space(value):
@@ -406,21 +432,50 @@ class State:
       return None
 
     desc = { }
+    last = None
     for line in lines:
       if line.startswith('DBG:'):
         continue
 
-      # Quit when we hit an externals status announcement.
-      ### someday we can fix the externals tests to expect the additional
-      ### flood of externals status data.
-      if line.startswith('Performing'):
-        break
-
       match = _re_parse_status.search(line)
       if not match or match.group(10) == '-':
+
+        ex_match = _re_parse_status_ex.search(line)
+
+        if ex_match:
+          if ex_match.group('moved_from'):
+            path = ex_match.group('moved_from')
+            if wc_dir_name and path.startswith(wc_dir_name + os.path.sep):
+              path = path[len(wc_dir_name) + 1:]
+            
+            last.tweak(moved_from = to_relpath(path))
+          elif ex_match.group('moved_to'):
+            path = ex_match.group('moved_to')
+            if wc_dir_name and path.startswith(wc_dir_name + os.path.sep):
+              path = path[len(wc_dir_name) + 1:]
+            
+            last.tweak(moved_to = to_relpath(path))
+          elif ex_match.group('swapped_with'):
+            path = ex_match.group('swapped_with')
+            if wc_dir_name and path.startswith(wc_dir_name + os.path.sep):
+              path = path[len(wc_dir_name) + 1:]
+            
+            last.tweak(moved_to = to_relpath(path))
+            last.tweak(moved_from = to_relpath(path))
+
+          # Parse TC description?
+
         # ignore non-matching lines, or items that only exist on repos
         continue
 
+      prev_status = None
+      prev_treeconflict = None
+
+      path = to_relpath(match.group('path'))
+      if path in desc:
+        prev_status = desc[path].status
+        prev_treeconflict = desc[path].treeconflict
+
       item = StateItem(status=match.group(1),
                        locked=not_space(match.group(2)),
                        copied=not_space(match.group(3)),
@@ -428,8 +483,11 @@ class State:
                        writelocked=not_space(match.group(5)),
                        treeconflict=not_space(match.group(6)),
                        wc_rev=not_space(match.group('wc_rev')),
+                       prev_status=prev_status,
+                       prev_treeconflict =prev_treeconflict
                        )
-      desc[to_relpath(match.group('path'))] = item
+      desc[path] = item
+      last = item
 
     return cls('', desc)
 
@@ -444,7 +502,8 @@ class State:
 
       match = _re_parse_skipped.search(line)
       if match:
-        desc[to_relpath(match.group(1))] = StateItem()
+        desc[to_relpath(match.group(2))] =  StateItem(
+          verb=(match.group(1).strip(':')))
 
     return cls('', desc)
 
@@ -483,12 +542,38 @@ class State:
           treeconflict = match.group(3)
         else:
           treeconflict = None
-        desc[to_relpath(match.group(4))] = StateItem(status=match.group(1),
-                                                     treeconflict=treeconflict)
+        path = to_relpath(match.group(4))
+        prev_status = None
+        prev_verb = None
+        prev_treeconflict = None
+
+        if path in desc:
+          prev_status = desc[path].status
+          prev_verb = desc[path].verb
+          prev_treeconflict = desc[path].treeconflict
+
+        desc[path] = StateItem(status=match.group(1),
+                               treeconflict=treeconflict,
+                               prev_status=prev_status,
+                               prev_verb=prev_verb,
+                               prev_treeconflict=prev_treeconflict)
       else:
         match = re_extra.search(line)
         if match:
-          desc[to_relpath(match.group(2))] = StateItem(verb=match.group(1))
+          path = to_relpath(match.group(2))
+          prev_status = None
+          prev_verb = None
+          prev_treeconflict = None
+
+          if path in desc:
+            prev_status = desc[path].status
+            prev_verb = desc[path].verb
+            prev_treeconflict = desc[path].treeconflict
+
+          desc[path] = StateItem(verb=match.group(1),
+                                 prev_status=prev_status,
+                                 prev_verb=prev_verb,
+                                 prev_treeconflict=prev_treeconflict)
 
     return cls('', desc)
 
@@ -579,21 +664,14 @@ class State:
     desc = { }
     dot_svn = svntest.main.get_admin_name()
 
-    for dirpath in svntest.main.run_entriesdump_subdirs(base):
+    dump_data = svntest.main.run_entriesdump_tree(base)
 
-      if base == '.' and dirpath != '.':
-        dirpath = '.' + os.path.sep + dirpath
+    if not dump_data:
+      # Probably 'svn status' run on an actual only node
+      # ### Improve!
+      return cls('', desc)
 
-      entries = svntest.main.run_entriesdump(dirpath)
-      if entries is None:
-        continue
-
-      if dirpath == '.':
-        parent = ''
-      elif dirpath.startswith('.' + os.sep):
-        parent = to_relpath(dirpath[2:])
-      else:
-        parent = to_relpath(dirpath)
+    for parent, entries in sorted(dump_data.items()):
 
       parent_url = entries[''].url
 
@@ -606,6 +684,9 @@ class State:
         # entries that are ABSENT don't show up in status
         if entry.absent:
           continue
+        # entries that are User Excluded don't show up in status
+        if entry.depth == -1:
+          continue
         if name and entry.kind == 2:
           # stub subdirectory. leave a "missing" StateItem in here. note
           # that we can't put the status as "! " because that gets tweaked
@@ -631,6 +712,9 @@ class State:
         if implied_url and implied_url != entry.url:
           item.switched = 'S'
 
+        if entry.file_external:
+          item.switched = 'X'
+
     return cls('', desc)
 
 
@@ -644,9 +728,10 @@ class StateItem:
 
   def __init__(self, contents=None, props=None,
                status=None, verb=None, wc_rev=None,
-               entry_rev=None, entry_status=None,
+               entry_rev=None, entry_status=None, entry_copied=None,
                locked=None, copied=None, switched=None, writelocked=None,
-               treeconflict=None):
+               treeconflict=None, moved_from=None, moved_to=None,
+               prev_status=None, prev_verb=None, prev_treeconflict=None):
     # provide an empty prop dict if it wasn't provided
     if props is None:
       props = { }
@@ -663,22 +748,29 @@ class StateItem:
     self.props = props
     # A two-character string from the first two columns of 'svn status'.
     self.status = status
+    self.prev_status = prev_status
     # The action word such as 'Adding' printed by commands like 'svn update'.
     self.verb = verb
+    self.prev_verb = prev_verb
     # The base revision number of the node in the WC, as a string.
     self.wc_rev = wc_rev
     # These will be set when we expect the wc_rev/status to differ from those
     # found in the entries code.
     self.entry_rev = entry_rev
     self.entry_status = entry_status
+    self.entry_copied = entry_copied
     # For the following attributes, the value is the status character of that
     # field from 'svn status', except using value None instead of status ' '.
     self.locked = locked
     self.copied = copied
     self.switched = switched
     self.writelocked = writelocked
-    # Value 'C' or ' ', or None as an expected status meaning 'do not check'.
+    # Value 'C', 'A', 'D' or ' ', or None as an expected status meaning 'do not check'.
     self.treeconflict = treeconflict
+    self.prev_treeconflict = prev_treeconflict
+    # Relative paths to the move locations
+    self.moved_from = moved_from
+    self.moved_to = moved_to
 
   def copy(self):
     "Make a deep copy of self."
@@ -716,8 +808,12 @@ class StateItem:
     atts = { }
     if self.status is not None:
       atts['status'] = self.status
+    if self.prev_status is not None:
+      atts['prev_status'] = self.prev_status
     if self.verb is not None:
       atts['verb'] = self.verb
+    if self.prev_verb is not None:
+      atts['prev_verb'] = self.prev_verb
     if self.wc_rev is not None:
       atts['wc_rev'] = self.wc_rev
     if self.locked is not None:
@@ -730,6 +826,12 @@ class StateItem:
       atts['writelocked'] = self.writelocked
     if self.treeconflict is not None:
       atts['treeconflict'] = self.treeconflict
+    if self.prev_treeconflict is not None:
+      atts['prev_treeconflict'] = self.prev_treeconflict
+    if self.moved_from is not None:
+      atts['moved_from'] = self.moved_from
+    if self.moved_to is not None:
+      atts['moved_to'] = self.moved_to
 
     return (os.path.normpath(path), self.contents, self.props, atts)
 
@@ -827,9 +929,12 @@ def repos_join(base, path):
   """Join two repos paths. This generally works for URLs too."""
   if base == '':
     return path
-  if path == '':
+  elif path == '':
     return base
-  return base + '/' + path
+  elif base[len(base)-1:] == '/':
+    return base + path
+  else:
+    return base + '/' + path
 
 
 def svn_uri_quote(url):

Modified: subversion/branches/fsfs-format7/subversion/tests/cmdline/switch_tests.py
URL: http://svn.apache.org/viewvc/subversion/branches/fsfs-format7/subversion/tests/cmdline/switch_tests.py?rev=1442344&r1=1442343&r2=1442344&view=diff
==============================================================================
--- subversion/branches/fsfs-format7/subversion/tests/cmdline/switch_tests.py (original)
+++ subversion/branches/fsfs-format7/subversion/tests/cmdline/switch_tests.py Mon Feb  4 20:48:05 2013
@@ -891,7 +891,7 @@ def obstructed_switch(sbox):
 
   # svn info A/B/E/alpha
   expected_stdout = verify.RegexOutput(
-                      ".*local unversioned, incoming add upon switch",
+                      ".*local file unversioned, incoming file add upon switch",
                       match_all=False)
   actions.run_and_verify_svn2('OUTPUT', expected_stdout, [], 0, 'info',
     A_B_E_alpha)
@@ -1285,7 +1285,7 @@ def forced_switch_failures(sbox):
 
   # svn info A/B/F/pi
   expected_stdout = verify.ExpectedOutput(
-    'Tree conflict: local unversioned, incoming add upon switch\n',
+    'Tree conflict: local file unversioned, incoming file add upon switch\n',
     match_all=False)
 
   actions.run_and_verify_svn2('OUTPUT', expected_stdout, [], 0, 'info',
@@ -1463,15 +1463,15 @@ def switch_with_obstructing_local_adds(s
   expected_status.add({
     'A/B/F/gamma'     : Item(status='R ', treeconflict='C', wc_rev='1'),
     'A/B/F/G'         : Item(status='R ', treeconflict='C', wc_rev='1'),
-    'A/B/F/G/pi'      : Item(status='A ', wc_rev='-'),
-    'A/B/F/G/tau'     : Item(status='A ', wc_rev='-'),
-    'A/B/F/G/upsilon' : Item(status='A ', wc_rev='-'),
+    'A/B/F/G/pi'      : Item(status='A ', wc_rev='-', entry_status='R ', entry_rev='1'),
+    'A/B/F/G/tau'     : Item(status='A ', wc_rev='-', entry_status='R ', entry_rev='1'),
+    'A/B/F/G/upsilon' : Item(status='A ', wc_rev='-', entry_rev='0'),
     'A/B/F/G/rho'     : Item(status='D ', wc_rev='1'),
     'A/B/F/H'         : Item(status='  ', wc_rev='1'),
     'A/B/F/H/chi'     : Item(status='  ', wc_rev='1'),
     'A/B/F/H/omega'   : Item(status='  ', wc_rev='1'),
     'A/B/F/H/psi'     : Item(status='  ', wc_rev='1'),
-    'A/B/F/I'         : Item(status='A ', wc_rev='-'),
+    'A/B/F/I'         : Item(status='A ', wc_rev='-', entry_rev='0'),
   })
 
   # "Extra" files that we expect to result from the conflicts.
@@ -2213,7 +2213,7 @@ def switch_to_root(sbox):
   expected_status = svntest.actions.get_virginal_state(wc_dir, 1)
   expected_status.remove('A/D/G/pi', 'A/D/G/rho', 'A/D/G/tau')
   expected_status.add_state('A/D/G',
-                            svntest.actions.get_virginal_state(wc_dir, 1))
+                            svntest.actions.get_virginal_state(wc_dir + '/A/D/G', 1))
   expected_status.tweak('A/D/G', switched = 'S')
   svntest.actions.run_and_verify_switch(wc_dir, ADG_path, sbox.repo_url,
                                         expected_output,
@@ -2328,10 +2328,9 @@ def tree_conflicts_on_switch_1_1(sbox):
   })
 
   expected_disk = disk_empty_dirs.copy()
-  if  svntest.main.wc_is_singledb(sbox.wc_dir):
-    expected_disk.remove('D/D1', 'DF/D1', 'DD/D1', 'DD/D1/D2',
-                         'DDF/D1', 'DDF/D1/D2',
-                         'DDD/D1', 'DDD/D1/D2', 'DDD/D1/D2/D3')
+  expected_disk.remove('D/D1', 'DF/D1', 'DD/D1', 'DD/D1/D2',
+                       'DDF/D1', 'DDF/D1/D2',
+                       'DDD/D1', 'DDD/D1/D2', 'DDD/D1/D2/D3')
 
   # The files delta, epsilon, and zeta are incoming additions, but since
   # they are all within locally deleted trees they should also be schedule
@@ -2350,37 +2349,37 @@ def tree_conflicts_on_switch_1_1(sbox):
   expected_info = {
     'F/alpha' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local file delete, incoming file edit upon switch'
         + ' Source  left: .file.*/F/alpha@2'
         + ' Source right: .file.*/F/alpha@3$',
     },
     'DF/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/DF/D1@2'
         + ' Source right: .dir.*/DF/D1@3$',
     },
     'DDF/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/DDF/D1@2'
         + ' Source right: .dir.*/DDF/D1@3$',
     },
     'D/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/D/D1@2'
         + ' Source right: .dir.*/D/D1@3$',
     },
     'DD/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/DD/D1@2'
         + ' Source right: .dir.*/DD/D1@3$',
     },
     'DDD/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/DDD/D1@2'
         + ' Source right: .dir.*/DDD/D1@3$',
     },
@@ -2441,45 +2440,44 @@ def tree_conflicts_on_switch_1_2(sbox):
   expected_disk.remove('D/D1',
                        'DD/D1/D2',
                        'DDD/D1/D2/D3')
-  if svntest.main.wc_is_singledb(sbox.wc_dir):
-    expected_disk.remove('DF/D1', 'DD/D1',
-                         'DDF/D1', 'DDF/D1/D2',
-                         'DDD/D1', 'DDD/D1/D2')
+  expected_disk.remove('DF/D1', 'DD/D1',
+                       'DDF/D1', 'DDF/D1/D2',
+                       'DDD/D1', 'DDD/D1/D2')
 
   expected_info = {
     'F/alpha' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local file delete, incoming file delete upon switch'
         + ' Source  left: .file.*/F/alpha@2'
         + ' Source right: .none.*(/F/alpha@3)?$',
     },
     'DF/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/DF/D1@2'
         + ' Source right: .dir.*/DF/D1@3$',
     },
     'DDF/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/DDF/D1@2'
         + ' Source right: .dir.*/DDF/D1@3$',
     },
     'D/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/D/D1@2'
         + ' Source right: .none.*(/D/D1@3)?$',
     },
     'DD/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/DD/D1@2'
         + ' Source right: .dir.*/DD/D1@3$',
     },
     'DDD/D1' : {
       'Tree conflict' :
-        '^local delete, incoming edit upon switch'
+        '^local dir delete, incoming dir edit upon switch'
         + ' Source  left: .dir.*/DDD/D1@2'
         + ' Source right: .dir.*/DDD/D1@3$',
     },
@@ -2532,37 +2530,37 @@ def tree_conflicts_on_switch_2_1(sbox):
   expected_info = {
     'F/alpha' : {
       'Tree conflict' :
-        '^local edit, incoming delete upon switch'
+        '^local file edit, incoming file delete upon switch'
         + ' Source  left: .file.*/F/alpha@2'
         + ' Source right: .none.*(/F/alpha@3)?$',
     },
     'DF/D1' : {
       'Tree conflict' :
-        '^local edit, incoming delete upon switch'
+        '^local dir edit, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DF/D1@2'
         + ' Source right: .none.*(/DF/D1@3)?$',
     },
     'DDF/D1' : {
       'Tree conflict' :
-        '^local edit, incoming delete upon switch'
+        '^local dir edit, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DDF/D1@2'
         + ' Source right: .none.*(/DDF/D1@3)?$',
     },
     'D/D1' : {
       'Tree conflict' :
-        '^local edit, incoming delete upon switch'
+        '^local dir edit, incoming dir delete upon switch'
         + ' Source  left: .dir.*/D/D1@2'
         + ' Source right: .none.*(/D/D1@3)?$',
     },
     'DD/D1' : {
       'Tree conflict' :
-        '^local edit, incoming delete upon switch'
+        '^local dir edit, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DD/D1@2'
         + ' Source right: .none.*(/DD/D1@3)?$',
     },
     'DDD/D1' : {
       'Tree conflict' :
-        '^local edit, incoming delete upon switch'
+        '^local dir edit, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DDD/D1@2'
         + ' Source right: .none.*(/DDD/D1@3)?$',
     },
@@ -2631,37 +2629,37 @@ def tree_conflicts_on_switch_2_2(sbox):
   expected_info = {
     'F/alpha' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local file delete, incoming file delete upon switch'
         + ' Source  left: .file.*/F/alpha@2'
         + ' Source right: .none.*(/F/alpha@3)?$',
     },
     'DF/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DF/D1@2'
         + ' Source right: .none.*(/DF/D1@3)?$',
     },
     'DDF/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DDF/D1@2'
         + ' Source right: .none.*(/DDF/D1@3)?$',
     },
     'D/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/D/D1@2'
         + ' Source right: .none.*(/D/D1@3)?$',
     },
     'DD/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DD/D1@2'
         + ' Source right: .none.*(/DD/D1@3)?$',
     },
     'DDD/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DDD/D1@2'
         + ' Source right: .none.*(/DDD/D1@3)?$',
     },
@@ -2720,37 +2718,37 @@ def tree_conflicts_on_switch_3(sbox):
   expected_info = {
     'F/alpha' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local file delete, incoming file delete upon switch'
         + ' Source  left: .file.*/F/alpha@2'
         + ' Source right: .none.*(/F/alpha@3)?$',
     },
     'DF/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DF/D1@2'
         + ' Source right: .none.*(/DF/D1@3)?$',
     },
     'DDF/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DDF/D1@2'
         + ' Source right: .none.*(/DDF/D1@3)?$',
     },
     'D/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/D/D1@2'
         + ' Source right: .none.*(/D/D1@3)?$',
     },
     'DD/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DD/D1@2'
         + ' Source right: .none.*(/DD/D1@3)?$',
     },
     'DDD/D1' : {
       'Tree conflict' :
-        '^local delete, incoming delete upon switch'
+        '^local dir delete, incoming dir delete upon switch'
         + ' Source  left: .dir.*/DDD/D1@2'
         + ' Source right: .none.*(/DDD/D1@3)?$',
     },
@@ -2785,7 +2783,7 @@ def copy_with_switched_subdir(sbox):
                                      '--ignore-ancestry', E_url, G)
 
   state.tweak('A/D/G', switched='S')
-  state.remove('A/D/G/pi', 'A/D/G/rho', 'A/D/G/tau');
+  state.remove('A/D/G/pi', 'A/D/G/rho', 'A/D/G/tau')
   state.add({
     'A/D/G/alpha' : Item(status='  ', wc_rev=1),
     'A/D/G/beta' : Item(status='  ', wc_rev=1),
@@ -2908,6 +2906,33 @@ def different_node_kind(sbox):
   switch_to_file(sbox, 'iota', 'A/C')
   switch_to_file(sbox, 'A/D/gamma', 'A/D/G')
 
+@Issue(3332, 3333)
+def switch_to_spaces(sbox):
+  "switch to a directory with spaces in its name"
+
+  sbox.build()
+  wc_dir = sbox.wc_dir
+  repo_url = sbox.repo_url
+
+  # Paths are normalized in the command processing, so %20 is equivalent to ' '
+  svntest.actions.run_and_verify_svn(None, None, [],
+                                     'cp', repo_url + '/A',
+                                           repo_url + '/A%20with space',
+                                     '-m', '')
+
+  svntest.actions.run_and_verify_svn(None, None, [],
+                                     'mv', repo_url + '/A%20with space',
+                                           repo_url + '/A with%20more spaces',
+                                     '-m', '')
+
+  expected_status = svntest.actions.get_virginal_state(wc_dir, 3)
+  expected_status.tweak('A', switched='S')
+  expected_status.tweak('', 'iota', wc_rev=1)
+
+  svntest.actions.run_and_verify_switch(sbox.wc_dir, sbox.ospath('A'),
+                                        repo_url + '/A%20with more%20spaces',
+                                        None, None, expected_status)
+
 ########################################################################
 # Run the tests
 
@@ -2948,6 +2973,7 @@ test_list = [ None,
               copy_with_switched_subdir,
               up_to_old_rev_with_subtree_switched_to_root,
               different_node_kind,
+              switch_to_spaces,
               ]
 
 if __name__ == '__main__':

Modified: subversion/branches/fsfs-format7/subversion/tests/cmdline/tree_conflict_tests.py
URL: http://svn.apache.org/viewvc/subversion/branches/fsfs-format7/subversion/tests/cmdline/tree_conflict_tests.py?rev=1442344&r1=1442343&r2=1442344&view=diff
==============================================================================
--- subversion/branches/fsfs-format7/subversion/tests/cmdline/tree_conflict_tests.py (original)
+++ subversion/branches/fsfs-format7/subversion/tests/cmdline/tree_conflict_tests.py Mon Feb  4 20:48:05 2013
@@ -1223,7 +1223,7 @@ def actual_only_node_behaviour(sbox):
 
   # info
   expected_info = {
-    'Tree conflict': 'local missing, incoming edit upon merge.*',
+    'Tree conflict': 'local file missing, incoming file edit upon merge.*',
     'Name': 'foo',
     'Schedule': 'normal',
     'Node Kind': 'none',