On 2020/12/28 19:10, Daniel Shahaf wrote: > Yasuhito FUTATSUKI wrote on Sat, 26 Dec 2020 11:02 +00:00: >> On 2020/12/26 3:38, Nathan Hartman wrote: >> >>> But the solution you suggest above, adding a new_rev attribute to the >>> exception and leaving the return value as it was, sounds like a good way to >>> avoid the incompatibility and also avoid the API bump. >> >> I had written such a patch on typemaps at first because it was straight >> forward. Although I removed it after r1880967 is commited, I can write >> such a code again and I'll do if it is needed. >> >> Alternately, here is an easier way to do it. The patch below is a code >> that replace wrapper function entity on top of the svn.fs module >> (not tested, and more appropriate comment is needed in the code).
<snip> >> Of course, we can use svn_fs_commit_txn2 instead of _svn_fs_commit_txn >> in above patch, then it will satisfy the Daniel's proposal. > > I agree with Nathan that it'd be confusing for a Python > svn_fs_commit_txn2() to exist when a C function of the same name does > not, even if the C API skips a revv number in consideration of swig-py. > So, if we'd like to introduce a simplified API, I'd vote to name it > something else. Then I'll commit patch amended only for restore Python 2 support, without renaming to svn_fs_commit_txn2, if Mike agrees because he already wrote some code using svn.fs.commit_txn(). (attached swig-py-fs_commit_txn-revert-return-value-type-patch.txt) I also wrote tests for svn.fs.commit_txn, both for return value and attributes in SubversionException, by porting tests for C API into Python. (attached patch swig-py-fs_commit_txn-test-patch.txt; tested on Python 2.7.18 and 3.9.0 on FreeBSD 12) During writing the test above I also found that on swig-py for Python 3 wrapper functions using svn_string_t * or svn_stringbuf_t * for in input arguments don't accept str objects for their values. This was my overlook, so I'll fix it. However, this is another topic. Cheers, -- Yasuhito FUTATSUKI <futat...@yf.bsclub.org>
Follow up to r1880967: swig-py: Restore return value type of svn.fs.commit_txn On r1880967 we reverted a "wrong" behavior of svn.fs.commit_txn, that it returned always a 2-tuple of None and new_rev if it didn't raise Exception. However we reconsidered that it is more important to keep compatibility on existing program codes. To keep code simple, we adjust return value on Python code rather than SWIG typemap. * subversion/bindings/swig/python/svn/fs.py (): rename the wrapper function svn_fs_commit_txn imported from libsvn.fs module into internal name '_svn_fs_commit_txn'. (svn_fs_commit_txn): New function to wrap _svn_fs_commit_txn to adjust return value type. Index: subversion/bindings/swig/python/svn/fs.py =================================================================== --- subversion/bindings/swig/python/svn/fs.py (revision 1884802) +++ subversion/bindings/swig/python/svn/fs.py (working copy) @@ -24,6 +24,14 @@ ###################################################################### from libsvn.fs import * + +# For API compatibility we should replace wrapper function entity before +# adding alternative names. +_svn_fs_commit_txn = svn_fs_commit_txn +def svn_fs_commit_txn(*args): + r"""svn_fs_commit_txn(svn_fs_txn_t txn, apr_pool_t pool) -> svn_error_t""" + return None, _svn_fs_commit_txn(*args) + from svn.core import _unprefix_names, Pool, _as_list _unprefix_names(locals(), 'svn_fs_') _unprefix_names(locals(), 'SVN_FS_')
Follow up to r1880967: swig-py: Add test for svn.fs.commit_txn * subversion/bindings/swig/python/svn/tests/fs.py (): import os.path and svn.delta (SubversionTestTreeEntry): New class. A representation of svn_test__tree_entry_t in subversion/tests/svn_test_fs.h. (svn_test__stream_to_string, svn_test__set_file_contents, svn_test__get_file_contents, _get_dir_entries, _validate_tree_entry, svn_test__validate_tree, svn_test__check_greek_tree, svn_test__create_greek_tree_at, svn_test__create_greek_tree): New functions. Python imprementation of test helper functions in subversion/tests/svn_test_fs.c. (SubversionFSTestCase.check_commit_txn): New helper function. A Python imprementation of test_commit_txn() in subversion/tests/libsvn_fs/fs-test.c. (SubversionFSTestCase.test_basic_commit, SubversionFSTestCase.test_merging_commit): New test cases. Python imprementation of basic_commit() and test_merging_commit() in subversion/tests/libsvn_fs/fs-test.c. Index: subversion/bindings/swig/python/svn/fs.py =================================================================== --- subversion/bindings/swig/python/svn/fs.py (revision 1884802) +++ subversion/bindings/swig/python/svn/fs.py (working copy) @@ -24,6 +24,14 @@ ###################################################################### from libsvn.fs import * + +# For API compatibility we should replace wrapper function entity before +# adding alternative names. +_svn_fs_commit_txn = svn_fs_commit_txn +def svn_fs_commit_txn(*args): + r"""svn_fs_commit_txn(svn_fs_txn_t txn, apr_pool_t pool) -> svn_error_t""" + return None, _svn_fs_commit_txn(*args) + from svn.core import _unprefix_names, Pool, _as_list _unprefix_names(locals(), 'svn_fs_') _unprefix_names(locals(), 'SVN_FS_') Index: subversion/bindings/swig/python/tests/fs.py =================================================================== --- subversion/bindings/swig/python/tests/fs.py (revision 1884802) +++ subversion/bindings/swig/python/tests/fs.py (working copy) @@ -20,6 +20,7 @@ # # import os, unittest, sys, errno +import os.path from tempfile import mkstemp from subprocess import Popen, PIPE try: @@ -29,9 +30,212 @@ # 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) + name = dirent.name + if not isinstance(name, str): + name = name.decode('utf-8') + full_path = os.path.join(path, 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""" @@ -121,6 +325,982 @@ 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 + if hasattr(e, 'conflict_p'): + conflict = e.conflict_p + if isinstance(conflict, bytes) and not isinstance(conflict, str): + conflict = conflict.decode('utf-8') + if 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)