You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@subversion.apache.org by ju...@apache.org on 2022/01/14 14:01:51 UTC

svn commit: r1897034 [10/37] - in /subversion/branches/multi-wc-format: ./ build/ build/ac-macros/ build/generator/ build/generator/swig/ build/generator/templates/ contrib/client-side/ contrib/client-side/svn_load_dirs/ contrib/hook-scripts/ contrib/s...

Modified: subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/fs.py
URL: http://svn.apache.org/viewvc/subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/fs.py?rev=1897034&r1=1897033&r2=1897034&view=diff
==============================================================================
--- subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/fs.py (original)
+++ subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/fs.py Fri Jan 14 14:01:45 2022
@@ -20,6 +20,7 @@
 #
 #
 import os, unittest, sys, errno
+import os.path
 from tempfile import mkstemp
 from subprocess import Popen, PIPE
 try:
@@ -29,15 +30,215 @@ except ImportError:
   # Python <3.0
   from urlparse import urljoin
 
-from svn import core, repos, fs, client
+from svn import core, repos, fs, client, delta
 import utils
 
+# Helper functions.
+
+# brought from subversion/test/svn_test_fs.c
+class SubversionTestTreeEntry:
+  def __init__(self, path, contents):
+    self.path = path
+    self.contents = contents
+
+def svn_test__stream_to_string(stream):
+  ret_str = ''
+  while True:
+    rbuf = core.svn_stream_read_full(stream, 10)
+    if not rbuf:
+      return ret_str
+    if not isinstance(rbuf, str):
+      rbuf = rbuf.decode('utf-8')
+    ret_str += rbuf
+
+def svn_test__set_file_contents(root, path, contents):
+  if not isinstance(contents, bytes):
+    contents = contents.encode('utf-8')
+  consumer_func, consumer_baton = fs.apply_textdelta(root, path, None, None)
+  delta.svn_txdelta_send_string(contents, consumer_func, consumer_baton)
+  return
+
+def svn_test__get_file_contents(root, path):
+  return svn_test__stream_to_string(fs.file_contents(root, path))
+
+def _get_dir_entries(root, path, tree_entries=None):
+  if tree_entries is None:
+    tree_entries = {}
+  bpath = path if isinstance(path, bytes) else path.encode('utf-8')
+
+  entries = fs.dir_entries(root, bpath)
+
+  # Copy this list to the master list with the path prepended to the names
+  for key in entries:
+    dirent = entries[key]
+
+    # Calculate the full path of this entry (by appending the name
+    # to the path thus far)
+    full_path = core.svn_dirent_join(bpath, dirent.name)
+    if not isinstance(full_path, str):
+      full_path = full_path.decode('utf-8')
+
+    # Now, copy this dirent to the master hash, but this time, use
+    # the full path for the key
+    tree_entries[full_path] = dirent
+
+    # If this entry is a directory, recurse int the tree.
+    if dirent.kind == core.svn_node_dir:
+      tree_entries = _get_dir_entries(root, full_path,
+                                      tree_entries=tree_entries)
+  return tree_entries
+
+
+def _validate_tree_entry(root, path, contents):
+
+  # Verify that nod types are reported consistently.
+  kind = fs.check_path(root, path)
+  is_dir = fs.is_dir(root, path)
+  is_file = fs.is_file(root,path)
+
+  assert not is_dir or kind == core.svn_node_dir
+  assert not is_file or kind == core.svn_node_file
+  assert is_dir or is_file
+
+  # Verify that this is the expected type of node
+  if (not is_dir and contents is None) or (is_dir and contents is not None):
+    err_msg = "node '%s' in tree was of unexpected node type" % path
+    raise core.SubversionExcepton(err_msg, core.SVN_ERR_FS_GENERLL)
+
+  # Verify that the contents are as expected (files only)
+  if not is_dir:
+    # File lengths.
+    assert len(contents) == fs.file_length(root, path)
+
+    # Text contents.
+    rstream = fs.file_contents(root, path)
+    rstring = svn_test__stream_to_string(rstream)
+    if rstring != contents:
+      err_msg = "node '%s' in tree had unexpected contents" % path
+      raise core.SubversionExcepton(err_msg, core.SVN_ERR_FS_GENERLL)
+  return
+
+
+VALIDATE_TREE_NA_NAME = "es-vee-en"
+
+def svn_test__validate_tree(root, entries):
+  def format_entries(entries):
+    return "   " + "\n   ".join(entries) + "\n" if entries else ""
+
+  # There should be no entry with this name.
+
+  # Recursively get the whole tree
+  tree_entries = _get_dir_entries(root, "")
+
+  # Copy our list of expected_entries into dict
+  expected_entries = dict([(ent.path, ent) for ent in entries])
+
+  # For each entry in our EXPECTED_ENTRIES dict, try to find that
+  # entry in the TREE_ENTRIES dict given us by the FS.  If we find
+  # that object, remove it from the TREE_ENTRIES.  If we don't find
+  # it, there's a problem to report!
+  corrupt_entries = []
+  missing_entries = []
+  for key in expected_entries:
+    entry = expected_entries[key]
+    if key in tree_entries:
+      try:
+        epath = entry.path
+        if not isinstance(epath, str):
+          epath = epath.decode('utf-8')
+        econtents = entry.contents
+        if econtents is not None and not isinstance(econtents, str):
+          econtents = econtents.decode('utf-8')
+        _validate_tree_entry(root, epath, entry.contents)
+      except (SubversionException,AssertionError) as e:
+        # Append this entry name to the list of corrupt entries.
+        corrupt_entries.append(key)
+      del tree_entries[key]
+    else:
+      # Append this entry name to the list of missing entries.
+      missing_entries.append(key)
+  # Any entries still left in TREE_ENTRIES are extra ones that are
+  # not expected to be present.  Assemble a string with their names.
+  extra_entries = list(tree_entries.keys())
+
+  # Test that non-existent paths will not be found.
+  # Skip this test if somebody sneakily added NA_NAME.
+  if expected_entries.get(VALIDATE_TREE_NA_NAME) is not None:
+    assert fs.check_path(root, VALIDATE_TREE_NA_NAME) == core.svn_node_none
+    assert not fs.is_file(root, VALIDATE_TREE_NA_NAME)
+    assert not fs.is_dir(root, VALIDATE_TREE_NA_NAME)
+
+  if missing_entries or extra_entries or corrupt_entries:
+    err_msg = ("Repository tree does not look as expected.\n"
+               "Corrupt entries:\n%s"
+               "Missing entries:\n%s"
+               "Extra entries:\n%s"
+               % tuple(map(format_entries,(corrupt_entries,
+                                           missing_entries,
+                                           extra_entries))))
+    raise core.SubversionException(err_msg, core.SVN_ERR_FS_GENERAL)
+  return
+
+
+greek_tree_nodes = [
+  SubversionTestTreeEntry("iota",         "This is the file 'iota'.\n" ),
+  SubversionTestTreeEntry("A",            None ),
+  SubversionTestTreeEntry("A/mu",         "This is the file 'mu'.\n" ),
+  SubversionTestTreeEntry("A/B",          None ),
+  SubversionTestTreeEntry("A/B/lambda",   "This is the file 'lambda'.\n" ),
+  SubversionTestTreeEntry("A/B/E",        None ),
+  SubversionTestTreeEntry("A/B/E/alpha",  "This is the file 'alpha'.\n" ),
+  SubversionTestTreeEntry("A/B/E/beta",   "This is the file 'beta'.\n" ),
+  SubversionTestTreeEntry("A/B/F",        None ),
+  SubversionTestTreeEntry("A/C",          None ),
+  SubversionTestTreeEntry("A/D",          None ),
+  SubversionTestTreeEntry("A/D/gamma",    "This is the file 'gamma'.\n" ),
+  SubversionTestTreeEntry("A/D/G",        None ),
+  SubversionTestTreeEntry("A/D/G/pi",     "This is the file 'pi'.\n" ),
+  SubversionTestTreeEntry("A/D/G/rho",    "This is the file 'rho'.\n" ),
+  SubversionTestTreeEntry("A/D/G/tau",    "This is the file 'tau'.\n" ),
+  SubversionTestTreeEntry("A/D/H",        None ),
+  SubversionTestTreeEntry("A/D/H/chi",    "This is the file 'chi'.\n" ),
+  SubversionTestTreeEntry("A/D/H/psi",    "This is the file 'psi'.\n" ),
+  SubversionTestTreeEntry("A/D/H/omega",  "This is the file 'omega'.\n" )]
+
+def svn_test__check_greek_tree(root):
+  # Loop through the list of files, checking for matching content.
+  for node in greek_tree_nodes:
+    if node.contents is not None:
+      rstream = fs.file_contents(root, node.path)
+      rstring = svn_test__stream_to_string(rstream)
+      if not isinstance(rstring, str):
+        rstring = rstring.decode('utf-8')
+      if rstring != node.contents:
+        raise core.SubversionException(
+                    "data read != data written in file '%s'." % node.path,
+                    core.SVN_ERR_FS_GENERAL)
+  return
+
+
+def svn_test__create_greek_tree_at(txn_root, root_dir):
+  for node in greek_tree_nodes:
+    path = core.svn_relpath_join(root_dir, node.path)
+
+    if node.contents is not None:
+      fs.make_file(txn_root, path)
+      svn_test__set_file_contents(txn_root, path, node.contents)
+    else:
+      fs.make_dir(txn_root, path)
+  return
+
+
+def svn_test__create_greek_tree(txn_root):
+  return svn_test__create_greek_tree_at(txn_root, "")
+
+
 class SubversionFSTestCase(unittest.TestCase):
   """Test cases for the Subversion FS layer"""
 
   def log_message_func(self, items, pool):
     """ Simple log message provider for unit tests. """
-    return "Test unicode log message"
+    return b"Test unicode log message"
 
   def setUp(self):
     """Load a Subversion repository"""
@@ -68,8 +269,12 @@ class SubversionFSTestCase(unittest.Test
 
     clientctx.auth_baton = core.svn_auth_open(providers)
 
-    commitinfo = client.import2(self.tmpfile,
-                                urljoin(self.repos_uri +"/", "trunk/UniTest.txt"),
+    if isinstance(self.tmpfile, bytes):
+        tmpfile_bytes = self.tmpfile
+    else:
+        tmpfile_bytes = self.tmpfile.encode('UTF-8')
+    commitinfo = client.import2(tmpfile_bytes,
+                                urljoin(self.repos_uri + b"/",b"trunk/UniTest.txt"),
                                 True, True,
                                 clientctx)
 
@@ -87,11 +292,12 @@ class SubversionFSTestCase(unittest.Test
     """Test diffing of a repository path using the internal diff."""
 
     # Test standard internal diff
-    fdiff = fs.FileDiff(fs.revision_root(self.fs, self.commitedrev), "/trunk/UniTest.txt",
+    fdiff = fs.FileDiff(fs.revision_root(self.fs, self.commitedrev), b"/trunk/UniTest.txt",
                         None, None, diffoptions=None)
 
     diffp = fdiff.get_pipe()
     diffoutput = diffp.read().decode('utf8')
+    diffp.close()
 
     self.assertTrue(diffoutput.find(u'-' + self.unistr) > 0)
 
@@ -108,13 +314,990 @@ class SubversionFSTestCase(unittest.Test
       else:
         raise err
 
-    fdiff = fs.FileDiff(fs.revision_root(self.fs, self.commitedrev), "/trunk/UniTest.txt",
+    fdiff = fs.FileDiff(fs.revision_root(self.fs, self.commitedrev), b"/trunk/UniTest.txt",
                         None, None, diffoptions=[])
     diffp = fdiff.get_pipe()
     diffoutput = diffp.read().decode('utf8')
+    diffp.close()
 
     self.assertTrue(diffoutput.find(u'< ' + self.unistr) > 0)
 
+
+  # Helper:  commit TXN, expecting either success or failure:
+  #
+  # If EXPECTED_CONFLICT is null, then the commit is expected to
+  # succeed.  If it does succeed, set *NEW_REV to the new revision;
+  # raise error.
+  #
+  # If EXPECTED_CONFLICT is not None, it is either the empty string or
+  # the expected path of the conflict.  If it is the empty string, any
+  # conflict is acceptable.  If it is a non-empty string, the commit
+  # must fail due to conflict, and the conflict path must match
+  # EXPECTED_CONFLICT.  If they don't match, raise Assertion error.
+  #
+  # If a conflict is expected but the commit succeeds anyway, raise
+  # Assertion error.  If the commit fails but does not provide an error,
+  # raise Assertion error.
+  #
+  # This function was taken from test_commit_txn() in
+  # subversion/tests/libsvn_fs/fs-test.c but renamed to avoid confusion.
+  #
+  def check_commit_txn(self, txn, expected_conflict, pool=None):
+    if (isinstance(expected_conflict, bytes)
+        and not isinstance(expected_conflict, str)):
+      expected_conflict = expected_conflict.decode('utf-8')
+    err = None
+    new_rev = None
+    conflict = None
+    try:
+      conflict, new_rev = fs.commit_txn(txn, pool)
+    except core.SubversionException as e:
+      err = e
+      self.assertTrue(hasattr(e, 'conflict_p'))
+      conflict = e.conflict_p
+      if isinstance(conflict, bytes) and not isinstance(conflict, str):
+        conflict = conflict.decode('utf-8')
+      self.assertTrue(hasattr(e, 'new_rev'))
+      new_rev = e.new_rev
+
+    if err and err.apr_err == core.SVN_ERR_FS_CONFLICT:
+      self.assertIsNotNone(expected_conflict,
+          "commit conflicted at '%s', but no conflict expected"
+          % conflict if conflict else '(missing conflict info!)')
+      self.assertIsNotNone(conflict,
+          "commit conflicted as expected, "
+          "but no conflict path was returned ('%s' expected)"
+          % expected_conflict)
+      if expected_conflict:
+        self.assertEqual(conflict, expected_conflict,
+            "commit conflicted at '%s', but expected conflict at '%s'"
+            % (conflict, expected_conflict))
+
+      # The svn_fs_commit_txn() API promises to set *NEW_REV to an
+      # invalid revision number in the case of a conflict.
+      self.assertEqual(new_rev, core.SVN_INVALID_REVNUM,
+                       "conflicting commit returned valid new revision")
+
+    elif err:
+      # commit may have succeeded, but always report an error
+      if new_rev != core.SVN_INVALID_REVNUM:
+        raise core.SubversionException(
+                    "commit succeeded but something else failed",
+                    err.apr_err, err)
+      else:
+        raise core.SubversionException(
+                    "commit failed due to something other than conflict",
+                    err.apr_err, err)
+    else:
+      # err == None, commit should have succeeded
+      self.assertNotEqual(new_rev, core.SVN_INVALID_REVNUM,
+                          "commit failed but no error was returned")
+      self.assertIsNone(expected_conflict,
+                        "commit succeeded that was expected to fail at '%s'"
+                        % expected_conflict)
+    return new_rev
+
+
+  def test_basic_commit(self):
+    """Test committing against an empty repository."""
+
+    # Prepare a filesystem
+    handle, repo_path, rep_uri = self.temper.alloc_empty_repo(
+                                              "-test-repo-basic-commit")
+    test_fs = repos.fs(handle)
+
+    # Save the current youngest revision.
+    before_rev = fs.youngest_rev(test_fs)
+
+    # Prepare a txn to recive the greek tree.
+    txn = fs.begin_txn2(test_fs,0, 0)
+    txn_root = fs.txn_root(txn)
+
+    # Paranoidly check that the current youngest rev is unchanged.
+    after_rev = fs.youngest_rev(test_fs)
+    self.assertEqual(before_rev, after_rev,
+                     'youngest revision changed unexpectedly')
+
+    # Create the greek tree
+    svn_test__create_greek_tree(txn_root)
+    self.assertTrue(fs.is_txn_root(txn_root))
+    self.assertFalse(fs.is_revision_root(txn_root))
+
+    # Commit it.
+    _, after_rev = fs.commit_txn(txn)
+    self.assertNotEqual(after_rev, core.SVN_INVALID_REVNUM)
+
+    # Make sure it's a different revision than before.
+    self.assertNotEqual(after_rev, before_rev,
+                        "youngest revision failed to change")
+
+    # Get root of the revision
+    revision_root = fs.revision_root(test_fs, after_rev)
+    self.assertFalse(fs.is_txn_root(revision_root))
+    self.assertTrue(fs.is_revision_root(revision_root))
+
+    # Check the tree.
+    svn_test__check_greek_tree(revision_root)
+
+  def test_merging_commit(self):
+    """Commit with merging (committing against non-youngest)."""
+    # Python implementation of fs-test.c: merging_commit()
+
+    # Prepare a filesystem
+    handle, repo_path, rep_uri = self.temper.alloc_empty_repo(
+                                              "-test-repo-merging-commit")
+    test_fs = repos.fs(handle)
+
+    # initialize our revision number stuffs.
+    revisions = [core.SVN_INVALID_REVNUM] * 24
+    revision_count = 0
+    revisions[revision_count] = 0
+    revision_count += 1
+
+    ########################################################################
+    # REVISION 0
+    ########################################################################
+
+    # In one txn, create and commit the greek tree.
+    txn = fs.begin_txn2(test_fs, 0, 0)
+    txn_root = fs.txn_root(txn)
+    svn_test__create_greek_tree(txn_root)
+    after_rev = self.check_commit_txn(txn, None)
+
+    ########################################################################
+    # REVISION 1
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("iota",        "This is the file 'iota'.\n"),
+        SubversionTestTreeEntry("A"   ,        None),
+        SubversionTestTreeEntry("A/mu",        "This is the file 'mu'.\n"),
+        SubversionTestTreeEntry("A/B",         None),
+        SubversionTestTreeEntry("A/B/lambda",  "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E",       None),
+        SubversionTestTreeEntry("A/B/E/alpha", "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",  "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F",       None),
+        SubversionTestTreeEntry("A/C",         None),
+        SubversionTestTreeEntry("A/D",         None),
+        SubversionTestTreeEntry("A/D/gamma",   "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G",       None),
+        SubversionTestTreeEntry("A/D/G/pi",    "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",   "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",   "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/H",       None),
+        SubversionTestTreeEntry("A/D/H/chi",   "This is the file 'chi'.\n"),
+        SubversionTestTreeEntry("A/D/H/psi",   "This is the file 'psi'.\n"),
+        SubversionTestTreeEntry("A/D/H/omega", "This is the file 'omega'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # Let's add a directory and some files to the tree, and delete 'iota'
+    txn = fs.begin_txn2(test_fs, revisions[revision_count-1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.make_dir(txn_root, "A/D/I")
+    fs.make_file(txn_root, "A/D/I/delta")
+    svn_test__set_file_contents(txn_root, "A/D/I/delta",
+                                "This is the file 'delta'.\n")
+    fs.make_file(txn_root, "A/D/I/epsilon")
+    svn_test__set_file_contents(txn_root, "A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")
+    fs.make_file(txn_root, "A/C/kappa")
+    svn_test__set_file_contents(txn_root, "A/C/kappa",
+                                "This is the file 'kappa'.\n")
+    fs.delete(txn_root, "iota")
+    after_rev = self.check_commit_txn(txn, None)
+
+    ########################################################################
+    # REVISION 2
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",          "This is the file 'mu'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/H", None),
+        SubversionTestTreeEntry("A/D/H/chi",
+                                "This is the file 'chi'.\n"),
+        SubversionTestTreeEntry("A/D/H/psi",
+                                "This is the file 'psi'.\n"),
+        SubversionTestTreeEntry("A/D/H/omega",
+                                "This is the file 'omega'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # We don't think the A/D/H directory is pulling its weight...let's
+    # knock it off.  Oh, and let's re-add iota, too.
+    txn = fs.begin_txn2(test_fs, revisions[revision_count-1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.delete(txn_root, "A/D/H")
+    fs.make_file(txn_root, "iota")
+    svn_test__set_file_contents(txn_root, "iota",
+                                "This is the new file 'iota'.\n")
+    after_rev = self.check_commit_txn(txn, None)
+
+    ########################################################################
+    # REVISION 3
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("iota",
+                                "This is the new file 'iota'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "This is the file 'mu'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # Delete iota (yet again).
+    txn = fs.begin_txn2(test_fs, revisions[revision_count-1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.delete(txn_root, "iota")
+    after_rev = self.check_commit_txn(txn, None)
+
+    ########################################################################
+    # REVISION 4
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "This is the file 'mu'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    ########################################################################
+    # GIVEN:  A and B, with common ancestor ANCESTOR, where A and B
+    # directories, and E, an entry in either A, B, or ANCESTOR.
+    #
+    # For every E, the following cases exist:
+    #  - E exists in neither ANCESTOR nor A.
+    #  - E doesn't exist in ANCESTOR, and has been added to A.
+    #  - E exists in ANCESTOR, but has been deleted from A.
+    #  - E exists in both ANCESTOR and A ...
+    #    - but refers to different node revisions.
+    #    - and refers to the same node revision.
+    #
+    # The same set of possible relationships with ANCESTOR holds for B,
+    # so there are thirty-six combinations.  The matrix is symmetrical
+    # with A and B reversed, so we only have to describe one triangular
+    # half, including the diagonal --- 21 combinations.
+    #
+    # Our goal here is to test all the possible scenarios that can
+    # occur given the above boolean logic table, and to make sure that
+    # the results we get are as expected.
+    #
+    # The test cases below have the following features:
+    #
+    # - They run straight through the scenarios as described in the
+    #   `structure' document at this time.
+    #
+    # - In each case, a txn is begun based on some revision (ANCESTOR),
+    #   is modified into a new tree (B), and then is attempted to be
+    #   committed (which happens against the head of the tree, A).
+    #
+    # - If the commit is successful (and is *expected* to be such),
+    #   that new revision (which exists now as a result of the
+    #   successful commit) is thoroughly tested for accuracy of tree
+    #   entries, and in the case of files, for their contents.  It is
+    #   important to realize that these successful commits are
+    #   advancing the head of the tree, and each one effective becomes
+    #   the new `A' described in further test cases.
+    #
+    ########################################################################
+
+    # (6) E exists in neither ANCESTOR nor A.
+    #   (1) E exists in neither ANCESTOR nor B.  Can't occur, by
+    #     assumption that E exists in either A, B, or ancestor.
+
+    #   (1) E has been added to B.  Add E in the merged result.
+    txn = fs.begin_txn2(test_fs, revisions[0], 0)
+    txn_root = fs.txn_root(txn)
+    fs.make_file(txn_root, "theta")
+    svn_test__set_file_contents(txn_root, "theta",
+                                "This is the file 'theta'.\n")
+    after_rev = self.check_commit_txn(txn, None)
+
+    ########################################################################
+    # REVISION 5
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("theta",
+                                "This is the file 'theta'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "This is the file 'mu'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    #   (1) E has been deleted from B.  Can't occur, by assumption that
+    #     E doesn't exist in ANCESTOR.
+
+    #   (3) E exists in both ANCESTOR and B.  Can't occur, by
+    #     assumption that E doesn't exist in ancestor.
+
+
+    # (5) E doesn't exist in ANCESTOR, and has been added to A.
+    #   (1) E doesn't exist in ANCESTOR, and has been added to B.
+    txn = fs.begin_txn2(test_fs, revisions[4], 0)
+    txn_root = fs.txn_root(txn)
+    fs.make_file(txn_root, "theta")
+    svn_test__set_file_contents(txn_root, "theta",
+                                "This is another file 'theta'.\n")
+
+    # TXN must actually be based upon revisions[4] (instead of HEAD).
+    self.assertEqual(fs.txn_base_revision(txn), revisions[4])
+
+    failed_rev = self.check_commit_txn(txn, "/theta")
+    fs.abort_txn(txn)
+
+    #   (1) E exists in ANCESTOR, but has been deleted from B.  Can't
+    #     occur, by assumption that E doesn't exist in ANCESTOR.
+
+    #   (3) E exists in both ANCESTOR and B.  Can't occur, by assumption
+    #     that E doesn't exist in ANCESTOR.
+
+    self.assertEqual(failed_rev, core.SVN_INVALID_REVNUM)
+
+    # (4) E exists in ANCESTOR, but has been deleted from A
+    #   (1) E exists in ANCESTOR, but has been deleted from B.  If
+    #     neither delete was a result of a rename, then omit E from the
+    #     merged tree.  Otherwise, conflict.
+    #     ### cmpilato todo: the rename case isn't actually handled by
+    #     merge yet, so we know we won't get a conflict here.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.delete(txn_root, "A/D/H")
+
+    # TXN must actually be based upon revisions[1] (instead of HEAD).
+    self.assertEqual(fs.txn_base_revision(txn), revisions[1])
+
+    # We used to create the revision like this before fixing issue
+    # #2751 -- Directory prop mods reverted in overlapping commits scenario.
+    #
+    # But we now expect that to fail as out of date
+
+    failed_rev = self.check_commit_txn(txn, "/A/D/H")
+
+    self.assertEqual(failed_rev, core.SVN_INVALID_REVNUM)
+
+    ########################################################################
+    # REVISION 6
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("theta",
+                                "This is the file 'theta'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "This is the file 'mu'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # Try deleting a file F inside a subtree S where S does not exist
+    # in the most recent revision, but does exist in the ancestor
+    # tree.  This should conflict.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.delete(txn_root, "A/D/H/omega")
+    failed_rev = self.check_commit_txn(txn, "/A/D/H")
+    fs.abort_txn(txn)
+
+    self.assertEqual(failed_rev, core.SVN_INVALID_REVNUM)
+
+    # E exists in both ANCESTOR and B ...
+    #   (1) but refers to different nodes.  Conflict.
+
+    txn = fs.begin_txn2(test_fs, after_rev, 0)
+    txn_root = fs.txn_root(txn)
+    fs.make_dir(txn_root, "A/D/H")
+    after_rev = self.check_commit_txn(txn, None)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    ########################################################################
+    # REVISION 7
+    ########################################################################
+
+    # Re-remove A/D/H because future tests expect it to be absent.
+    txn = fs.begin_txn2(test_fs, revisions[revision_count - 1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.delete(txn_root, "A/D/H")
+    after_rev = self.check_commit_txn(txn, None)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    ########################################################################
+    # REVISION 8 (looks exactly like revision 6, we hope)
+    ########################################################################
+
+    # (1) but refers to different revisions of the same node.
+    # Conflict.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.make_file(txn_root, "A/D/H/zeta")
+    after_rev = self.check_commit_txn(txn, "/A/D/H")
+    fs.abort_txn(txn)
+
+    # (1) and refers to the same node revision.  Omit E from the
+    # merged tree.  This is already tested in Merge-Test 3
+    # (A/D/H/chi, A/D/H/psi, e.g.), but we'll test it here again
+    # anyway.  A little paranoia never hurt anyone.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.delete(txn_root, "A/mu")  # unrelated change
+    after_rev = self.check_commit_txn(txn, None)
+
+    ########################################################################
+    # REVISION 9
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("theta",
+                                "This is the file 'theta'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # Preparation for upcoming tests.
+    # We make a new head revision, with A/mu restored, but containing
+    # slightly different contents than its first incarnation.
+    txn = fs.begin_txn2(test_fs, revisions[revision_count - 1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.make_file(txn_root, "A/mu")
+    svn_test__set_file_contents(txn_root, "A/mu",
+                                "A new file 'mu'.\n")
+    fs.make_file(txn_root, "A/D/G/xi")
+    svn_test__set_file_contents(txn_root, "A/D/G/xi",
+                                "This is the file 'xi'.\n")
+    after_rev = self.check_commit_txn(txn, None)
+    ########################################################################
+    # REVISION 10
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("theta",
+                                "This is the file 'theta'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "A new file 'mu'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/G/xi",
+                                "This is the file 'xi'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # (3) E exists in both ANCESTOR and A, but refers to different
+    #  nodes.
+    #
+    #   (1) E exists in both ANCESTOR and B, but refers to different
+    #   nodes, and not all nodes are directories.  Conflict.
+
+    #   ### kff todo: A/mu's contents will be exactly the same.
+    #   If the fs ever starts optimizing this case, these tests may
+    #   start to fail.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.delete(txn_root, "A/mu")
+    fs.make_file(txn_root, "A/mu")
+    svn_test__set_file_contents(txn_root, "A/mu",
+                                "This is the file 'mu'.\n")
+    after_rev = self.check_commit_txn(txn, "/A/mu")
+    fs.abort_txn(txn)
+
+    #  (1) E exists in both ANCESTOR and B, but refers to different
+    #  revisions of the same node.  Conflict.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    svn_test__set_file_contents(txn_root, "A/mu",
+                                "A change to file 'mu'.\n")
+    after_rev = self.check_commit_txn(txn, "/A/mu")
+    fs.abort_txn(txn)
+
+    #  (1) E exists in both ANCESTOR and B, and refers to the same
+    #  node revision.  Replace E with A's node revision.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    old_mu_contents = svn_test__get_file_contents(txn_root, "A/mu")
+    if (not isinstance(old_mu_contents, str)
+        or old_mu_contents != "This is the file 'mu'.\n"):
+      raise core.SubversionException(
+                    "got wrong contents from an old revision tree",
+                    core.SVN_ERR_FS_GENERAL)
+    fs.make_file(txn_root, "A/sigma")  # unrelated change
+    svn_test__set_file_contents(txn_root, "A/sigma",
+                                "This is the file 'sigma'.\n")
+    after_rev = self.check_commit_txn(txn, None)
+    ########################################################################
+    # REVISION 11
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("theta",
+                                "This is the file 'theta'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "A new file 'mu'.\n"),
+        SubversionTestTreeEntry("A/sigma",
+                                "This is the file 'sigma'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "This is the file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/G/xi",
+                                "This is the file 'xi'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # Preparation for upcoming tests.
+    # We make a new head revision.  There are two changes in the new
+    # revision: A/B/lambda has been modified.  We will also use the
+    # recent addition of A/D/G/xi, treated as a modification to
+    # A/D/G.
+    txn = fs.begin_txn2(test_fs, revisions[revision_count - 1], 0)
+    txn_root = fs.txn_root(txn)
+    svn_test__set_file_contents(txn_root, "A/B/lambda",
+                                "Change to file 'lambda'.\n")
+    after_rev = self.check_commit_txn(txn, None)
+    ########################################################################
+    # REVISION 12
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("theta",
+                                "This is the file 'theta'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "A new file 'mu'.\n"),
+        SubversionTestTreeEntry("A/sigma",
+                                "This is the file 'sigma'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "Change to file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/G/xi",
+                                "This is the file 'xi'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # (2) E exists in both ANCESTOR and A, but refers to different
+    # revisions of the same node.
+
+    #   (1a) E exists in both ANCESTOR and B, but refers to different
+    #   revisions of the same file node.  Conflict.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    svn_test__set_file_contents(txn_root, "A/B/lambda",
+                                "A different change to 'lambda'.\n")
+    after_rev = self.check_commit_txn(txn, "/A/B/lambda")
+    fs.abort_txn(txn)
+
+    #   (1b) E exists in both ANCESTOR and B, but refers to different
+    #   revisions of the same directory node.  Merge A/E and B/E,
+    #   recursively.  Succeed, because no conflict beneath E.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.make_file(txn_root, "A/D/G/nu")
+    svn_test__set_file_contents(txn_root, "A/D/G/nu",
+                                "This is the file 'nu'.\n")
+    after_rev = self.check_commit_txn(txn, None)
+    ########################################################################
+    # REVISION 13
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("theta",
+                                "This is the file 'theta'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "A new file 'mu'.\n"),
+        SubversionTestTreeEntry("A/sigma",
+                                "This is the file 'sigma'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "Change to file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is the file 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/G/xi",
+                                "This is the file 'xi'.\n"),
+        SubversionTestTreeEntry("A/D/G/nu",
+                                "This is the file 'nu'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    #   (1c) E exists in both ANCESTOR and B, but refers to different
+    #   revisions of the same directory node.  Merge A/E and B/E,
+    #   recursively.  Fail, because conflict beneath E.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    fs.make_file(txn_root, "A/D/G/xi")
+    svn_test__set_file_contents(txn_root, "A/D/G/xi",
+                                "This is a different file 'xi'.\n")
+    after_rev = self.check_commit_txn(txn, "/A/D/G/xi")
+    fs.abort_txn(txn)
+
+    #   (1) E exists in both ANCESTOR and B, and refers to the same node
+    #   revision.  Replace E with A's node revision.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    old_lambda_ctnts = svn_test__get_file_contents(txn_root, "A/B/lambda")
+    if (not isinstance(old_lambda_ctnts, str)
+        or old_lambda_ctnts != "This is the file 'lambda'.\n"):
+      raise core.SubversionException(
+                    "got wrong contents from an old revision tree",
+                    core.SVN_ERR_FS_GENERAL)
+    svn_test__set_file_contents(txn_root, "A/D/G/rho",
+                                "This is an irrelevant change to 'rho'.\n")
+    after_rev = self.check_commit_txn(txn, None)
+    ########################################################################
+    # REVISION 14
+    ########################################################################
+    expected_entries = [
+        # path, contents (None = dir)
+        SubversionTestTreeEntry("theta",
+                                "This is the file 'theta'.\n"),
+        SubversionTestTreeEntry("A", None),
+        SubversionTestTreeEntry("A/mu",
+                                "A new file 'mu'.\n"),
+        SubversionTestTreeEntry("A/sigma",
+                                "This is the file 'sigma'.\n"),
+        SubversionTestTreeEntry("A/B", None),
+        SubversionTestTreeEntry("A/B/lambda",
+                                "Change to file 'lambda'.\n"),
+        SubversionTestTreeEntry("A/B/E", None),
+        SubversionTestTreeEntry("A/B/E/alpha",
+                                "This is the file 'alpha'.\n"),
+        SubversionTestTreeEntry("A/B/E/beta",
+                                "This is the file 'beta'.\n"),
+        SubversionTestTreeEntry("A/B/F", None),
+        SubversionTestTreeEntry("A/C", None),
+        SubversionTestTreeEntry("A/C/kappa",
+                                "This is the file 'kappa'.\n"),
+        SubversionTestTreeEntry("A/D", None),
+        SubversionTestTreeEntry("A/D/gamma",
+                                "This is the file 'gamma'.\n"),
+        SubversionTestTreeEntry("A/D/G", None),
+        SubversionTestTreeEntry("A/D/G/pi",
+                                "This is the file 'pi'.\n"),
+        SubversionTestTreeEntry("A/D/G/rho",
+                                "This is an irrelevant change to 'rho'.\n"),
+        SubversionTestTreeEntry("A/D/G/tau",
+                                "This is the file 'tau'.\n"),
+        SubversionTestTreeEntry("A/D/G/xi",
+                                "This is the file 'xi'.\n"),
+        SubversionTestTreeEntry("A/D/G/nu",
+                                "This is the file 'nu'.\n"),
+        SubversionTestTreeEntry("A/D/I", None),
+        SubversionTestTreeEntry("A/D/I/delta",
+                                "This is the file 'delta'.\n"),
+        SubversionTestTreeEntry("A/D/I/epsilon",
+                                "This is the file 'epsilon'.\n")]
+    revision_root = fs.revision_root(test_fs, after_rev)
+    svn_test__validate_tree(revision_root, expected_entries)
+    revisions[revision_count] = after_rev
+    revision_count += 1
+
+    # (1) E exists in both ANCESTOR and A, and refers to the same node
+    # revision.
+
+    #   (1) E exists in both ANCESTOR and B, and refers to the same
+    #   node revision.  Nothing has happened to ANCESTOR/E, so no
+    #   change is necessary.
+
+    #   This has now been tested about fifty-four trillion times.  We
+    #   don't need to test it again here.
+
+    # E exists in ANCESTOR, but has been deleted from A.  E exists in
+    # both ANCESTOR and B but refers to different revisions of the same
+    # node.  Conflict.
+    txn = fs.begin_txn2(test_fs, revisions[1], 0)
+    txn_root = fs.txn_root(txn)
+    svn_test__set_file_contents(txn_root, "iota",
+                                "New contents for 'iota'.\n")
+    after_rev = self.check_commit_txn(txn, "/iota")
+    fs.abort_txn(txn)
+
+    return
+
+
 def suite():
     return unittest.defaultTestLoader.loadTestsFromTestCase(
       SubversionFSTestCase)

Modified: subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/mergeinfo.py
URL: http://svn.apache.org/viewvc/subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/mergeinfo.py?rev=1897034&r1=1897033&r2=1897034&view=diff
==============================================================================
--- subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/mergeinfo.py (original)
+++ subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/mergeinfo.py Fri Jan 14 14:01:45 2022
@@ -34,7 +34,7 @@ def get_svn_merge_range_t_objects():
      garbage collector, used for detecting memory leaks."""
   return [
     o for o in gc.get_objects()
-      if hasattr(o, '__class__') and
+      if getattr(o, '__class__', None) is not None and
         o.__class__.__name__ == 'svn_merge_range_t'
   ]
 
@@ -42,11 +42,11 @@ class SubversionMergeinfoTestCase(unitte
   """Test cases for mergeinfo"""
 
   # Some textual mergeinfo.
-  TEXT_MERGEINFO1 = "/trunk:3-9,27,42*"
-  TEXT_MERGEINFO2 = "/trunk:27-29,41-43*"
+  TEXT_MERGEINFO1 = b"/trunk:3-9,27,42*"
+  TEXT_MERGEINFO2 = b"/trunk:27-29,41-43*"
 
   # Meta data used in conjunction with this mergeinfo.
-  MERGEINFO_SRC = "/trunk"
+  MERGEINFO_SRC = b"/trunk"
   MERGEINFO_NBR_REV_RANGES = 3
 
   def setUp(self):
@@ -93,9 +93,9 @@ class SubversionMergeinfoTestCase(unitte
     reversed_rl = core.svn_rangelist_reverse(rangelist)
     expected_ranges = ((42, 41), (27, 26), (9, 2))
     for i in range(0, len(reversed_rl)):
-      self.assertEquals(reversed_rl[i].start, expected_ranges[i][0],
+      self.assertEqual(reversed_rl[i].start, expected_ranges[i][0],
                         "Unexpected range start: %d" % reversed_rl[i].start)
-      self.assertEquals(reversed_rl[i].end, expected_ranges[i][1],
+      self.assertEqual(reversed_rl[i].end, expected_ranges[i][1],
                         "Unexpected range end: %d" % reversed_rl[i].end)
 
   def test_mergeinfo_sort(self):
@@ -113,15 +113,15 @@ class SubversionMergeinfoTestCase(unitte
                                 self.MERGEINFO_NBR_REV_RANGES)
 
   def test_mergeinfo_get(self):
-    mergeinfo = repos.fs_get_mergeinfo(self.repos, ['/trunk'], self.rev,
+    mergeinfo = repos.fs_get_mergeinfo(self.repos, [b'/trunk'], self.rev,
                                        core.svn_mergeinfo_inherited,
                                        False, None, None)
     expected_mergeinfo = \
-      { '/trunk' :
-          { '/branches/a' : [RevRange(2, 11)],
-            '/branches/b' : [RevRange(9, 13)],
-            '/branches/c' : [RevRange(2, 16)],
-            '/trunk'      : [RevRange(1, 9)],  },
+      { b'/trunk' :
+          { b'/branches/a' : [RevRange(2, 11)],
+            b'/branches/b' : [RevRange(9, 13)],
+            b'/branches/c' : [RevRange(2, 16)],
+            b'/trunk'      : [RevRange(1, 9)],  },
       }
     self.compare_mergeinfo_catalogs(mergeinfo, expected_mergeinfo)
 
@@ -131,7 +131,7 @@ class SubversionMergeinfoTestCase(unitte
     # When reference counting is working properly, each svn_merge_range_t in
     # the returned mergeinfo will have a ref count of 1...
     mergeinfo = core.svn_mergeinfo_parse(self.TEXT_MERGEINFO1)
-    for (path, rangelist) in mergeinfo.items():
+    for (path, rangelist) in core._as_list(mergeinfo.items()):
       # ....and now 2 (incref during iteration of rangelist)
 
       for (i, r) in enumerate(rangelist):
@@ -144,7 +144,7 @@ class SubversionMergeinfoTestCase(unitte
         # Note: if path and index are not '/trunk' and 0 respectively, then
         # only some of the range objects are leaking, which is, as far as
         # leaks go, even more impressive.
-        self.assertEquals(refcount, expected, (
+        self.assertEqual(refcount, expected, (
           "Memory leak!  Expected a ref count of %d for svn_merge_range_t "
           "object, but got %d instead (path: %s, index: %d).  Probable "
           "cause: incorrect Py_INCREF/Py_DECREF usage in libsvn_swig_py/"
@@ -165,7 +165,7 @@ class SubversionMergeinfoTestCase(unitte
     del mergeinfo
     gc.collect()
     lingering = get_svn_merge_range_t_objects()
-    self.assertEquals(lingering, list(), (
+    self.assertEqual(lingering, list(), (
       "Memory leak!  Found lingering svn_merge_range_t objects left over from "
       "our call to svn_mergeinfo_parse(), even though we explicitly deleted "
       "the returned mergeinfo object.  Probable cause: incorrect Py_INCREF/"
@@ -177,16 +177,16 @@ class SubversionMergeinfoTestCase(unitte
     self.inspect_rangelist_tuple(rangelist, nbr_rev_ranges)
 
   def inspect_rangelist_tuple(self, rangelist, nbr_rev_ranges):
-    self.assert_(rangelist is not None,
+    self.assertTrue(rangelist is not None,
                  "Rangelist for '%s' not parsed" % self.MERGEINFO_SRC)
-    self.assertEquals(len(rangelist), nbr_rev_ranges,
+    self.assertEqual(len(rangelist), nbr_rev_ranges,
                       "Wrong number of revision ranges parsed")
-    self.assertEquals(rangelist[0].inheritable, True,
+    self.assertEqual(rangelist[0].inheritable, True,
                       "Unexpected revision range 'non-inheritable' flag: %s" %
                       rangelist[0].inheritable)
-    self.assertEquals(rangelist[1].start, 26,
+    self.assertEqual(rangelist[1].start, 26,
                       "Unexpected revision range end: %d" % rangelist[1].start)
-    self.assertEquals(rangelist[2].inheritable, False,
+    self.assertEqual(rangelist[2].inheritable, False,
                       "Missing revision range 'non-inheritable' flag")
 
   def compare_mergeinfo_catalogs(self, catalog1, catalog2):
@@ -194,7 +194,7 @@ class SubversionMergeinfoTestCase(unitte
     keys2 = sorted(catalog2.keys())
     self.assertEqual(keys1, keys2)
 
-    for k in catalog1.keys():
+    for k in catalog1:
         self.compare_mergeinfos(catalog1[k], catalog2[k])
 
   def compare_mergeinfos(self, mergeinfo1, mergeinfo2):
@@ -202,7 +202,7 @@ class SubversionMergeinfoTestCase(unitte
     keys2 = sorted(mergeinfo2.keys())
     self.assertEqual(keys1, keys2)
 
-    for k in mergeinfo1.keys():
+    for k in mergeinfo1:
         self.compare_rangelists(mergeinfo1[k], mergeinfo2[k])
 
   def compare_rangelists(self, rangelist1, rangelist2):

Modified: subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/pool.py
URL: http://svn.apache.org/viewvc/subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/pool.py?rev=1897034&r1=1897033&r2=1897034&view=diff
==============================================================================
--- subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/pool.py (original)
+++ subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/pool.py Fri Jan 14 14:01:45 2022
@@ -19,10 +19,11 @@
 #
 #
 import unittest, weakref, setup_path
-import os, tempfile
+import os, tempfile, gc
 import svn.core, svn.client, libsvn.core
 from svn.core import *
 from libsvn.core import application_pool, GenericSWIGWrapper
+import utils
 
 # Test case for the new automatic pool management infrastructure
 
@@ -208,6 +209,51 @@ class PoolTestCase(unittest.TestCase):
     # We can still destroy and create pools at will
     svn_pool_destroy(svn_pool_create())
 
+  def _test_pools_in_circular_reference(self, finalizer=False):
+
+    class Circular(object):
+
+      def __init__(self, pool):
+        self.pool = pool
+        self.loop = None
+
+      if finalizer:
+        def __del__(self):
+          self.pool = self.loop = None
+
+    def create_circularl():
+      pool = Pool(libsvn.core.application_pool)
+      subpool1 = Pool(pool)
+      subpool2 = Pool(pool)
+      circularly1 = Circular(pool)
+      circularly2 = Circular(subpool2)
+      circularly3 = Circular(subpool1)
+      circularly1.loop = circularly3
+      circularly2.loop = circularly1
+      circularly3.loop = circularly2
+      refs = weakref.WeakValueDictionary()
+      refs['pool'] = pool
+      refs['subpool1'] = subpool1
+      refs['subpool2'] = subpool2
+      return refs
+
+    refs = create_circularl()
+    self.assertEqual({'pool', 'subpool1', 'subpool2'},
+                     set(name for name, pool in refs.items()
+                              if pool is not None))
+    gc.collect()
+    self.assertEqual(set(), set(name for name, pool in refs.items()
+                                     if pool is not None))
+
+  def test_pools_in_circular_reference_without_finalizer(self):
+    self._test_pools_in_circular_reference(finalizer=False)
+
+  @unittest.skipIf(not utils.IS_PY3,
+                   "Python 2 cannot collect garbage which involves circular "
+                   "references with finalizer")
+  def test_pools_in_circular_reference_with_finalizer(self):
+    self._test_pools_in_circular_reference(finalizer=True)
+
 def suite():
   return unittest.defaultTestLoader.loadTestsFromTestCase(PoolTestCase)
 

Modified: subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/ra.py
URL: http://svn.apache.org/viewvc/subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/ra.py?rev=1897034&r1=1897033&r2=1897034&view=diff
==============================================================================
--- subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/ra.py (original)
+++ subversion/branches/multi-wc-format/subversion/bindings/swig/python/tests/ra.py Fri Jan 14 14:01:45 2022
@@ -22,12 +22,7 @@ import unittest, setup_path
 
 from svn import core, repos, fs, delta, ra
 from sys import version_info # For Python version check
-if version_info[0] >= 3:
-  # Python >=3.0
-  from io import StringIO
-else:
-  # Python <3.0
-  from StringIO import StringIO
+from io import BytesIO
 
 import utils
 
@@ -58,16 +53,16 @@ class SubversionRepositoryAccessTestCase
   def test_get_file(self):
     # Test getting the properties of a file
     fs_revnum = fs.youngest_rev(self.fs)
-    rev, properties = ra.get_file(self.ra_ctx, "trunk/README2.txt",
+    rev, properties = ra.get_file(self.ra_ctx, b"trunk/README2.txt",
                                   core.SVN_INVALID_REVNUM, None)
     self.assertEqual(rev, fs_revnum)
-    self.assertEqual(properties["svn:mime-type"], "text/plain")
+    self.assertEqual(properties[b"svn:mime-type"], b"text/plain")
 
     # Test getting the contents of a file
-    filestream = StringIO()
-    rev, properties = ra.get_file(self.ra_ctx, "trunk/README2.txt",
+    filestream = BytesIO()
+    rev, properties = ra.get_file(self.ra_ctx, b"trunk/README2.txt",
                                   fs_revnum, filestream)
-    self.assertEqual("A test.\n", filestream.getvalue())
+    self.assertEqual(b"A test.\n", filestream.getvalue())
 
   def test_get_repos_root(self):
     root = ra.get_repos_root(self.ra_ctx)
@@ -84,53 +79,53 @@ class SubversionRepositoryAccessTestCase
     self.assertEqual(ra_revnum, fs_revnum)
 
   def test_get_dir2(self):
-    (dirents, _, props) = ra.get_dir2(self.ra_ctx, '', 1, core.SVN_DIRENT_KIND)
-    self.assert_('trunk' in dirents)
-    self.assert_('branches' in dirents)
-    self.assert_('tags' in dirents)
-    self.assertEqual(dirents['trunk'].kind, core.svn_node_dir)
-    self.assertEqual(dirents['branches'].kind, core.svn_node_dir)
-    self.assertEqual(dirents['tags'].kind, core.svn_node_dir)
-    self.assert_(core.SVN_PROP_ENTRY_UUID in props)
-    self.assert_(core.SVN_PROP_ENTRY_LAST_AUTHOR in props)
+    (dirents, _, props) = ra.get_dir2(self.ra_ctx, b'', 1, core.SVN_DIRENT_KIND)
+    self.assertTrue(b'trunk' in dirents)
+    self.assertTrue(b'branches' in dirents)
+    self.assertTrue(b'tags' in dirents)
+    self.assertEqual(dirents[b'trunk'].kind, core.svn_node_dir)
+    self.assertEqual(dirents[b'branches'].kind, core.svn_node_dir)
+    self.assertEqual(dirents[b'tags'].kind, core.svn_node_dir)
+    self.assertTrue(core.SVN_PROP_ENTRY_UUID in props)
+    self.assertTrue(core.SVN_PROP_ENTRY_LAST_AUTHOR in props)
 
-    (dirents, _, _) = ra.get_dir2(self.ra_ctx, 'trunk', 1, core.SVN_DIRENT_KIND)
+    (dirents, _, _) = ra.get_dir2(self.ra_ctx, b'trunk', 1, core.SVN_DIRENT_KIND)
 
     self.assertEqual(dirents, {})
 
-    (dirents, _, _) = ra.get_dir2(self.ra_ctx, 'trunk', 10,
+    (dirents, _, _) = ra.get_dir2(self.ra_ctx, b'trunk', 10,
                                   core.SVN_DIRENT_KIND)
 
-    self.assert_('README2.txt' in dirents)
-    self.assertEqual(dirents['README2.txt'].kind, core.svn_node_file)
+    self.assertTrue(b'README2.txt' in dirents)
+    self.assertEqual(dirents[b'README2.txt'].kind, core.svn_node_file)
 
   def test_commit3(self):
     commit_info = []
     def my_callback(info, pool):
       commit_info.append(info)
 
-    revprops = {"svn:log": "foobar", "testprop": ""}
+    revprops = {b"svn:log": b"foobar", b"testprop": b""}
     editor, edit_baton = ra.get_commit_editor3(self.ra_ctx, revprops, my_callback, None, False)
     root = editor.open_root(edit_baton, 4)
     self.assertNotEqual(root, None)
-    child = editor.add_directory("bla3", root, None, 0)
+    child = editor.add_directory(b"bla3", root, None, 0)
     self.assertNotEqual(child, None)
     editor.close_edit(edit_baton)
 
     info = commit_info[0]
     self.assertEqual(info.revision, fs.youngest_rev(self.fs))
-    revprops['svn:author'] = info.author
-    revprops['svn:date'] = info.date
+    revprops[b'svn:author'] = info.author
+    revprops[b'svn:date'] = info.date
     self.assertEqual(ra.rev_proplist(self.ra_ctx, info.revision), revprops)
 
   def test_commit2(self):
     def my_callback(info, pool):
         self.assertEqual(info.revision, fs.youngest_rev(self.fs))
 
-    editor, edit_baton = ra.get_commit_editor2(self.ra_ctx, "foobar", my_callback, None, False)
+    editor, edit_baton = ra.get_commit_editor2(self.ra_ctx, b"foobar", my_callback, None, False)
     root = editor.open_root(edit_baton, 4)
     self.assertNotEqual(root, None)
-    child = editor.add_directory("bla", root, None, 0)
+    child = editor.add_directory(b"bla", root, None, 0)
     self.assertNotEqual(child, None)
     editor.close_edit(edit_baton)
 
@@ -138,18 +133,18 @@ class SubversionRepositoryAccessTestCase
     def my_callback(revision, date, author):
         self.assertEqual(revision, fs.youngest_rev(self.fs))
 
-    editor, edit_baton = ra.get_commit_editor(self.ra_ctx, "foobar", my_callback, None, False)
+    editor, edit_baton = ra.get_commit_editor(self.ra_ctx, b"foobar", my_callback, None, False)
     root = editor.open_root(edit_baton, 4)
-    child = editor.add_directory("blah", root, None, 0)
+    child = editor.add_directory(b"blah", root, None, 0)
     editor.close_edit(edit_baton)
 
   def test_delta_driver_commit(self):
     # Setup paths we'll commit in this test.
-    to_delete = ['trunk/README.txt', 'trunk/dir1/dir2']
-    to_mkdir = ['test_delta_driver_commit.d', 'test_delta_driver_commit2.d']
-    to_add = ['test_delta_driver_commit', 'test_delta_driver_commit2']
-    to_dir_prop = ['trunk/dir1/dir3', 'test_delta_driver_commit2.d']
-    to_file_prop = ['trunk/README2.txt', 'test_delta_driver_commit2']
+    to_delete = [b'trunk/README.txt', b'trunk/dir1/dir2']
+    to_mkdir = [b'test_delta_driver_commit.d', b'test_delta_driver_commit2.d']
+    to_add = [b'test_delta_driver_commit', b'test_delta_driver_commit2']
+    to_dir_prop = [b'trunk/dir1/dir3', b'test_delta_driver_commit2.d']
+    to_file_prop = [b'trunk/README2.txt', b'test_delta_driver_commit2']
     all_paths = {}
     for i in to_delete + to_mkdir + to_add + to_dir_prop + to_file_prop:
       all_paths[i] = True
@@ -159,12 +154,12 @@ class SubversionRepositoryAccessTestCase
     commit_info = []
     def commit_cb(info, pool):
       commit_info.append(info)
-    revprops = {"svn:log": "foobar", "testprop": ""}
+    revprops = {b"svn:log": b"foobar", b"testprop": b""}
     (editor, edit_baton) = ra.get_commit_editor3(self.ra_ctx, revprops,
                                                  commit_cb, None, False)
     try:
       def driver_cb(parent, path, pool):
-        self.assert_(path in all_paths)
+        self.assertTrue(path in all_paths)
         dir_baton = file_baton = None
         if path in to_delete:
           # Leave dir_baton alone, as it must be None for delete.
@@ -182,16 +177,16 @@ class SubversionRepositoryAccessTestCase
           if dir_baton is None:
             dir_baton = editor.open_directory(path, parent, revision, pool)
           editor.change_dir_prop(dir_baton,
-                                 'test_delta_driver_commit', 'foo', pool)
+                                 b'test_delta_driver_commit', b'foo', pool)
         elif path in to_file_prop:
           if file_baton is None:
             file_baton = editor.open_file(path, parent, revision, pool)
           editor.change_file_prop(file_baton,
-                                  'test_delta_driver_commit', 'foo', pool)
+                                  b'test_delta_driver_commit', b'foo', pool)
         if file_baton is not None:
           editor.close_file(file_baton, None, pool)
         return dir_baton
-      delta.path_driver(editor, edit_baton, -1, list(all_paths.keys()), driver_cb)
+      delta.path_driver(editor, edit_baton, -1, core._as_list(all_paths.keys()), driver_cb)
       editor.close_edit(edit_baton)
     except:
       try:
@@ -204,8 +199,8 @@ class SubversionRepositoryAccessTestCase
     info = commit_info[0]
 
     if info.author is not None:
-      revprops['svn:author'] = info.author
-    revprops['svn:date'] = info.date
+      revprops[b'svn:author'] = info.author
+    revprops[b'svn:date'] = info.date
     self.assertEqual(ra.rev_proplist(self.ra_ctx, info.revision), revprops)
 
     receiver_called = [False]
@@ -214,22 +209,22 @@ class SubversionRepositoryAccessTestCase
       self.assertEqual(revision, info.revision)
       self.assertEqual(author, info.author)
       self.assertEqual(date, info.date)
-      self.assertEqual(message, revprops['svn:log'])
-      for (path, change) in changed_paths.items():
-        path = path.lstrip('/')
-        self.assert_(path in all_paths)
+      self.assertEqual(message, revprops[b'svn:log'])
+      for (path, change) in core._as_list(changed_paths.items()):
+        path = path.lstrip(b'/')
+        self.assertTrue(path in all_paths)
         if path in to_delete:
-          self.assertEqual(change.action, 'D')
+          self.assertEqual(change.action, b'D')
         elif path in to_mkdir or path in to_add:
-          self.assertEqual(change.action, 'A')
+          self.assertEqual(change.action, b'A')
         elif path in to_dir_prop or path in to_file_prop:
-          self.assertEqual(change.action, 'M')
-    ra.get_log(self.ra_ctx, [''], info.revision, info.revision,
+          self.assertEqual(change.action, b'M')
+    ra.get_log(self.ra_ctx, [b''], info.revision, info.revision,
                0,                       # limit
                True,                    # discover_changed_paths
                True,                    # strict_node_history
                receiver)
-    self.assert_(receiver_called[0])
+    self.assertTrue(receiver_called[0])
 
   def test_do_diff2(self):
 
@@ -251,26 +246,26 @@ class SubversionRepositoryAccessTestCase
 
     sess_url = ra.get_session_url(self.ra_ctx)
     try:
-        ra.reparent(self.ra_ctx, self.repos_uri+"/trunk")
+        ra.reparent(self.ra_ctx, self.repos_uri+b"/trunk")
         reporter, reporter_baton = ra.do_diff2(self.ra_ctx, fs_revnum,
-                                               "README.txt", 0, 0, 1,
+                                               b"README.txt", 0, 0, 1,
                                                self.repos_uri
-                                                 +"/trunk/README.txt",
+                                                 +b"/trunk/README.txt",
                                                e_ptr, e_baton)
-        reporter.set_path(reporter_baton, "", 0, True, None)
+        reporter.set_path(reporter_baton, b"", 0, True, None)
         reporter.finish_report(reporter_baton)
     finally:
         ra.reparent(self.ra_ctx, sess_url)
 
-    self.assertEqual("A test.\n", editor.textdeltas[0].new_data)
+    self.assertEqual(b"A test.\n", editor.textdeltas[0].new_data)
     self.assertEqual(1, len(editor.textdeltas))
 
   def test_get_locations(self):
-    locations = ra.get_locations(self.ra_ctx, "trunk/README.txt", 2, list(range(1, 5)))
+    locations = ra.get_locations(self.ra_ctx, b"trunk/README.txt", 2, list(range(1, 5)))
     self.assertEqual(locations, {
-        2: '/trunk/README.txt',
-        3: '/trunk/README.txt',
-        4: '/trunk/README.txt'})
+        2: b'/trunk/README.txt',
+        3: b'/trunk/README.txt',
+        4: b'/trunk/README.txt'})
 
   def test_has_capability(self):
       self.assertEqual(True, ra.has_capability(self.ra_ctx,
@@ -278,24 +273,24 @@ class SubversionRepositoryAccessTestCase
 
   def test_get_file_revs(self):
     def rev_handler(path, rev, rev_props, prop_diffs, pool):
-        self.assert_(rev == 2 or rev == 3)
-        self.assertEqual(path, "/trunk/README.txt")
+        self.assertTrue(rev == 2 or rev == 3)
+        self.assertEqual(path, b"/trunk/README.txt")
         if rev == 2:
             self.assertEqual(rev_props, {
-              'svn:log': 'Added README.',
-              'svn:author': 'john',
-              'svn:date': '2005-04-01T13:12:18.216267Z'
+              b'svn:log': b'Added README.',
+              b'svn:author': b'john',
+              b'svn:date': b'2005-04-01T13:12:18.216267Z'
             })
             self.assertEqual(prop_diffs, {})
         elif rev == 3:
             self.assertEqual(rev_props, {
-              'svn:log': 'Fixed README.\n',
-              'svn:author': 'kate',
-              'svn:date': '2005-04-01T13:24:58.234643Z'
+              b'svn:log': b'Fixed README.\n',
+              b'svn:author': b'kate',
+              b'svn:date': b'2005-04-01T13:24:58.234643Z'
             })
-            self.assertEqual(prop_diffs, {'svn:mime-type': 'text/plain', 'svn:eol-style': 'native'})
+            self.assertEqual(prop_diffs, {b'svn:mime-type': b'text/plain', b'svn:eol-style': b'native'})
 
-    ra.get_file_revs(self.ra_ctx, "trunk/README.txt", 0, 10, rev_handler)
+    ra.get_file_revs(self.ra_ctx, b"trunk/README.txt", 0, 10, rev_handler)
 
   def test_lock(self):
 
@@ -304,12 +299,12 @@ class SubversionRepositoryAccessTestCase
     self.errors = 0
     def callback(path, do_lock, lock, ra_err, pool):
       self.calls += 1
-      self.assertEqual(path, "trunk/README2.txt")
+      self.assertEqual(path, b"trunk/README2.txt")
       if lock:
-        self.assertEqual(lock.owner, "jrandom")
+        self.assertEqual(lock.owner, b"jrandom")
         self.locks += 1
       if ra_err:
-        self.assert_(ra_err.apr_err == core.SVN_ERR_FS_PATH_ALREADY_LOCKED
+        self.assertTrue(ra_err.apr_err == core.SVN_ERR_FS_PATH_ALREADY_LOCKED
                      or ra_err.apr_err == core.SVN_ERR_FS_NO_SUCH_LOCK)
         self.errors += 1
 
@@ -317,50 +312,50 @@ class SubversionRepositoryAccessTestCase
     self.callbacks.auth_baton = core.svn_auth_open(providers)
     core.svn_auth_set_parameter(self.callbacks.auth_baton,
                                 core.SVN_AUTH_PARAM_DEFAULT_USERNAME,
-                                "jrandom")
+                                b"jrandom")
     self.ra_ctx = ra.open2(self.repos_uri, self.callbacks, {})
     rev = fs.youngest_rev(self.fs)
-    ra.lock(self.ra_ctx, {"trunk/README2.txt":rev}, "sleutel", False, callback)
+    ra.lock(self.ra_ctx, {b"trunk/README2.txt":rev}, b"sleutel", False, callback)
     self.assertEqual(self.calls, 1)
     self.assertEqual(self.locks, 1)
     self.assertEqual(self.errors, 0)
 
     self.calls = 0
     self.locks = 0
-    ra.lock(self.ra_ctx, {"trunk/README2.txt":rev}, "sleutel", False, callback)
+    ra.lock(self.ra_ctx, {b"trunk/README2.txt":rev}, b"sleutel", False, callback)
     self.assertEqual(self.calls, 1)
     self.assertEqual(self.locks, 0)
     self.assertEqual(self.errors, 1)
 
     self.calls = 0
     self.errors = 0
-    the_lock = fs.get_lock(self.fs, "/trunk/README2.txt")
-    ra.unlock(self.ra_ctx, {"trunk/README2.txt":the_lock.token}, False, callback)
+    the_lock = fs.get_lock(self.fs, b"/trunk/README2.txt")
+    ra.unlock(self.ra_ctx, {b"trunk/README2.txt":the_lock.token}, False, callback)
     self.assertEqual(self.calls, 1)
     self.assertEqual(self.locks, 0)
     self.assertEqual(self.errors, 0)
 
     self.calls = 0
-    ra.unlock(self.ra_ctx, {"trunk/README2.txt":the_lock.token}, False, callback)
+    ra.unlock(self.ra_ctx, {b"trunk/README2.txt":the_lock.token}, False, callback)
     self.assertEqual(self.calls, 1)
     self.assertEqual(self.locks, 0)
     self.assertEqual(self.errors, 1)
 
   def test_get_log2(self):
-    # Get an interesting commmit.
+    # Get an interesting commit.
     self.test_commit3()
     rev = fs.youngest_rev(self.fs)
     revprops = ra.rev_proplist(self.ra_ctx, rev)
-    self.assert_("svn:log" in revprops)
-    self.assert_("testprop" in revprops)
+    self.assertTrue(b"svn:log" in revprops)
+    self.assertTrue(b"testprop" in revprops)
 
     def receiver(log_entry, pool):
       called[0] = True
       self.assertEqual(log_entry.revision, rev)
       if discover_changed_paths:
-        self.assertEqual(list(log_entry.changed_paths.keys()), ['/bla3'])
-        changed_path = log_entry.changed_paths['/bla3']
-        self.assert_(changed_path.action in ['A', 'D', 'R', 'M'])
+        self.assertEqual(core._as_list(log_entry.changed_paths.keys()), [b'/bla3'])
+        changed_path = log_entry.changed_paths[b'/bla3']
+        self.assertTrue(changed_path.action in [b'A', b'D', b'R', b'M'])
         self.assertEqual(changed_path.copyfrom_path, None)
         self.assertEqual(changed_path.copyfrom_rev, -1)
       else:
@@ -368,7 +363,7 @@ class SubversionRepositoryAccessTestCase
       if log_revprops is None:
         self.assertEqual(log_entry.revprops, revprops)
       elif len(log_revprops) == 0:
-        self.assert_(log_entry.revprops == None or len(log_entry.revprops) == 0)
+        self.assertTrue(log_entry.revprops == None or len(log_entry.revprops) == 0)
       else:
         revprop_names = sorted(log_entry.revprops.keys())
         log_revprops.sort()
@@ -381,9 +376,9 @@ class SubversionRepositoryAccessTestCase
 
     for log_revprops in (
       # Retrieve the standard three.
-      ["svn:author", "svn:date", "svn:log"],
+      [b"svn:author", b"svn:date", b"svn:log"],
       # Retrieve just testprop.
-      ["testprop"],
+      [b"testprop"],
       # Retrieve all.
       None,
       # Retrieve none.
@@ -391,14 +386,14 @@ class SubversionRepositoryAccessTestCase
       ):
       for discover_changed_paths in [True, False]:
         called = [False]
-        ra.get_log2(self.ra_ctx, [""],
+        ra.get_log2(self.ra_ctx, [b""],
                     rev, rev,   # start, end
                     1,          # limit
                     discover_changed_paths,
                     True,       # strict_node_history
                     False,      # include_merged_revisions
                     log_revprops, receiver)
-        self.assert_(called[0])
+        self.assertTrue(called[0])
 
   def test_update(self):
     class TestEditor(delta.Editor):
@@ -408,23 +403,23 @@ class SubversionRepositoryAccessTestCase
 
     e_ptr, e_baton = delta.make_editor(editor)
 
-    reporter, reporter_baton = ra.do_update(self.ra_ctx, 10, "", True, e_ptr, e_baton)
+    reporter, reporter_baton = ra.do_update(self.ra_ctx, 10, b"", True, e_ptr, e_baton)
 
-    reporter.set_path(reporter_baton, "", 0, True, None)
+    reporter.set_path(reporter_baton, b"", 0, True, None)
 
     reporter.finish_report(reporter_baton)
 
   def test_namestring(self):
     # Only ra-{svn,serf} support this right now.
     uri = self.repos_uri
-    if uri.startswith('http') or uri.startswith('svn'):
+    if uri.startswith(b'http') or uri.startswith(b'svn'):
       called = [False]
       def cb(pool):
         called[0] = True
-        return 'namestring_test'
+        return b'namestring_test'
       self.callbacks.get_client_string = cb
-      ra.stat(self.ra_ctx, "", 1)
-      self.assert_(called[0])
+      ra.stat(self.ra_ctx, b"", 1)
+      self.assertTrue(called[0])
 
 def suite():
     return unittest.defaultTestLoader.loadTestsFromTestCase(