diff options
Diffstat (limited to 'subversion/tests/libsvn_fs/fs-test.c')
-rw-r--r-- | subversion/tests/libsvn_fs/fs-test.c | 2368 |
1 files changed, 2284 insertions, 84 deletions
diff --git a/subversion/tests/libsvn_fs/fs-test.c b/subversion/tests/libsvn_fs/fs-test.c index 104a692..fcab5e4 100644 --- a/subversion/tests/libsvn_fs/fs-test.c +++ b/subversion/tests/libsvn_fs/fs-test.c @@ -23,6 +23,7 @@ #include <stdlib.h> #include <string.h> #include <apr_pools.h> +#include <apr_thread_proc.h> #include <assert.h> #include "../svn_test.h" @@ -37,11 +38,16 @@ #include "svn_props.h" #include "svn_version.h" +#include "svn_private_config.h" +#include "private/svn_fs_util.h" #include "private/svn_fs_private.h" +#include "private/svn_fspath.h" +#include "private/svn_sqlite.h" #include "../svn_test_fs.h" #include "../../libsvn_delta/delta.h" +#include "../../libsvn_fs/fs-loader.h" #define SET_STR(ps, s) ((ps)->data = (s), (ps)->len = strlen(s)) @@ -203,24 +209,35 @@ reopen_trivial_transaction(const svn_test_opts_t *opts, { svn_fs_t *fs; svn_fs_txn_t *txn; + svn_fs_root_t *root; const char *txn_name; apr_pool_t *subpool = svn_pool_create(pool); SVN_ERR(svn_test__create_fs(&fs, "test-repo-reopen-trivial-txn", opts, pool)); - /* Begin a new transaction that is based on revision 0. */ + /* Create a first transaction - we don't want that one to reopen. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, subpool)); + + /* Begin a second transaction that is based on revision 0. */ SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, subpool)); /* Don't use the subpool, txn_name must persist beyond the current txn */ SVN_ERR(svn_fs_txn_name(&txn_name, txn, pool)); + /* Create a third transaction - we don't want that one to reopen. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, subpool)); + /* Close the transaction. */ svn_pool_clear(subpool); /* Reopen the transaction by name */ SVN_ERR(svn_fs_open_txn(&txn, fs, txn_name, subpool)); + /* Does it have the same name? */ + SVN_ERR(svn_fs_txn_root(&root, txn, subpool)); + SVN_TEST_STRING_ASSERT(svn_fs_txn_root_name(root, subpool), txn_name); + /* Close the transaction ... again. */ svn_pool_destroy(subpool); @@ -1019,7 +1036,7 @@ static svn_error_t * check_entry_present(svn_fs_root_t *root, const char *path, const char *name, apr_pool_t *pool) { - svn_boolean_t present; + svn_boolean_t present = FALSE; SVN_ERR(check_entry(root, path, name, &present, pool)); if (! present) @@ -1036,7 +1053,7 @@ static svn_error_t * check_entry_absent(svn_fs_root_t *root, const char *path, const char *name, apr_pool_t *pool) { - svn_boolean_t present; + svn_boolean_t present = TRUE; SVN_ERR(check_entry(root, path, name, &present, pool)); if (present) @@ -1122,6 +1139,8 @@ basic_commit(const svn_test_opts_t *opts, /* Create the greek tree. */ SVN_ERR(svn_test__create_greek_tree(txn_root, pool)); + SVN_TEST_ASSERT(svn_fs_is_txn_root(txn_root)); + SVN_TEST_ASSERT(!svn_fs_is_revision_root(txn_root)); /* Commit it. */ SVN_ERR(svn_fs_commit_txn(&conflict, &after_rev, txn, pool)); @@ -1135,6 +1154,8 @@ basic_commit(const svn_test_opts_t *opts, /* Get root of the revision */ SVN_ERR(svn_fs_revision_root(&revision_root, fs, after_rev, pool)); + SVN_TEST_ASSERT(!svn_fs_is_txn_root(revision_root)); + SVN_TEST_ASSERT(svn_fs_is_revision_root(revision_root)); /* Check the tree. */ SVN_ERR(svn_test__check_greek_tree(revision_root, pool)); @@ -1576,6 +1597,10 @@ merging_commit(const svn_test_opts_t *opts, SVN_ERR(svn_fs_make_file(txn_root, "theta", pool)); SVN_ERR(svn_test__set_file_contents (txn_root, "theta", "This is another file 'theta'.\n", pool)); + + /* TXN must actually be based upon revisions[4] (instead of HEAD). */ + SVN_TEST_ASSERT(svn_fs_txn_base_revision(txn) == revisions[4]); + SVN_ERR(test_commit_txn(&failed_rev, txn, "/theta", pool)); SVN_ERR(svn_fs_abort_txn(txn, pool)); @@ -1599,6 +1624,9 @@ merging_commit(const svn_test_opts_t *opts, SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_fs_delete(txn_root, "A/D/H", pool)); + /* TXN must actually be based upon revisions[1] (instead of HEAD). */ + SVN_TEST_ASSERT(svn_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. @@ -2075,7 +2103,7 @@ copy_test(const svn_test_opts_t *opts, svn_revnum_t after_rev; /* Prepare a filesystem. */ - SVN_ERR(svn_test__create_fs(&fs, "test-repo-copy-test", + SVN_ERR(svn_test__create_fs(&fs, "test-repo-copy", opts, pool)); /* In first txn, create and commit the greek tree. */ @@ -3756,6 +3784,17 @@ small_file_integrity(const svn_test_opts_t *opts, static svn_error_t * +almostmedium_file_integrity(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + apr_uint32_t seed = (apr_uint32_t) apr_time_now(); + + return file_integrity_helper(SVN_DELTA_WINDOW_SIZE - 1, &seed, opts, + "test-repo-almostmedium-file-integrity", pool); +} + + +static svn_error_t * medium_file_integrity(const svn_test_opts_t *opts, apr_pool_t *pool) { @@ -4156,6 +4195,12 @@ check_related(const svn_test_opts_t *opts, { "E", 7 }, { "E", 8 }, { "F", 9 }, { "F", 10 } }; + /* Latest revision that touched the respective path. */ + struct path_rev_t latest_changes[6] = { + { "A", 4 }, { "B", 6 }, { "C", 6 }, + { "D", 7 }, { "E", 8 }, { "F", 10 } + }; + int related_matrix[16][16] = { /* A1 ... F10 across the top here*/ { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0 }, /* A1 */ @@ -4185,14 +4230,16 @@ check_related(const svn_test_opts_t *opts, struct path_rev_t pr2 = path_revs[j]; const svn_fs_id_t *id1, *id2; int related = 0; + svn_fs_node_relation_t relation; + svn_fs_root_t *rev_root1, *rev_root2; /* Get the ID for the first path/revision combination. */ - SVN_ERR(svn_fs_revision_root(&rev_root, fs, pr1.rev, subpool)); - SVN_ERR(svn_fs_node_id(&id1, rev_root, pr1.path, subpool)); + SVN_ERR(svn_fs_revision_root(&rev_root1, fs, pr1.rev, subpool)); + SVN_ERR(svn_fs_node_id(&id1, rev_root1, pr1.path, subpool)); /* Get the ID for the second path/revision combination. */ - SVN_ERR(svn_fs_revision_root(&rev_root, fs, pr2.rev, subpool)); - SVN_ERR(svn_fs_node_id(&id2, rev_root, pr2.path, subpool)); + SVN_ERR(svn_fs_revision_root(&rev_root2, fs, pr2.rev, subpool)); + SVN_ERR(svn_fs_node_id(&id2, rev_root2, pr2.path, subpool)); /* <exciting> Now, run the relationship check! </exciting> */ related = svn_fs_check_related(id1, id2) ? 1 : 0; @@ -4215,9 +4262,263 @@ check_related(const svn_test_opts_t *opts, pr1.path, (int)pr1.rev, pr2.path, (int)pr2.rev); } + /* Asking directly, i.e. without involving the noderev IDs as + * an intermediate, should yield the same results. */ + SVN_ERR(svn_fs_node_relation(&relation, rev_root1, pr1.path, + rev_root2, pr2.path, subpool)); + if (i == j) + { + /* Identical note. */ + if (!related || relation != svn_fs_node_unchanged) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s:%d' to be the same as '%s:%d';" + " it was not", + pr1.path, (int)pr1.rev, pr2.path, (int)pr2.rev); + } + } + else if (related && relation != svn_fs_node_common_ancestor) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s:%d' to have a common ancestor with '%s:%d';" + " it had not", + pr1.path, (int)pr1.rev, pr2.path, (int)pr2.rev); + } + else if (!related && relation != svn_fs_node_unrelated) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s:%d' to not be related to '%s:%d'; it was", + pr1.path, (int)pr1.rev, pr2.path, (int)pr2.rev); + } + + svn_pool_clear(subpool); + } /* for ... */ + } /* for ... */ + + /* Verify that the noderevs stay the same after their last change. */ + for (i = 0; i < 6; ++i) + { + const char *path = latest_changes[i].path; + svn_revnum_t latest = latest_changes[i].rev; + svn_fs_root_t *latest_root; + svn_revnum_t rev; + svn_fs_node_relation_t relation; + + /* FS root of the latest change. */ + svn_pool_clear(subpool); + SVN_ERR(svn_fs_revision_root(&latest_root, fs, latest, subpool)); + + /* All future revisions. */ + for (rev = latest + 1; rev <= 10; ++rev) + { + /* Query their noderev relationship to the latest change. */ + SVN_ERR(svn_fs_revision_root(&rev_root, fs, rev, subpool)); + SVN_ERR(svn_fs_node_relation(&relation, latest_root, path, + rev_root, path, subpool)); + + /* They shall use the same noderevs */ + if (relation != svn_fs_node_unchanged) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s:%d' to be the same as '%s:%d';" + " it was not", + path, (int)latest, path, (int)rev); + } + } /* for ... */ + } /* for ... */ + } + + /* Destroy the subpool. */ + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + + +static svn_error_t * +check_txn_related(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + apr_pool_t *subpool = svn_pool_create(pool); + svn_fs_t *fs; + svn_fs_txn_t *txn[3]; + svn_fs_root_t *root[3]; + svn_revnum_t youngest_rev = 0; + + /* Create a filesystem and repository. */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-check-related", + opts, pool)); + + /*** Step I: Build up some state in our repository through a series + of commits */ + + /* This is the node graph we are testing. It contains one revision (r1) + and two transactions, T1 and T2 - yet uncommitted. + + A is a file that exists in r1 (A-0) and gets modified in both txns. + C is a copy of A1 made in both txns. + B is a new node created in both txns + D is a file that exists in r1 (D-0) and never gets modified. + + +--A-0--+ D-0 + | | + +-----+ +-----+ + | | | | + B-1 C-T A-1 A-2 C-1 B-2 + */ + /* Revision 1 */ + SVN_ERR(svn_fs_begin_txn(&txn[0], fs, youngest_rev, subpool)); + SVN_ERR(svn_fs_txn_root(&root[0], txn[0], subpool)); + SVN_ERR(svn_fs_make_file(root[0], "A", subpool)); + SVN_ERR(svn_test__set_file_contents(root[0], "A", "1", subpool)); + SVN_ERR(svn_fs_make_file(root[0], "D", subpool)); + SVN_ERR(svn_fs_commit_txn(NULL, &youngest_rev, txn[0], subpool)); + SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev)); + svn_pool_clear(subpool); + SVN_ERR(svn_fs_revision_root(&root[0], fs, youngest_rev, pool)); + + /* Transaction 1 */ + SVN_ERR(svn_fs_begin_txn(&txn[1], fs, youngest_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root[1], txn[1], pool)); + SVN_ERR(svn_test__set_file_contents(root[1], "A", "2", pool)); + SVN_ERR(svn_fs_copy(root[0], "A", root[1], "C", pool)); + SVN_ERR(svn_fs_make_file(root[1], "B", pool)); + + /* Transaction 2 */ + SVN_ERR(svn_fs_begin_txn(&txn[2], fs, youngest_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root[2], txn[2], pool)); + SVN_ERR(svn_test__set_file_contents(root[2], "A", "2", pool)); + SVN_ERR(svn_fs_copy(root[0], "A", root[2], "C", pool)); + SVN_ERR(svn_fs_make_file(root[2], "B", pool)); + + /*** Step II: Exhaustively verify relationship between all nodes in + existence. */ + { + int i, j; + + struct path_rev_t + { + const char *path; + int root; + }; + + /* Our 16 existing files/revisions. */ + struct path_rev_t path_revs[8] = { + { "A", 0 }, { "A", 1 }, { "A", 2 }, + { "B", 1 }, { "B", 2 }, + { "C", 1 }, { "C", 2 }, + { "D", 0 } + }; + + int related_matrix[8][8] = { + /* A-0 ... D-0 across the top here*/ + { 1, 1, 1, 0, 0, 1, 1, 0 }, /* A-0 */ + { 1, 1, 1, 0, 0, 1, 1, 0 }, /* A-1 */ + { 1, 1, 1, 0, 0, 1, 1, 0 }, /* A-2 */ + { 0, 0, 0, 1, 0, 0, 0, 0 }, /* C-1 */ + { 0, 0, 0, 0, 1, 0, 0, 0 }, /* C-2 */ + { 1, 1, 1, 0, 0, 1, 1, 0 }, /* B-1 */ + { 1, 1, 1, 0, 0, 1, 1, 0 }, /* B-2 */ + { 0, 0, 0, 0, 0, 0, 0, 1 } /* D-0 */ + }; + + /* Here's the fun part. Running the tests. */ + for (i = 0; i < 8; i++) + { + for (j = 0; j < 8; j++) + { + struct path_rev_t pr1 = path_revs[i]; + struct path_rev_t pr2 = path_revs[j]; + const svn_fs_id_t *id1, *id2; + int related = 0; + svn_fs_node_relation_t relation; + svn_pool_clear(subpool); + + /* Get the ID for the first path/revision combination. */ + SVN_ERR(svn_fs_node_id(&id1, root[pr1.root], pr1.path, subpool)); + + /* Get the ID for the second path/revision combination. */ + SVN_ERR(svn_fs_node_id(&id2, root[pr2.root], pr2.path, subpool)); + + /* <exciting> Now, run the relationship check! </exciting> */ + related = svn_fs_check_related(id1, id2) ? 1 : 0; + if (related == related_matrix[i][j]) + { + /* xlnt! */ + } + else if ((! related) && related_matrix[i][j]) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s-%d' to be related to '%s-%d'; it was not", + pr1.path, pr1.root, pr2.path, pr2.root); + } + else if (related && (! related_matrix[i][j])) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s-%d' to not be related to '%s-%d'; it was", + pr1.path, pr1.root, pr2.path, pr2.root); + } + + /* Asking directly, i.e. without involving the noderev IDs as + * an intermediate, should yield the same results. */ + SVN_ERR(svn_fs_node_relation(&relation, root[pr1.root], pr1.path, + root[pr2.root], pr2.path, subpool)); + if (i == j) + { + /* Identical note. */ + if (!related || relation != svn_fs_node_unchanged) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s-%d' to be the same as '%s-%d';" + " it was not", + pr1.path, pr1.root, pr2.path, pr2.root); + } + } + else if (related && relation != svn_fs_node_common_ancestor) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s-%d' to have a common ancestor with '%s-%d';" + " it had not", + pr1.path, pr1.root, pr2.path, pr2.root); + } + else if (!related && relation != svn_fs_node_unrelated) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected '%s-%d' to not be related to '%s-%d'; it was", + pr1.path, pr1.root, pr2.path, pr2.root); + } } /* for ... */ } /* for ... */ + + /* Verify that the noderevs stay the same after their last change. + There is only D that is not changed. */ + for (i = 1; i <= 2; ++i) + { + svn_fs_node_relation_t relation; + svn_pool_clear(subpool); + + /* Query their noderev relationship to the latest change. */ + SVN_ERR(svn_fs_node_relation(&relation, root[i], "D", + root[0], "D", subpool)); + + /* They shall use the same noderevs */ + if (relation != svn_fs_node_unchanged) + { + return svn_error_createf + (SVN_ERR_TEST_FAILED, NULL, + "expected 'D-%d' to be the same as 'D-0'; it was not", i); + } + } /* for ... */ } /* Destroy the subpool. */ @@ -4238,7 +4539,7 @@ branch_test(const svn_test_opts_t *opts, svn_revnum_t youngest_rev = 0; /* Create a filesystem and repository. */ - SVN_ERR(svn_test__create_fs(&fs, "test-repo-branch-test", + SVN_ERR(svn_test__create_fs(&fs, "test-repo-branch", opts, pool)); /*** Revision 1: Create the greek tree in revision. ***/ @@ -4305,22 +4606,49 @@ branch_test(const svn_test_opts_t *opts, } +/* Verify that file FILENAME under ROOT has the same contents checksum + * as CONTENTS when comparing the checksums of the given TYPE. + * Use POOL for temporary allocations. */ +static svn_error_t * +verify_file_checksum(svn_stringbuf_t *contents, + svn_fs_root_t *root, + const char *filename, + svn_checksum_kind_t type, + apr_pool_t *pool) +{ + svn_checksum_t *expected_checksum, *actual_checksum; + + /* Write a file, compare the repository's idea of its checksum + against our idea of its checksum. They should be the same. */ + SVN_ERR(svn_checksum(&expected_checksum, type, contents->data, + contents->len, pool)); + SVN_ERR(svn_fs_file_checksum(&actual_checksum, type, root, filename, TRUE, + pool)); + if (!svn_checksum_match(expected_checksum, actual_checksum)) + return svn_error_createf + (SVN_ERR_FS_GENERAL, NULL, + "verify-checksum: checksum mismatch:\n" + " expected: %s\n" + " actual: %s\n", + svn_checksum_to_cstring(expected_checksum, pool), + svn_checksum_to_cstring(actual_checksum, pool)); + + return SVN_NO_ERROR; +} + static svn_error_t * verify_checksum(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_txn_t *txn; - svn_fs_root_t *txn_root; + svn_fs_root_t *txn_root, *rev_root; svn_stringbuf_t *str; - svn_checksum_t *expected_checksum, *actual_checksum; + svn_revnum_t rev; /* Write a file, compare the repository's idea of its checksum against our idea of its checksum. They should be the same. */ - str = svn_stringbuf_create("My text editor charges me rent.", pool); - SVN_ERR(svn_checksum(&expected_checksum, svn_checksum_md5, str->data, - str->len, pool)); SVN_ERR(svn_test__create_fs(&fs, "test-repo-verify-checksum", opts, pool)); @@ -4328,17 +4656,20 @@ verify_checksum(const svn_test_opts_t *opts, SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_fs_make_file(txn_root, "fact", pool)); SVN_ERR(svn_test__set_file_contents(txn_root, "fact", str->data, pool)); - SVN_ERR(svn_fs_file_checksum(&actual_checksum, svn_checksum_md5, txn_root, - "fact", TRUE, pool)); - if (!svn_checksum_match(expected_checksum, actual_checksum)) - return svn_error_createf - (SVN_ERR_FS_GENERAL, NULL, - "verify-checksum: checksum mismatch:\n" - " expected: %s\n" - " actual: %s\n", - svn_checksum_to_cstring(expected_checksum, pool), - svn_checksum_to_cstring(actual_checksum, pool)); + /* Do it for the txn. */ + SVN_ERR(verify_file_checksum(str, txn_root, "fact", svn_checksum_md5, + pool)); + SVN_ERR(verify_file_checksum(str, txn_root, "fact", svn_checksum_sha1, + pool)); + + /* Do it again - this time for the revision. */ + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, pool)); + SVN_ERR(svn_fs_revision_root(&rev_root, fs, rev, pool)); + SVN_ERR(verify_file_checksum(str, rev_root, "fact", svn_checksum_md5, + pool)); + SVN_ERR(verify_file_checksum(str, rev_root, "fact", svn_checksum_sha1, + pool)); return SVN_NO_ERROR; } @@ -4568,6 +4899,7 @@ unordered_txn_dirprops(const svn_test_opts_t *opts, svn_fs_root_t *txn_root, *txn_root2; svn_string_t pval; svn_revnum_t new_rev, not_rev; + svn_boolean_t is_bdb = strcmp(opts->fs_type, "bdb") == 0; /* This is a regression test for issue #2751. */ @@ -4624,10 +4956,21 @@ unordered_txn_dirprops(const svn_test_opts_t *opts, /* Commit the first one first. */ SVN_ERR(test_commit_txn(&new_rev, txn, NULL, pool)); - /* Then commit the second -- but expect an conflict because the - directory wasn't up-to-date, which is required for propchanges. */ - SVN_ERR(test_commit_txn(¬_rev, txn2, "/A/B", pool)); - SVN_ERR(svn_fs_abort_txn(txn2, pool)); + /* Some backends are clever then others. */ + if (is_bdb) + { + /* Then commit the second -- but expect an conflict because the + directory wasn't up-to-date, which is required for propchanges. */ + SVN_ERR(test_commit_txn(¬_rev, txn2, "/A/B", pool)); + SVN_ERR(svn_fs_abort_txn(txn2, pool)); + } + else + { + /* Then commit the second -- there will be no conflict despite the + directory being out-of-data because the properties as well as the + directory structure (list of nodes) was up-to-date. */ + SVN_ERR(test_commit_txn(¬_rev, txn2, NULL, pool)); + } return SVN_NO_ERROR; } @@ -4936,7 +5279,7 @@ delete_fs(const svn_test_opts_t *opts, return SVN_NO_ERROR; } -/* Issue 4340, "fs layer should reject filenames with trailing \n" */ +/* Issue 4340, "filenames containing \n corrupt FSFS repositories" */ static svn_error_t * filename_trailing_newline(const svn_test_opts_t *opts, apr_pool_t *pool) @@ -4947,15 +5290,11 @@ filename_trailing_newline(const svn_test_opts_t *opts, svn_fs_root_t *txn_root, *root; svn_revnum_t youngest_rev = 0; svn_error_t *err; - svn_boolean_t legacy_backend; - static const char contents[] = "foo\003bar"; - - /* The FS API wants \n to be permitted, but FSFS never implemented that, - * so for FSFS we expect errors rather than successes in some of the commits. - * Use a blacklist approach so that new FSes default to implementing the API - * as originally defined. */ - legacy_backend = (!strcmp(opts->fs_type, SVN_FS_TYPE_FSFS)); + /* The FS API wants \n to be permitted, but FSFS never implemented that. + * Moreover, formats like svn:mergeinfo and svn:externals don't support + * it either. So, we can't have newlines in file names in any FS. + */ SVN_ERR(svn_test__create_fs(&fs, "test-repo-filename-trailing-newline", opts, pool)); @@ -4967,73 +5306,1874 @@ filename_trailing_newline(const svn_test_opts_t *opts, SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev)); svn_pool_clear(subpool); - /* Attempt to copy /foo to "/bar\n". This should fail on FSFS. */ + /* Attempt to copy /foo to "/bar\n". This should fail. */ SVN_ERR(svn_fs_begin_txn(&txn, fs, youngest_rev, subpool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool)); SVN_ERR(svn_fs_revision_root(&root, fs, youngest_rev, subpool)); err = svn_fs_copy(root, "/foo", txn_root, "/bar\n", subpool); - if (!legacy_backend) - SVN_TEST_ASSERT(err == SVN_NO_ERROR); - else - SVN_TEST_ASSERT_ERROR(err, SVN_ERR_FS_PATH_SYNTAX); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_FS_PATH_SYNTAX); - /* Attempt to create a file /foo/baz\n. This should fail on FSFS. */ + /* Attempt to create a file /foo/baz\n. This should fail. */ err = svn_fs_make_file(txn_root, "/foo/baz\n", subpool); - if (!legacy_backend) - SVN_TEST_ASSERT(err == SVN_NO_ERROR); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_FS_PATH_SYNTAX); + + /* Attempt to create a directory /foo/bang\n. This should fail. */ + err = svn_fs_make_dir(txn_root, "/foo/bang\n", subpool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_FS_PATH_SYNTAX); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_fs_info_format(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + int fs_format; + svn_version_t *supports_version; + svn_version_t v1_5_0 = {1, 5, 0, ""}; + svn_version_t v1_9_0 = {1, 9, 0, ""}; + svn_test_opts_t opts2; + svn_boolean_t is_fsx = strcmp(opts->fs_type, "fsx") == 0; + + opts2 = *opts; + opts2.server_minor_version = is_fsx ? 9 : 5; + + SVN_ERR(svn_test__create_fs(&fs, "test-fs-format-info", &opts2, pool)); + SVN_ERR(svn_fs_info_format(&fs_format, &supports_version, fs, pool, pool)); + + if (is_fsx) + { + SVN_TEST_ASSERT(fs_format == 1); + SVN_TEST_ASSERT(svn_ver_equal(supports_version, &v1_9_0)); + } else - SVN_TEST_ASSERT_ERROR(err, SVN_ERR_FS_PATH_SYNTAX); - + { + /* happens to be the same for FSFS and BDB */ + SVN_TEST_ASSERT(fs_format == 3); + SVN_TEST_ASSERT(svn_ver_equal(supports_version, &v1_5_0)); + } + + return SVN_NO_ERROR; +} - /* Create another file, with contents. */ - if (!legacy_backend) +/* Sleeps until apr_time_now() value changes. */ +static void sleep_for_timestamps(void) +{ + apr_time_t start = apr_time_now(); + + while (start == apr_time_now()) { - SVN_ERR(svn_fs_make_file(txn_root, "/bar\n/baz\n", subpool)); - SVN_ERR(svn_test__set_file_contents(txn_root, "bar\n/baz\n", - contents, pool)); + apr_sleep(APR_USEC_PER_SEC / 1000); } +} + +static svn_error_t * +commit_timestamp(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root; + svn_string_t *date = svn_string_create("Yesterday", pool); + svn_revnum_t rev = 0; + apr_hash_t *proplist; + svn_string_t *svn_date; + svn_string_t *txn_svn_date; + + SVN_ERR(svn_test__create_fs(&fs, "test-fs-commit-timestamp", + opts, pool)); + + /* Commit with a specified svn:date. */ + SVN_ERR(svn_fs_begin_txn2(&txn, fs, rev, SVN_FS_TXN_CLIENT_DATE, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_dir(txn_root, "/foo", pool)); + SVN_ERR(svn_fs_change_txn_prop(txn, SVN_PROP_REVISION_DATE, date, pool)); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, pool)); + + SVN_ERR(svn_fs_revision_proplist(&proplist, fs, rev, pool)); + svn_date = apr_hash_get(proplist, SVN_PROP_REVISION_DATE, + APR_HASH_KEY_STRING); + SVN_TEST_ASSERT(svn_date && !strcmp(svn_date->data, date->data)); + + /* Commit that overwrites the specified svn:date. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, rev, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_dir(txn_root, "/bar", pool)); + SVN_ERR(svn_fs_change_txn_prop(txn, SVN_PROP_REVISION_DATE, date, pool)); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, pool)); + + SVN_ERR(svn_fs_revision_proplist(&proplist, fs, rev, pool)); + svn_date = apr_hash_get(proplist, SVN_PROP_REVISION_DATE, + APR_HASH_KEY_STRING); + SVN_TEST_ASSERT(svn_date && strcmp(svn_date->data, date->data)); + + /* Commit with a missing svn:date. */ + SVN_ERR(svn_fs_begin_txn2(&txn, fs, rev, SVN_FS_TXN_CLIENT_DATE, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_dir(txn_root, "/zag", pool)); + SVN_ERR(svn_fs_change_txn_prop(txn, SVN_PROP_REVISION_DATE, NULL, pool)); + SVN_ERR(svn_fs_txn_prop(&svn_date, txn, SVN_PROP_REVISION_DATE, pool)); + SVN_TEST_ASSERT(!svn_date); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, pool)); + + SVN_ERR(svn_fs_revision_proplist(&proplist, fs, rev, pool)); + svn_date = apr_hash_get(proplist, SVN_PROP_REVISION_DATE, + APR_HASH_KEY_STRING); + SVN_TEST_ASSERT(!svn_date); + + /* Commit that overwites a missing svn:date. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, rev, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_dir(txn_root, "/zig", pool)); + SVN_ERR(svn_fs_change_txn_prop(txn, SVN_PROP_REVISION_DATE, NULL, pool)); + SVN_ERR(svn_fs_txn_prop(&svn_date, txn, SVN_PROP_REVISION_DATE, pool)); + SVN_TEST_ASSERT(!svn_date); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, pool)); + + SVN_ERR(svn_fs_revision_proplist(&proplist, fs, rev, pool)); + svn_date = apr_hash_get(proplist, SVN_PROP_REVISION_DATE, + APR_HASH_KEY_STRING); + SVN_TEST_ASSERT(svn_date); + + /* Commit that doesn't do anything special about svn:date. */ + SVN_ERR(svn_fs_begin_txn2(&txn, fs, rev, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_dir(txn_root, "/zig/foo", pool)); + SVN_ERR(svn_fs_txn_prop(&txn_svn_date, txn, SVN_PROP_REVISION_DATE, pool)); + SVN_TEST_ASSERT(txn_svn_date); + sleep_for_timestamps(); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, pool)); + + SVN_ERR(svn_fs_revision_proplist(&proplist, fs, rev, pool)); + svn_date = apr_hash_get(proplist, SVN_PROP_REVISION_DATE, + APR_HASH_KEY_STRING); + SVN_TEST_ASSERT(svn_date); + SVN_TEST_ASSERT(!svn_string_compare(svn_date, txn_svn_date)); + + /* Commit that instructs the backend to use a specific svn:date, but + * doesn't provide one. This used to fail with BDB prior to r1663697. */ + SVN_ERR(svn_fs_begin_txn2(&txn, fs, rev, SVN_FS_TXN_CLIENT_DATE, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_dir(txn_root, "/zig/bar", pool)); + SVN_ERR(svn_fs_txn_prop(&txn_svn_date, txn, SVN_PROP_REVISION_DATE, pool)); + SVN_TEST_ASSERT(txn_svn_date); + sleep_for_timestamps(); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, pool)); + + SVN_ERR(svn_fs_revision_proplist(&proplist, fs, rev, pool)); + svn_date = apr_hash_get(proplist, SVN_PROP_REVISION_DATE, + APR_HASH_KEY_STRING); + SVN_TEST_ASSERT(svn_date); + SVN_TEST_ASSERT(!svn_string_compare(svn_date, txn_svn_date)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_compat_version(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_version_t *compatible_version; + apr_hash_t *config = apr_hash_make(pool); + + svn_version_t vcurrent = {SVN_VER_MAJOR, SVN_VER_MINOR, 0, ""}; + svn_version_t v1_2_0 = {1, 2, 0, ""}; + svn_version_t v1_3_0 = {1, 3, 0, ""}; + svn_version_t v1_5_0 = {1, 5, 0, ""}; + + /* no version specified -> default to the current one */ + SVN_ERR(svn_fs__compatible_version(&compatible_version, config, pool)); + SVN_TEST_ASSERT(svn_ver_equal(compatible_version, &vcurrent)); + + /* test specific compat option */ + svn_hash_sets(config, SVN_FS_CONFIG_PRE_1_6_COMPATIBLE, "1"); + SVN_ERR(svn_fs__compatible_version(&compatible_version, config, pool)); + SVN_TEST_ASSERT(svn_ver_equal(compatible_version, &v1_5_0)); + + /* test precedence amongst compat options */ + svn_hash_sets(config, SVN_FS_CONFIG_PRE_1_8_COMPATIBLE, "1"); + SVN_ERR(svn_fs__compatible_version(&compatible_version, config, pool)); + SVN_TEST_ASSERT(svn_ver_equal(compatible_version, &v1_5_0)); + + svn_hash_sets(config, SVN_FS_CONFIG_PRE_1_4_COMPATIBLE, "1"); + SVN_ERR(svn_fs__compatible_version(&compatible_version, config, pool)); + SVN_TEST_ASSERT(svn_ver_equal(compatible_version, &v1_3_0)); + + /* precedence should work with the generic option as well */ + svn_hash_sets(config, SVN_FS_CONFIG_COMPATIBLE_VERSION, "1.4.17-??"); + SVN_ERR(svn_fs__compatible_version(&compatible_version, config, pool)); + SVN_TEST_ASSERT(svn_ver_equal(compatible_version, &v1_3_0)); + + svn_hash_sets(config, SVN_FS_CONFIG_COMPATIBLE_VERSION, "1.2.3-no!"); + SVN_ERR(svn_fs__compatible_version(&compatible_version, config, pool)); + SVN_TEST_ASSERT(svn_ver_equal(compatible_version, &v1_2_0)); + + /* test generic option alone */ + config = apr_hash_make(pool); + svn_hash_sets(config, SVN_FS_CONFIG_COMPATIBLE_VERSION, "1.2.3-no!"); + SVN_ERR(svn_fs__compatible_version(&compatible_version, config, pool)); + SVN_TEST_ASSERT(svn_ver_equal(compatible_version, &v1_2_0)); + + /* out of range values should be caped by the current tool version */ + svn_hash_sets(config, SVN_FS_CONFIG_COMPATIBLE_VERSION, "2.3.4-x"); + SVN_ERR(svn_fs__compatible_version(&compatible_version, config, pool)); + SVN_TEST_ASSERT(svn_ver_equal(compatible_version, &vcurrent)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +dir_prop_merge(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_revnum_t head_rev; + svn_fs_root_t *root; + svn_fs_txn_t *txn, *mid_txn, *top_txn, *sub_txn, *c_txn; + svn_boolean_t is_bdb = strcmp(opts->fs_type, "bdb") == 0; + + /* Create test repository. */ + SVN_ERR(svn_test__create_fs(&fs, "test-fs-dir_prop-merge", opts, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn, pool)); - if (!legacy_backend) + /* Create and verify the greek tree. */ + SVN_ERR(svn_test__create_greek_tree(root, pool)); + SVN_ERR(test_commit_txn(&head_rev, txn, NULL, pool)); + + /* Start concurrent transactions */ + + /* 1st: modify a mid-level directory */ + SVN_ERR(svn_fs_begin_txn2(&mid_txn, fs, head_rev, 0, pool)); + SVN_ERR(svn_fs_txn_root(&root, mid_txn, pool)); + SVN_ERR(svn_fs_change_node_prop(root, "A/D", "test-prop", + svn_string_create("val1", pool), pool)); + svn_fs_close_root(root); + + /* 2st: modify a top-level directory */ + SVN_ERR(svn_fs_begin_txn2(&top_txn, fs, head_rev, 0, pool)); + SVN_ERR(svn_fs_txn_root(&root, top_txn, pool)); + SVN_ERR(svn_fs_change_node_prop(root, "A", "test-prop", + svn_string_create("val2", pool), pool)); + svn_fs_close_root(root); + + SVN_ERR(svn_fs_begin_txn2(&sub_txn, fs, head_rev, 0, pool)); + SVN_ERR(svn_fs_txn_root(&root, sub_txn, pool)); + SVN_ERR(svn_fs_change_node_prop(root, "A/D/G", "test-prop", + svn_string_create("val3", pool), pool)); + svn_fs_close_root(root); + + /* 3rd: modify a conflicting change to the mid-level directory */ + SVN_ERR(svn_fs_begin_txn2(&c_txn, fs, head_rev, 0, pool)); + SVN_ERR(svn_fs_txn_root(&root, c_txn, pool)); + SVN_ERR(svn_fs_change_node_prop(root, "A/D", "test-prop", + svn_string_create("valX", pool), pool)); + svn_fs_close_root(root); + + /* Prop changes to the same node should conflict */ + SVN_ERR(test_commit_txn(&head_rev, mid_txn, NULL, pool)); + SVN_ERR(test_commit_txn(&head_rev, c_txn, "/A/D", pool)); + SVN_ERR(svn_fs_abort_txn(c_txn, pool)); + + /* Changes in a sub-tree should not conflict with prop changes to some + parent directory but some backends are clever then others. */ + if (is_bdb) { - svn_revnum_t after_rev; - static svn_test__tree_entry_t expected_entries[] = { - { "foo", NULL }, - { "bar\n", NULL }, - { "foo/baz\n", "" }, - { "bar\n/baz\n", contents }, - { NULL, NULL } - }; - const char *expected_changed_paths[] = { - "/bar\n", - "/foo/baz\n", - "/bar\n/baz\n", - NULL - }; - apr_hash_t *expected_changes = apr_hash_make(pool); - int i; + SVN_ERR(test_commit_txn(&head_rev, top_txn, "/A", pool)); + SVN_ERR(svn_fs_abort_txn(top_txn, pool)); + } + else + { + SVN_ERR(test_commit_txn(&head_rev, top_txn, NULL, pool)); + } + + /* The inverted case is not that trivial to handle. Hence, conflict. + Depending on the checking order, the reported conflict path differs. */ + SVN_ERR(test_commit_txn(&head_rev, sub_txn, is_bdb ? "/A/D" : "/A", pool)); + SVN_ERR(svn_fs_abort_txn(sub_txn, pool)); + + return SVN_NO_ERROR; +} + +#if APR_HAS_THREADS +struct reopen_modify_baton_t { + const char *fs_path; + const char *txn_name; + apr_pool_t *pool; + svn_error_t *err; +}; + +static void * APR_THREAD_FUNC +reopen_modify_child(apr_thread_t *tid, void *data) +{ + struct reopen_modify_baton_t *baton = data; + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *root; + + baton->err = svn_fs_open(&fs, baton->fs_path, NULL, baton->pool); + if (!baton->err) + baton->err = svn_fs_open_txn(&txn, fs, baton->txn_name, baton->pool); + if (!baton->err) + baton->err = svn_fs_txn_root(&root, txn, baton->pool); + if (!baton->err) + baton->err = svn_fs_change_node_prop(root, "A", "name", + svn_string_create("value", + baton->pool), + baton->pool); + svn_pool_destroy(baton->pool); + apr_thread_exit(tid, 0); + return NULL; +} +#endif + +static svn_error_t * +reopen_modify(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ +#if APR_HAS_THREADS + svn_fs_t *fs; + svn_revnum_t head_rev = 0; + svn_fs_root_t *root; + svn_fs_txn_t *txn; + const char *fs_path, *txn_name; + svn_string_t *value; + struct reopen_modify_baton_t baton; + apr_status_t status, child_status; + apr_threadattr_t *tattr; + apr_thread_t *tid; + + /* Create test repository with greek tree. */ + fs_path = "test-reopen-modify"; + SVN_ERR(svn_test__create_fs(&fs, fs_path, opts, pool)); + SVN_ERR(svn_fs_begin_txn(&txn, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn, pool)); + SVN_ERR(svn_test__create_greek_tree(root, pool)); + SVN_ERR(test_commit_txn(&head_rev, txn, NULL, pool)); + + /* Create txn with changes. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_name(&txn_name, txn, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn, pool)); + SVN_ERR(svn_fs_make_dir(root, "X", pool)); + + /* In another thread: reopen fs and txn, and add more changes. This + works in BDB and FSX but in FSFS the txn_dir_cache becomes + out-of-date and the thread's changes don't reach the revision. */ + baton.fs_path = fs_path; + baton.txn_name = txn_name; + baton.pool = svn_pool_create(pool); + status = apr_threadattr_create(&tattr, pool); + if (status) + return svn_error_wrap_apr(status, _("Can't create threadattr")); + status = apr_thread_create(&tid, tattr, reopen_modify_child, &baton, pool); + if (status) + return svn_error_wrap_apr(status, _("Can't create thread")); + status = apr_thread_join(&child_status, tid); + if (status) + return svn_error_wrap_apr(status, _("Can't join thread")); + if (baton.err) + return svn_error_trace(baton.err); + + /* Commit */ + SVN_ERR(test_commit_txn(&head_rev, txn, NULL, pool)); + + /* Check for change made by thread. */ + SVN_ERR(svn_fs_revision_root(&root, fs, head_rev, pool)); + SVN_ERR(svn_fs_node_prop(&value, root, "A", "name", pool)); + SVN_TEST_ASSERT(value && !strcmp(value->data, "value")); + + return SVN_NO_ERROR; +#else + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, "no thread support"); +#endif +} + +static svn_error_t * +upgrade_while_committing(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_revnum_t head_rev = 0; + svn_fs_root_t *root; + svn_fs_txn_t *txn1, *txn2; + const char *fs_path; + apr_hash_t *fs_config = apr_hash_make(pool); + + /* Bail (with success) on known-untestable scenarios */ + if (strcmp(opts->fs_type, "fsfs") != 0) + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, + "this will test FSFS repositories only"); + + if (opts->server_minor_version && (opts->server_minor_version < 6)) + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, + "pre-1.6 SVN doesn't support FSFS packing"); + + /* Create test repository with greek tree. */ + fs_path = "test-upgrade-while-committing"; + + svn_hash_sets(fs_config, SVN_FS_CONFIG_COMPATIBLE_VERSION, "1.7"); + svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_SHARD_SIZE, "2"); + SVN_ERR(svn_test__create_fs2(&fs, fs_path, opts, fs_config, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn1, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn1, pool)); + SVN_ERR(svn_test__create_greek_tree(root, pool)); + SVN_ERR(test_commit_txn(&head_rev, txn1, NULL, pool)); + + /* Create txn with changes. */ + SVN_ERR(svn_fs_begin_txn(&txn1, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn1, pool)); + SVN_ERR(svn_fs_make_dir(root, "/foo", pool)); + + /* Upgrade filesystem, but keep existing svn_fs_t object. */ + SVN_ERR(svn_fs_upgrade(fs_path, pool)); + + /* Creating a new txn for the old svn_fs_t should not fail. */ + SVN_ERR(svn_fs_begin_txn(&txn2, fs, head_rev, pool)); + + /* Committing the already existing txn should not fail. */ + SVN_ERR(test_commit_txn(&head_rev, txn1, NULL, pool)); + + /* Verify filesystem content. */ + SVN_ERR(svn_fs_verify(fs_path, NULL, 0, SVN_INVALID_REVNUM, NULL, NULL, + NULL, NULL, pool)); + + return SVN_NO_ERROR; +} + +/* Utility method for test_paths_changed. Verify that REV in FS changes + * exactly one path and that that change is a property change. Expect + * the MERGEINFO_MOD flag of the change to have the given value. + */ +static svn_error_t * +verify_root_prop_change(svn_fs_t *fs, + svn_revnum_t rev, + svn_tristate_t mergeinfo_mod, + apr_pool_t *pool) +{ + svn_fs_path_change2_t *change; + svn_fs_root_t *root; + apr_hash_t *changes; + + SVN_ERR(svn_fs_revision_root(&root, fs, rev, pool)); + SVN_ERR(svn_fs_paths_changed2(&changes, root, pool)); + SVN_TEST_ASSERT(apr_hash_count(changes) == 1); + change = svn_hash_gets(changes, "/"); + + SVN_TEST_ASSERT(change->node_rev_id); + SVN_TEST_ASSERT(change->change_kind == svn_fs_path_change_modify); + SVN_TEST_ASSERT( change->node_kind == svn_node_dir + || change->node_kind == svn_node_unknown); + SVN_TEST_ASSERT(change->text_mod == FALSE); + SVN_TEST_ASSERT(change->prop_mod == TRUE); + + if (change->copyfrom_known) + { + SVN_TEST_ASSERT(change->copyfrom_rev == SVN_INVALID_REVNUM); + SVN_TEST_ASSERT(change->copyfrom_path == NULL); + } + + SVN_TEST_ASSERT(change->mergeinfo_mod == mergeinfo_mod); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_paths_changed(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_revnum_t head_rev = 0; + svn_fs_root_t *root; + svn_fs_txn_t *txn; + const char *fs_path; + apr_hash_t *changes; + svn_boolean_t has_mergeinfo_mod = FALSE; + apr_hash_index_t *hi; + int i; + + /* The "mergeinfo_mod flag will say "unknown" until recently. */ + if ( strcmp(opts->fs_type, "bdb") != 0 + && (!opts->server_minor_version || (opts->server_minor_version >= 9))) + has_mergeinfo_mod = TRUE; + + /* Create test repository with greek tree. */ + fs_path = "test-paths-changed"; + + SVN_ERR(svn_test__create_fs2(&fs, fs_path, opts, NULL, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn, pool)); + SVN_ERR(svn_test__create_greek_tree(root, pool)); + SVN_ERR(test_commit_txn(&head_rev, txn, NULL, pool)); + + /* Create txns with various prop changes. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn, pool)); + SVN_ERR(svn_fs_change_node_prop(root, "/", "propname", + svn_string_create("propval", pool), pool)); + SVN_ERR(test_commit_txn(&head_rev, txn, NULL, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn, pool)); + SVN_ERR(svn_fs_change_node_prop(root, "/", "svn:mergeinfo", + svn_string_create("/: 1\n", pool), pool)); + SVN_ERR(test_commit_txn(&head_rev, txn, NULL, pool)); + + /* Verify changed path lists. */ + + /* Greek tree creation rev. */ + SVN_ERR(svn_fs_revision_root(&root, fs, head_rev - 2, pool)); + SVN_ERR(svn_fs_paths_changed2(&changes, root, pool)); + + /* Reports all paths? */ + for (i = 0; svn_test__greek_tree_nodes[i].path; ++i) + { + const char *path + = svn_fspath__canonicalize(svn_test__greek_tree_nodes[i].path, pool); + + SVN_TEST_ASSERT(svn_hash_gets(changes, path)); + } + + SVN_TEST_ASSERT(apr_hash_count(changes) == i); + + /* Verify per-path info. */ + for (hi = apr_hash_first(pool, changes); hi; hi = apr_hash_next(hi)) + { + svn_fs_path_change2_t *change = apr_hash_this_val(hi); + + SVN_TEST_ASSERT(change->node_rev_id); + SVN_TEST_ASSERT(change->change_kind == svn_fs_path_change_add); + SVN_TEST_ASSERT( change->node_kind == svn_node_file + || change->node_kind == svn_node_dir + || change->node_kind == svn_node_unknown); + + if (change->node_kind != svn_node_unknown) + SVN_TEST_ASSERT(change->text_mod == ( change->node_kind + == svn_node_file)); + + SVN_TEST_ASSERT(change->prop_mod == FALSE); + + if (change->copyfrom_known) + { + SVN_TEST_ASSERT(change->copyfrom_rev == SVN_INVALID_REVNUM); + SVN_TEST_ASSERT(change->copyfrom_path == NULL); + } + + if (has_mergeinfo_mod) + SVN_TEST_ASSERT(change->mergeinfo_mod == svn_tristate_false); + else + SVN_TEST_ASSERT(change->mergeinfo_mod == svn_tristate_unknown); + } + + /* Propset rev. */ + SVN_ERR(verify_root_prop_change(fs, head_rev - 1, + has_mergeinfo_mod ? svn_tristate_false + : svn_tristate_unknown, + pool)); + + /* Mergeinfo set rev. */ + SVN_ERR(verify_root_prop_change(fs, head_rev, + has_mergeinfo_mod ? svn_tristate_true + : svn_tristate_unknown, + pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_delete_replaced_paths_changed(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_revnum_t head_rev = 0; + svn_fs_root_t *root; + svn_fs_txn_t *txn; + const char *fs_path; + apr_hash_t *changes; + svn_fs_path_change2_t *change; + const svn_fs_id_t *file_id; + + /* Create test repository with greek tree. */ + fs_path = "test-delete-replace-paths-changed"; + + SVN_ERR(svn_test__create_fs2(&fs, fs_path, opts, NULL, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn, pool)); + SVN_ERR(svn_test__create_greek_tree(root, pool)); + SVN_ERR(test_commit_txn(&head_rev, txn, NULL, pool)); + + /* Create that replaces a file with a folder and then deletes that + * replacement. Start with the deletion. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, head_rev, pool)); + SVN_ERR(svn_fs_txn_root(&root, txn, pool)); + SVN_ERR(svn_fs_delete(root, "/iota", pool)); + + /* The change list should now report a deleted file. */ + SVN_ERR(svn_fs_paths_changed2(&changes, root, pool)); + change = svn_hash_gets(changes, "/iota"); + file_id = change->node_rev_id; + SVN_TEST_ASSERT( change->node_kind == svn_node_file + || change->node_kind == svn_node_unknown); + SVN_TEST_ASSERT(change->change_kind == svn_fs_path_change_delete); + + /* Add a replacement. */ + SVN_ERR(svn_fs_make_dir(root, "/iota", pool)); + + /* The change list now reports a replacement by a directory. */ + SVN_ERR(svn_fs_paths_changed2(&changes, root, pool)); + change = svn_hash_gets(changes, "/iota"); + SVN_TEST_ASSERT( change->node_kind == svn_node_dir + || change->node_kind == svn_node_unknown); + SVN_TEST_ASSERT(change->change_kind == svn_fs_path_change_replace); + SVN_TEST_ASSERT(svn_fs_compare_ids(change->node_rev_id, file_id) != 0); + + /* Delete the replacement again. */ + SVN_ERR(svn_fs_delete(root, "/iota", pool)); + + /* The change list should now be reported as a deleted file again. */ + SVN_ERR(svn_fs_paths_changed2(&changes, root, pool)); + change = svn_hash_gets(changes, "/iota"); + SVN_TEST_ASSERT( change->node_kind == svn_node_file + || change->node_kind == svn_node_unknown); + SVN_TEST_ASSERT(change->change_kind == svn_fs_path_change_delete); + SVN_TEST_ASSERT(svn_fs_compare_ids(change->node_rev_id, file_id) == 0); + + /* Finally, commit the change. */ + SVN_ERR(test_commit_txn(&head_rev, txn, NULL, pool)); + + /* The committed revision should still report the same change. */ + SVN_ERR(svn_fs_revision_root(&root, fs, head_rev, pool)); + SVN_ERR(svn_fs_paths_changed2(&changes, root, pool)); + change = svn_hash_gets(changes, "/iota"); + SVN_TEST_ASSERT( change->node_kind == svn_node_file + || change->node_kind == svn_node_unknown); + SVN_TEST_ASSERT(change->change_kind == svn_fs_path_change_delete); + + return SVN_NO_ERROR; +} + +/* Get rid of transaction NAME in FS. This function deals with backend- + * specific behavior as permitted by the API. */ +static svn_error_t * +cleanup_txn(svn_fs_t *fs, + const char *name, + apr_pool_t *scratch_pool) +{ + /* Get rid of the txns one at a time. */ + svn_error_t *err = svn_fs_purge_txn(fs, name, scratch_pool); + + /* Some backends (BDB) don't support purging transactions that have never + * seen an abort or commit attempt. Simply abort those txns. */ + if (err && err->apr_err == SVN_ERR_FS_TRANSACTION_NOT_DEAD) + { + svn_fs_txn_t *txn; + svn_error_clear(err); + err = SVN_NO_ERROR; + + SVN_ERR(svn_fs_open_txn(&txn, fs, name, scratch_pool)); + SVN_ERR(svn_fs_abort_txn(txn, scratch_pool)); + + /* Should be gone now ... */ + SVN_TEST_ASSERT_ERROR(svn_fs_open_txn(&txn, fs, name, scratch_pool), + SVN_ERR_FS_NO_SUCH_TRANSACTION); + } + + return svn_error_trace(err); +} + +/* Make sure we get txn lists correctly. */ +static svn_error_t * +purge_txn_test(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + const char *name1, *name2; + apr_array_header_t *txn_list; + apr_pool_t *subpool = svn_pool_create(pool); + + SVN_ERR(svn_test__create_fs(&fs, "test-repo-purge-txn", + opts, pool)); - SVN_ERR(svn_fs_commit_txn(NULL, &after_rev, txn, subpool)); - SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(after_rev)); + /* Begin a new transaction, get its name (in the top pool), close it. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, subpool)); + SVN_ERR(svn_fs_txn_name(&name1, txn, pool)); + + /* Begin *another* transaction, get its name (in the top pool), close it. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, subpool)); + SVN_ERR(svn_fs_txn_name(&name2, txn, pool)); + svn_pool_clear(subpool); + + /* Get rid of the txns one at a time. */ + SVN_ERR(cleanup_txn(fs, name1, pool)); + + /* There should be exactly one left. */ + SVN_ERR(svn_fs_list_transactions(&txn_list, fs, pool)); + + /* Check the list. It should have *exactly* one entry. */ + SVN_TEST_ASSERT( txn_list->nelts == 1 + && !strcmp(name2, APR_ARRAY_IDX(txn_list, 0, const char *))); + + /* Get rid of the other txn as well. */ + SVN_ERR(cleanup_txn(fs, name2, pool)); + + /* There should be exactly one left. */ + SVN_ERR(svn_fs_list_transactions(&txn_list, fs, pool)); + + /* Check the list. It should have no entries. */ + SVN_TEST_ASSERT(txn_list->nelts == 0); + + return SVN_NO_ERROR; +} + +static svn_error_t * +compare_contents(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root, *root1, *root2; + const char *original = "original contents"; + svn_revnum_t rev; + int i; + apr_pool_t *iterpool = svn_pool_create(pool); + + /* Two similar but different texts that yield the same MD5 digest. */ + const char *evil_text1 + = "\xd1\x31\xdd\x02\xc5\xe6\xee\xc4\x69\x3d\x9a\x06\x98\xaf\xf9\x5c" + "\x2f\xca\xb5\x87\x12\x46\x7e\xab\x40\x04\x58\x3e\xb8\xfb\x7f\x89" + "\x55\xad\x34\x06\x09\xf4\xb3\x02\x83\xe4\x88\x83\x25\x71\x41\x5a" + "\x08\x51\x25\xe8\xf7\xcd\xc9\x9f\xd9\x1d\xbd\xf2\x80\x37\x3c\x5b" + "\xd8\x82\x3e\x31\x56\x34\x8f\x5b\xae\x6d\xac\xd4\x36\xc9\x19\xc6" + "\xdd\x53\xe2\xb4\x87\xda\x03\xfd\x02\x39\x63\x06\xd2\x48\xcd\xa0" + "\xe9\x9f\x33\x42\x0f\x57\x7e\xe8\xce\x54\xb6\x70\x80\xa8\x0d\x1e" + "\xc6\x98\x21\xbc\xb6\xa8\x83\x93\x96\xf9\x65\x2b\x6f\xf7\x2a\x70"; + const char *evil_text2 + = "\xd1\x31\xdd\x02\xc5\xe6\xee\xc4\x69\x3d\x9a\x06\x98\xaf\xf9\x5c" + "\x2f\xca\xb5\x07\x12\x46\x7e\xab\x40\x04\x58\x3e\xb8\xfb\x7f\x89" + "\x55\xad\x34\x06\x09\xf4\xb3\x02\x83\xe4\x88\x83\x25\xf1\x41\x5a" + "\x08\x51\x25\xe8\xf7\xcd\xc9\x9f\xd9\x1d\xbd\x72\x80\x37\x3c\x5b" + "\xd8\x82\x3e\x31\x56\x34\x8f\x5b\xae\x6d\xac\xd4\x36\xc9\x19\xc6" + "\xdd\x53\xe2\x34\x87\xda\x03\xfd\x02\x39\x63\x06\xd2\x48\xcd\xa0" + "\xe9\x9f\x33\x42\x0f\x57\x7e\xe8\xce\x54\xb6\x70\x80\x28\x0d\x1e" + "\xc6\x98\x21\xbc\xb6\xa8\x83\x93\x96\xf9\x65\xab\x6f\xf7\x2a\x70"; + svn_checksum_t *checksum1, *checksum2; + + /* (path, rev) pairs to compare plus the expected API return values */ + struct + { + svn_revnum_t rev1; + const char *path1; + svn_revnum_t rev2; + const char *path2; + + svn_boolean_t different; /* result of svn_fs_*_different */ + svn_tristate_t changed; /* result of svn_fs_*_changed */ + } to_compare[] = + { + /* same representation */ + { 1, "foo", 2, "foo", FALSE, svn_tristate_false }, + { 1, "foo", 2, "bar", FALSE, svn_tristate_false }, + { 2, "foo", 2, "bar", FALSE, svn_tristate_false }, + + /* different content but MD5 check is not reliable */ + { 3, "foo", 3, "bar", TRUE, svn_tristate_true }, + + /* different contents */ + { 1, "foo", 3, "bar", TRUE, svn_tristate_true }, + { 1, "foo", 3, "foo", TRUE, svn_tristate_true }, + { 3, "foo", 4, "bar", TRUE, svn_tristate_true }, + { 3, "foo", 4, "bar", TRUE, svn_tristate_true }, + { 2, "bar", 3, "bar", TRUE, svn_tristate_true }, + { 3, "bar", 4, "bar", TRUE, svn_tristate_true }, + + /* variations on the same theme: same content, possibly different rep */ + { 4, "foo", 4, "bar", FALSE, svn_tristate_unknown }, + { 1, "foo", 4, "bar", FALSE, svn_tristate_unknown }, + { 2, "foo", 4, "bar", FALSE, svn_tristate_unknown }, + { 1, "foo", 4, "foo", FALSE, svn_tristate_unknown }, + { 2, "foo", 4, "foo", FALSE, svn_tristate_unknown }, + { 2, "bar", 4, "bar", FALSE, svn_tristate_unknown }, + + /* EOL */ + { 0 }, + }; + + /* Same same, but different. + * Just checking that we actually have an MD5 collision. */ + SVN_ERR(svn_checksum(&checksum1, svn_checksum_md5, evil_text1, + strlen(evil_text1), pool)); + SVN_ERR(svn_checksum(&checksum2, svn_checksum_md5, evil_text2, + strlen(evil_text2), pool)); + SVN_TEST_ASSERT(svn_checksum_match(checksum1, checksum1)); + SVN_TEST_ASSERT(strcmp(evil_text1, evil_text2)); + + /* Now, build up our test repo. */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-compare-contents", + opts, pool)); + + /* Rev 1: create a file. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_fs_make_file(txn_root, "foo", iterpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "foo", original, iterpool)); + SVN_ERR(svn_fs_change_node_prop(txn_root, "foo", "prop", + svn_string_create(original, iterpool), + iterpool)); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, iterpool)); + SVN_TEST_ASSERT(rev == 1); + svn_pool_clear(iterpool); + + /* Rev 2: copy that file. */ + SVN_ERR(svn_fs_revision_root(&root1, fs, rev, iterpool)); + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_fs_copy(root1, "foo", txn_root, "bar", iterpool)); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, iterpool)); + SVN_TEST_ASSERT(rev == 2); + svn_pool_clear(iterpool); + + /* Rev 3: modify both files. + * The new contents differs for both files but has the same length and MD5. + */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, rev, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "foo", evil_text1, iterpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "bar", evil_text2, iterpool)); + SVN_ERR(svn_fs_change_node_prop(txn_root, "foo", "prop", + svn_string_create(evil_text1, iterpool), + iterpool)); + SVN_ERR(svn_fs_change_node_prop(txn_root, "bar", "prop", + svn_string_create(evil_text2, iterpool), + iterpool)); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, iterpool)); + SVN_TEST_ASSERT(rev == 3); + svn_pool_clear(iterpool); + + /* Rev 4: revert both file contents. + */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, rev, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "foo", original, iterpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "bar", original, iterpool)); + SVN_ERR(svn_fs_change_node_prop(txn_root, "foo", "prop", + svn_string_create(original, iterpool), + iterpool)); + SVN_ERR(svn_fs_change_node_prop(txn_root, "bar", "prop", + svn_string_create(original, iterpool), + iterpool)); + SVN_ERR(svn_fs_commit_txn(NULL, &rev, txn, iterpool)); + SVN_TEST_ASSERT(rev == 4); + svn_pool_clear(iterpool); + + /* Perform all comparisons listed in TO_COMPARE. */ + for (i = 0; to_compare[i].rev1 > 0; ++i) + { + svn_boolean_t text_different; + svn_boolean_t text_changed; + svn_boolean_t props_different; + svn_boolean_t props_changed; + + svn_pool_clear(iterpool); + SVN_ERR(svn_fs_revision_root(&root1, fs, to_compare[i].rev1, iterpool)); + SVN_ERR(svn_fs_revision_root(&root2, fs, to_compare[i].rev2, iterpool)); + + /* Compare node texts. */ + SVN_ERR(svn_fs_contents_different(&text_different, + root1, to_compare[i].path1, + root2, to_compare[i].path2, + iterpool)); + SVN_ERR(svn_fs_contents_changed(&text_changed, + root1, to_compare[i].path1, + root2, to_compare[i].path2, + iterpool)); + + /* Compare properties. */ + SVN_ERR(svn_fs_props_different(&props_different, + root1, to_compare[i].path1, + root2, to_compare[i].path2, + iterpool)); + SVN_ERR(svn_fs_props_changed(&props_changed, + root1, to_compare[i].path1, + root2, to_compare[i].path2, + iterpool)); + + /* Check results. */ + SVN_TEST_ASSERT(text_different == to_compare[i].different); + SVN_TEST_ASSERT(props_different == to_compare[i].different); + + switch (to_compare[i].changed) + { + case svn_tristate_true: + SVN_TEST_ASSERT(text_changed); + SVN_TEST_ASSERT(props_changed); + break; + + case svn_tristate_false: + SVN_TEST_ASSERT(!text_changed); + SVN_TEST_ASSERT(!props_changed); + break; + + default: + break; + } + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_path_change_create(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_root_t *root; + const svn_fs_id_t *id; + svn_fs_path_change2_t *change; + + /* Build an empty test repo ... */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-path-change-create", + opts, pool)); + + /* ... just to give us a valid ID. */ + SVN_ERR(svn_fs_revision_root(&root, fs, 0, pool)); + SVN_ERR(svn_fs_node_id(&id, root, "", pool)); + + /* Do what we came here for. */ + change = svn_fs_path_change2_create(id, svn_fs_path_change_replace, pool); + + SVN_TEST_ASSERT(change); + SVN_TEST_ASSERT(change->node_rev_id == id); + SVN_TEST_ASSERT(change->change_kind == svn_fs_path_change_replace); + + /* All other fields should be "empty" / "unused". */ + SVN_TEST_ASSERT(change->node_kind == svn_node_none); + + SVN_TEST_ASSERT(change->text_mod == FALSE); + SVN_TEST_ASSERT(change->prop_mod == FALSE); + SVN_TEST_ASSERT(change->mergeinfo_mod == svn_tristate_unknown); + + SVN_TEST_ASSERT(change->copyfrom_known == FALSE); + SVN_TEST_ASSERT(change->copyfrom_rev == SVN_INVALID_REVNUM); + SVN_TEST_ASSERT(change->copyfrom_path == NULL); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_node_created_info(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root, *root; + svn_revnum_t rev; + int i; + apr_pool_t *iterpool = svn_pool_create(pool); + + /* Test vectors. */ + struct + { + svn_revnum_t rev; + const char *path; + svn_revnum_t crev; + const char *cpath; + } to_check[] = + { + /* New noderev only upon modification. */ + { 1, "A/B/E/beta", 1, "/A/B/E/beta" }, + { 2, "A/B/E/beta", 1, "/A/B/E/beta" }, + { 3, "A/B/E/beta", 3, "/A/B/E/beta" }, + { 4, "A/B/E/beta", 3, "/A/B/E/beta" }, + + /* Lazily copied node. */ + { 2, "Z/B/E/beta", 1, "/A/B/E/beta" }, + { 3, "Z/B/E/beta", 1, "/A/B/E/beta" }, + { 4, "Z/B/E/beta", 4, "/Z/B/E/beta" }, + + /* Bubble-up upon sub-tree change. */ + { 2, "Z", 2, "/Z" }, + { 3, "Z", 2, "/Z" }, + { 4, "Z", 4, "/Z" }, + + { 0 } + }; + + /* Start with a new repo and the greek tree in rev 1. */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-node-created-path", + opts, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_test__create_greek_tree(txn_root, iterpool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, iterpool)); + svn_pool_clear(iterpool); + + /* r2: copy a subtree */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, rev, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_fs_revision_root(&root, fs, rev, iterpool)); + SVN_ERR(svn_fs_copy(root, "A", txn_root, "Z", iterpool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, iterpool)); + svn_pool_clear(iterpool); + + /* r3: touch node in copy source */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, rev, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "A/B/E/beta", "new", iterpool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, iterpool)); + svn_pool_clear(iterpool); + + /* r4: touch same relative node in copy target */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, rev, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "Z/B/E/beta", "new", iterpool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, iterpool)); + svn_pool_clear(iterpool); + + /* Now ask for some 'node created' info. */ + for (i = 0; to_check[i].rev > 0; ++i) + { + svn_revnum_t crev; + const char *cpath; + + svn_pool_clear(iterpool); + + /* Get created path and rev. */ + SVN_ERR(svn_fs_revision_root(&root, fs, to_check[i].rev, iterpool)); + SVN_ERR(svn_fs_node_created_path(&cpath, root, to_check[i].path, + iterpool)); + SVN_ERR(svn_fs_node_created_rev(&crev, root, to_check[i].path, + iterpool)); + + /* Compare the results with our expectations. */ + SVN_TEST_STRING_ASSERT(cpath, to_check[i].cpath); + + if (crev != to_check[i].crev) + return svn_error_createf(SVN_ERR_TEST_FAILED, NULL, + "created rev mismatch for %s@%ld:\n" + " expected '%ld'\n" + " found '%ld", + to_check[i].path, + to_check[i].rev, + to_check[i].crev, + crev); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_print_modules(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + const char *expected, *module_name; + svn_stringbuf_t *modules = svn_stringbuf_create_empty(pool); + + /* Name of the providing module */ + if (strcmp(opts->fs_type, "fsx") == 0) + module_name = "fs_x"; + else if (strcmp(opts->fs_type, "fsfs") == 0) + module_name = "fs_fs"; + else if (strcmp(opts->fs_type, "bdb") == 0) + module_name = "fs_base"; + else + return svn_error_createf(SVN_ERR_TEST_SKIPPED, NULL, + "don't know the module name for %s", + opts->fs_type); + + SVN_ERR(svn_fs_print_modules(modules, pool)); + + /* The requested FS type must be listed amongst the available modules. */ + expected = apr_psprintf(pool, "* %s : ", module_name); + SVN_TEST_ASSERT(strstr(modules->data, expected)); + + return SVN_NO_ERROR; +} + +/* Baton to be used with process_file_contents. */ +typedef struct process_file_contents_baton_t +{ + const char *contents; + svn_boolean_t processed; +} process_file_contents_baton_t; + +/* Implements svn_fs_process_contents_func_t. + * We flag the BATON as "processed" and compare the CONTENTS we've got to + * what we expect through the BATON. + */ +static svn_error_t * +process_file_contents(const unsigned char *contents, + apr_size_t len, + void *baton, + apr_pool_t *scratch_pool) +{ + process_file_contents_baton_t *b = baton; + + SVN_TEST_ASSERT(strlen(b->contents) == len); + SVN_TEST_ASSERT(memcmp(b->contents, contents, len) == 0); + b->processed = TRUE; - /* Validate the DAG. */ - SVN_ERR(svn_fs_revision_root(&root, fs, after_rev, pool)); - SVN_ERR(svn_test__validate_tree(root, expected_entries, 4, pool)); + return SVN_NO_ERROR; +} + +static svn_error_t * +test_zero_copy_processsing(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root, *root; + svn_revnum_t rev; + const struct svn_test__tree_entry_t *node; + apr_pool_t *iterpool = svn_pool_create(pool); + + /* Start with a new repo and the greek tree in rev 1. */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-zero-copy-processing", + opts, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, iterpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, iterpool)); + SVN_ERR(svn_test__create_greek_tree(txn_root, iterpool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, iterpool)); + svn_pool_clear(iterpool); + + SVN_ERR(svn_fs_revision_root(&root, fs, rev, pool)); + + /* Prime the full-text cache by reading all file contents. */ + for (node = svn_test__greek_tree_nodes; node->path; node++) + if (node->contents) + { + svn_stream_t *stream; + svn_pool_clear(iterpool); + + SVN_ERR(svn_fs_file_contents(&stream, root, node->path, iterpool)); + SVN_ERR(svn_stream_copy3(stream, svn_stream_buffered(iterpool), + NULL, NULL, iterpool)); + } + + /* Now, try to get the data directly from cache + * (if the backend has caches). */ + for (node = svn_test__greek_tree_nodes; node->path; node++) + if (node->contents) + { + svn_boolean_t success; + + process_file_contents_baton_t baton; + baton.contents = node->contents; + baton.processed = FALSE; + + svn_pool_clear(iterpool); + + SVN_ERR(svn_fs_try_process_file_contents(&success, root, node->path, + process_file_contents, &baton, + iterpool)); + SVN_TEST_ASSERT(success == baton.processed); + } + + svn_pool_destroy(iterpool); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_dir_optimal_order(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root, *root; + svn_revnum_t rev; + apr_hash_t *unordered; + apr_array_header_t *ordered; + int i; + apr_hash_index_t *hi; + + /* Create a new repo and the greek tree in rev 1. */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-dir-optimal-order", + opts, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_test__create_greek_tree(txn_root, pool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, pool)); + + SVN_ERR(svn_fs_revision_root(&root, fs, rev, pool)); + + /* Call the API function we are interested in. */ + SVN_ERR(svn_fs_dir_entries(&unordered, root, "A", pool)); + SVN_ERR(svn_fs_dir_optimal_order(&ordered, root, unordered, pool, pool)); + + /* Verify that all entries are returned. */ + SVN_TEST_ASSERT(ordered->nelts == apr_hash_count(unordered)); + for (hi = apr_hash_first(pool, unordered); hi; hi = apr_hash_next(hi)) + { + svn_boolean_t found = FALSE; + const char *name = apr_hash_this_key(hi); + + /* Compare hash -> array because the array might contain the same + * entry more than once. Since that can't happen in the hash, doing + * it in this direction ensures ORDERED won't contain duplicates. + */ + for (i = 0; !found && i < ordered->nelts; ++i) + { + svn_fs_dirent_t *item = APR_ARRAY_IDX(ordered, i, svn_fs_dirent_t*); + if (strcmp(item->name, name) == 0) + { + found = TRUE; + SVN_TEST_ASSERT(item == apr_hash_this_val(hi)); + } + } - /* Validate changed-paths, where the problem originally occurred. */ - for (i = 0; expected_changed_paths[i]; i++) - svn_hash_sets(expected_changes, expected_changed_paths[i], - "undefined value"); - SVN_ERR(svn_test__validate_changes(root, expected_changes, pool)); + SVN_TEST_ASSERT(found); } return SVN_NO_ERROR; } +static svn_error_t * +test_config_files(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + apr_array_header_t *files; + int i; + const char *repo_name = "test-repo-config-files"; + + /* Create a empty and get its config files. */ + SVN_ERR(svn_test__create_fs(&fs, repo_name, opts, pool)); + SVN_ERR(svn_fs_info_config_files(&files, fs, pool, pool)); + + /* All files should exist and be below the repo. */ + for (i = 0; i < files->nelts; ++i) + { + svn_node_kind_t kind; + const char *path = APR_ARRAY_IDX(files, i, const char*); + + SVN_ERR(svn_io_check_path(path, &kind, pool)); + + SVN_TEST_ASSERT(kind == svn_node_file); + SVN_TEST_ASSERT(svn_dirent_is_ancestor(repo_name, path)); + } + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_delta_file_stream(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root, *root1, *root2; + svn_revnum_t rev; + + const char *old_content = "some content"; + const char *new_content = "some more content"; + svn_txdelta_window_handler_t delta_handler; + void *delta_baton; + svn_txdelta_stream_t *delta_stream; + svn_stringbuf_t *source = svn_stringbuf_create_empty(pool); + svn_stringbuf_t *dest = svn_stringbuf_create_empty(pool); + + /* Create a new repo. */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-delta-file-stream", + opts, pool)); + + /* Revision 1: create a file. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_file(txn_root, "foo", pool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "foo", old_content, pool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, pool)); + + /* Revision 2: create a file. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, rev, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "foo", new_content, pool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, pool)); + + SVN_ERR(svn_fs_revision_root(&root1, fs, 1, pool)); + SVN_ERR(svn_fs_revision_root(&root2, fs, 2, pool)); + + /* Test 1: Get delta against empty target. */ + SVN_ERR(svn_fs_get_file_delta_stream(&delta_stream, + NULL, NULL, root1, "foo", pool)); + + svn_stringbuf_setempty(source); + svn_stringbuf_setempty(dest); + + svn_txdelta_apply(svn_stream_from_stringbuf(source, pool), + svn_stream_from_stringbuf(dest, pool), + NULL, NULL, pool, &delta_handler, &delta_baton); + SVN_ERR(svn_txdelta_send_txstream(delta_stream, + delta_handler, + delta_baton, + pool)); + SVN_TEST_STRING_ASSERT(old_content, dest->data); + + /* Test 2: Get delta against previous version. */ + SVN_ERR(svn_fs_get_file_delta_stream(&delta_stream, + root1, "foo", root2, "foo", pool)); + + svn_stringbuf_set(source, old_content); + svn_stringbuf_setempty(dest); + + svn_txdelta_apply(svn_stream_from_stringbuf(source, pool), + svn_stream_from_stringbuf(dest, pool), + NULL, NULL, pool, &delta_handler, &delta_baton); + SVN_ERR(svn_txdelta_send_txstream(delta_stream, + delta_handler, + delta_baton, + pool)); + SVN_TEST_STRING_ASSERT(new_content, dest->data); + + /* Test 3: Get reverse delta. */ + SVN_ERR(svn_fs_get_file_delta_stream(&delta_stream, + root2, "foo", root1, "foo", pool)); + + svn_stringbuf_set(source, new_content); + svn_stringbuf_setempty(dest); + + svn_txdelta_apply(svn_stream_from_stringbuf(source, pool), + svn_stream_from_stringbuf(dest, pool), + NULL, NULL, pool, &delta_handler, &delta_baton); + SVN_ERR(svn_txdelta_send_txstream(delta_stream, + delta_handler, + delta_baton, + pool)); + SVN_TEST_STRING_ASSERT(old_content, dest->data); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_fs_merge(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root, *root0, *root1; + svn_revnum_t rev; + + /* Very basic test for svn_fs_merge because all the other interesting + * cases get tested implicitly with concurrent txn / commit tests. + * This API is just a thin layer around the internal merge function + * and we simply check that the plumbing between them works. + */ + + /* Create a new repo. */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-fs-merge", + opts, pool)); + SVN_ERR(svn_fs_revision_root(&root0, fs, 0, pool)); + + /* Revision 1: create a file. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_file(txn_root, "foo", pool)); + SVN_ERR(test_commit_txn(&rev, txn, NULL, pool)); + SVN_ERR(svn_fs_revision_root(&root1, fs, rev, pool)); + + /* Merge-able txn: create another file. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_file(txn_root, "bar", pool)); + + SVN_ERR(svn_fs_merge(NULL, root1, "/", txn_root, "/", root0, "/", pool)); + + /* Not merge-able: create the same file file. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_file(txn_root, "foo", pool)); + + SVN_TEST_ASSERT_ERROR(svn_fs_merge(NULL, root1, "/", txn_root, "/", root0, + "/", pool), SVN_ERR_FS_CONFLICT); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_fsfs_config_opts(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + apr_hash_t *fs_config; + svn_fs_t *fs; + const svn_fs_info_placeholder_t *fs_info; + const svn_fs_fsfs_info_t *fsfs_info; + + /* Bail (with SKIP) on known-untestable scenarios */ + if (strcmp(opts->fs_type, "fsfs") != 0) + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, + "this will test FSFS repositories only"); + + /* Remove the test directory from previous runs. */ + SVN_ERR(svn_io_remove_dir2("test-fsfs-config-opts", TRUE, NULL, NULL, pool)); + + /* Create the test directory and add it to the test cleanup list. */ + SVN_ERR(svn_io_dir_make("test-fsfs-config-opts", APR_OS_DEFAULT, pool)); + svn_test_add_dir_cleanup("test-fsfs-config-opts"); + + /* Create an FSFS filesystem with default config.*/ + fs_config = apr_hash_make(pool); + svn_hash_sets(fs_config, SVN_FS_CONFIG_FS_TYPE, SVN_FS_TYPE_FSFS); + SVN_ERR(svn_fs_create(&fs, "test-fsfs-config-opts/default", fs_config, pool)); + + /* Re-open FS to test the data on disk. */ + SVN_ERR(svn_fs_open2(&fs, "test-fsfs-config-opts/default", NULL, pool, pool)); + + SVN_ERR(svn_fs_info(&fs_info, fs, pool, pool)); + SVN_TEST_STRING_ASSERT(fs_info->fs_type, SVN_FS_TYPE_FSFS); + fsfs_info = (const void *) fs_info; + + /* Check FSFS specific info. Don't check the SHARD_SIZE, because it depends + * on a compile-time constant and may be overridden. */ + SVN_TEST_ASSERT(fsfs_info->log_addressing); + SVN_TEST_ASSERT(fsfs_info->min_unpacked_rev == 0); + + /* Create an FSFS filesystem with custom settings: disabled log-addressing + * and custom shard size (123). */ + fs_config = apr_hash_make(pool); + svn_hash_sets(fs_config, SVN_FS_CONFIG_FS_TYPE, SVN_FS_TYPE_FSFS); + svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_LOG_ADDRESSING, "false"); + svn_hash_sets(fs_config, SVN_FS_CONFIG_FSFS_SHARD_SIZE, "123"); + SVN_ERR(svn_fs_create(&fs, "test-fsfs-config-opts/custom", fs_config, pool)); + + /* Re-open FS to test the data on disk. */ + SVN_ERR(svn_fs_open2(&fs, "test-fsfs-config-opts/custom", NULL, pool, pool)); + + SVN_ERR(svn_fs_info(&fs_info, fs, pool, pool)); + SVN_TEST_STRING_ASSERT(fs_info->fs_type, SVN_FS_TYPE_FSFS); + fsfs_info = (const void *) fs_info; + + /* Check FSFS specific info, including the SHARD_SIZE. */ + SVN_TEST_ASSERT(fsfs_info->log_addressing == FALSE); + SVN_TEST_ASSERT(fsfs_info->shard_size == 123); + SVN_TEST_ASSERT(fsfs_info->min_unpacked_rev == 0); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_txn_pool_lifetime(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + /* Technically, the FS API makes no assumption on the lifetime of logically + * dependent objects. In particular, a txn root object may get destroyed + * after the FS object that it has been built upon. Actual data access is + * implied to be invalid without a valid svn_fs_t. + * + * This test verifies that at least the destruction order of those two + * objects is arbitrary. + */ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root; + + /* We will allocate FS in FS_POOL. Using a separate allocator makes + * sure that we actually free the memory when destroying the pool. + */ + apr_allocator_t *fs_allocator = svn_pool_create_allocator(FALSE); + apr_pool_t *fs_pool = apr_allocator_owner_get(fs_allocator); + + /* Create a new repo. */ + SVN_ERR(svn_test__create_fs(&fs, "test-repo-pool-lifetime", + opts, fs_pool)); + + /* Create a TXN_ROOT referencing FS. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + + /* Destroy FS. Depending on the actual allocator implementation, + * these memory pages becomes inaccessible. */ + svn_pool_destroy(fs_pool); + + /* Unclean implementations will try to access FS and may segfault here. */ + svn_fs_close_root(txn_root); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_modify_txn_being_written(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + /* FSFS has a limitation (and check) that only one file can be + * modified in TXN at time: see r861812 and svn_fs_apply_text() docstring. + * This is regression test for this behavior. */ + svn_fs_t *fs; + svn_fs_txn_t *txn; + const char *txn_name; + svn_fs_root_t *txn_root; + svn_stream_t *foo_contents; + svn_stream_t *bar_contents; + + /* Bail (with success) on known-untestable scenarios */ + if (strcmp(opts->fs_type, SVN_FS_TYPE_FSFS) != 0) + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, + "this will test FSFS repositories only"); + + /* Create a new repo. */ + SVN_ERR(svn_test__create_fs(&fs, "test-modify-txn-being-written", + opts, pool)); + + /* Create a TXN_ROOT referencing FS. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_name(&txn_name, txn, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + + /* Make file /foo and open for writing.*/ + SVN_ERR(svn_fs_make_file(txn_root, "/foo", pool)); + SVN_ERR(svn_fs_apply_text(&foo_contents, txn_root, "/foo", NULL, pool)); + + /* Attempt to modify another file '/bar' -- FSFS doesn't allow this. */ + SVN_ERR(svn_fs_make_file(txn_root, "/bar", pool)); + SVN_TEST_ASSERT_ERROR( + svn_fs_apply_text(&bar_contents, txn_root, "/bar", NULL, pool), + SVN_ERR_FS_REP_BEING_WRITTEN); + + /* *Reopen TXN. */ + SVN_ERR(svn_fs_open_txn(&txn, fs, txn_name, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + + /* Check that file '/bar' still cannot be modified */ + SVN_TEST_ASSERT_ERROR( + svn_fs_apply_text(&bar_contents, txn_root, "/bar", NULL, pool), + SVN_ERR_FS_REP_BEING_WRITTEN); + + /* Close file '/foo'. */ + SVN_ERR(svn_stream_close(foo_contents)); + + /* Now file '/bar' can be modified. */ + SVN_ERR(svn_fs_apply_text(&bar_contents, txn_root, "/bar", NULL, pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_prop_and_text_rep_sharing_collision(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + /* Regression test for issue 4554: Wrong file length with PLAIN + * representations in FSFS. */ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root; + svn_fs_root_t *rev_root; + svn_revnum_t new_rev; + svn_filesize_t length; + const char *testdir = "test-prop-and-text-rep-sharing-collision"; + + /* Create a new repo. */ + SVN_ERR(svn_test__create_fs(&fs, testdir, opts, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + /* Set node property for the root. */ + SVN_ERR(svn_fs_change_node_prop(txn_root, "/", "prop", + svn_string_create("value", pool), + pool)); + + /* Commit revision r1. */ + SVN_ERR(test_commit_txn(&new_rev, txn, NULL, pool)); + + SVN_ERR(svn_fs_begin_txn(&txn, fs, 1, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + + /* Create file with same contents as property representation. */ + SVN_ERR(svn_fs_make_file(txn_root, "/foo", pool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "/foo", + "K 4\n" + "prop\n" + "V 5\n" + "value\n" + "END\n", pool)); + + /* Commit revision r2. */ + SVN_ERR(test_commit_txn(&new_rev, txn, NULL, pool)); + + /* Check that FS reports correct length for the file (23). */ + SVN_ERR(svn_fs_revision_root(&rev_root, fs, 2, pool)); + SVN_ERR(svn_fs_file_length(&length, rev_root, "/foo", pool)); + + SVN_TEST_ASSERT(length == 23); + return SVN_NO_ERROR; +} + +static svn_error_t * +test_internal_txn_props(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_string_t *val; + svn_prop_t prop; + svn_prop_t internal_prop; + apr_array_header_t *props; + apr_hash_t *proplist; + svn_error_t *err; + + SVN_ERR(svn_test__create_fs(&fs, "test-repo-internal-txn-props", + opts, pool)); + SVN_ERR(svn_fs_begin_txn2(&txn, fs, 0, + SVN_FS_TXN_CHECK_LOCKS | + SVN_FS_TXN_CHECK_OOD | + SVN_FS_TXN_CLIENT_DATE, pool)); + + /* Ensure that we cannot read internal transaction properties. */ + SVN_ERR(svn_fs_txn_prop(&val, txn, SVN_FS__PROP_TXN_CHECK_LOCKS, pool)); + SVN_TEST_ASSERT(!val); + SVN_ERR(svn_fs_txn_prop(&val, txn, SVN_FS__PROP_TXN_CHECK_OOD, pool)); + SVN_TEST_ASSERT(!val); + SVN_ERR(svn_fs_txn_prop(&val, txn, SVN_FS__PROP_TXN_CLIENT_DATE, pool)); + SVN_TEST_ASSERT(!val); + + SVN_ERR(svn_fs_txn_proplist(&proplist, txn, pool)); + SVN_TEST_ASSERT(apr_hash_count(proplist) == 1); + val = svn_hash_gets(proplist, SVN_PROP_REVISION_DATE); + SVN_TEST_ASSERT(val); + + /* We also cannot change or discard them. */ + val = svn_string_create("Ooops!", pool); + + err = svn_fs_change_txn_prop(txn, SVN_FS__PROP_TXN_CHECK_LOCKS, val, pool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_INCORRECT_PARAMS); + err = svn_fs_change_txn_prop(txn, SVN_FS__PROP_TXN_CHECK_LOCKS, NULL, pool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_INCORRECT_PARAMS); + err = svn_fs_change_txn_prop(txn, SVN_FS__PROP_TXN_CHECK_OOD, val, pool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_INCORRECT_PARAMS); + err = svn_fs_change_txn_prop(txn, SVN_FS__PROP_TXN_CHECK_OOD, NULL, pool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_INCORRECT_PARAMS); + err = svn_fs_change_txn_prop(txn, SVN_FS__PROP_TXN_CLIENT_DATE, val, pool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_INCORRECT_PARAMS); + err = svn_fs_change_txn_prop(txn, SVN_FS__PROP_TXN_CLIENT_DATE, NULL, pool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_INCORRECT_PARAMS); + + prop.name = "foo"; + prop.value = svn_string_create("bar", pool); + internal_prop.name = SVN_FS__PROP_TXN_CHECK_LOCKS; + internal_prop.value = svn_string_create("Ooops!", pool); + + props = apr_array_make(pool, 2, sizeof(svn_prop_t)); + APR_ARRAY_PUSH(props, svn_prop_t) = prop; + APR_ARRAY_PUSH(props, svn_prop_t) = internal_prop; + + err = svn_fs_change_txn_props(txn, props, pool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_INCORRECT_PARAMS); + + return SVN_NO_ERROR; +} + +/* A freeze function that expects an 'svn_error_t *' baton, and returns it. */ +/* This function implements svn_fs_freeze_func_t. */ +static svn_error_t * +freeze_func(void *baton, apr_pool_t *pool) +{ + return baton; +} + +static svn_error_t * +freeze_and_commit(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root; + svn_revnum_t new_rev = 0; + apr_pool_t *subpool = svn_pool_create(pool); + + if (!strcmp(opts->fs_type, "bdb")) + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, + "this will not test BDB repositories"); + + SVN_ERR(svn_test__create_fs(&fs, "test-freeze-and-commit", opts, subpool)); + + /* This test used to FAIL with an SQLite error since svn_fs_freeze() + * wouldn't unlock rep-cache.db. Therefore, part of the role of creating + * the Greek tree is to create a rep-cache.db, in order to test that + * svn_fs_freeze() unlocks it. */ + + /* r1: Commit the Greek tree. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, new_rev, subpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool)); + SVN_ERR(svn_test__create_greek_tree(txn_root, subpool)); + SVN_ERR(test_commit_txn(&new_rev, txn, NULL, subpool)); + + /* Freeze and unfreeze. */ + SVN_ERR(svn_fs_freeze(fs, freeze_func, SVN_NO_ERROR, pool)); + + /* Freeze again, but have freeze_func fail. */ + { + svn_error_t *err = svn_error_create(APR_EGENERAL, NULL, NULL); + SVN_TEST_ASSERT_ERROR(svn_fs_freeze(fs, freeze_func, err, pool), + err->apr_err); + } + + /* Make some commit using same FS instance. */ + SVN_ERR(svn_fs_begin_txn(&txn, fs, new_rev, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_change_node_prop(txn_root, "/", "temperature", + svn_string_create("310.05", pool), + pool)); + SVN_ERR(test_commit_txn(&new_rev, txn, NULL, pool)); + + /* Re-open FS and make another commit. */ + SVN_ERR(svn_fs_open(&fs, "test-freeze-and-commit", NULL, subpool)); + SVN_ERR(svn_fs_begin_txn(&txn, fs, new_rev, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_change_node_prop(txn_root, "/", "temperature", + svn_string_create("451", pool), + pool)); + SVN_ERR(test_commit_txn(&new_rev, txn, NULL, pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +commit_with_locked_rep_cache(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root; + svn_revnum_t new_rev; + svn_sqlite__db_t *sdb; + svn_error_t *err; + const char *fs_path; + const char *statements[] = { "SELECT MAX(revision) FROM rep_cache", NULL }; + + if (strcmp(opts->fs_type, SVN_FS_TYPE_FSFS) != 0) + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, + "this will test FSFS repositories only"); + + if (opts->server_minor_version && (opts->server_minor_version < 6)) + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, + "pre-1.6 SVN doesn't support FSFS rep-sharing"); + + fs_path = "test-repo-commit-with-locked-rep-cache"; + SVN_ERR(svn_test__create_fs(&fs, fs_path, opts, pool)); + + /* r1: Add a file. */ + SVN_ERR(svn_fs_begin_txn2(&txn, fs, 0, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_fs_make_file(txn_root, "/foo", pool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "/foo", "a", pool)); + SVN_ERR(test_commit_txn(&new_rev, txn, NULL, pool)); + SVN_TEST_INT_ASSERT(new_rev, 1); + + /* Begin a new transaction based on r1. */ + SVN_ERR(svn_fs_begin_txn2(&txn, fs, 1, 0, pool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "/foo", "b", pool)); + + /* Obtain a shared lock on the rep-cache.db by starting a new read + * transaction. */ + SVN_ERR(svn_sqlite__open(&sdb, + svn_dirent_join(fs_path, "rep-cache.db", pool), + svn_sqlite__mode_readonly, statements, 0, NULL, + 0, pool, pool)); + SVN_ERR(svn_sqlite__begin_transaction(sdb)); + SVN_ERR(svn_sqlite__exec_statements(sdb, 0)); + + /* Attempt to commit fs transaction. This should result in a commit + * post-processing error due to us still holding the shared lock on the + * rep-cache.db. */ + err = svn_fs_commit_txn(NULL, &new_rev, txn, pool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_SQLITE_BUSY); + SVN_TEST_INT_ASSERT(new_rev, 2); + + /* Release the shared lock. */ + SVN_ERR(svn_sqlite__finish_transaction(sdb, SVN_NO_ERROR)); + SVN_ERR(svn_sqlite__close(sdb)); + + /* Try an operation that reads from rep-cache.db. + * + * XFAIL: Around r1740802, this call was producing an error due to the + * svn_fs_t keeping an unusable db connection (and associated file + * locks) within it. + */ + SVN_ERR(svn_fs_verify(fs_path, NULL, 0, SVN_INVALID_REVNUM, NULL, NULL, + NULL, NULL, pool)); + + return SVN_NO_ERROR; +} + +static svn_error_t * +test_rep_sharing_strict_content_check(const svn_test_opts_t *opts, + apr_pool_t *pool) +{ + svn_fs_t *fs; + svn_fs_txn_t *txn; + svn_fs_root_t *txn_root; + svn_revnum_t new_rev; + const char *fs_path, *fs_path2; + apr_pool_t *subpool = svn_pool_create(pool); + svn_error_t *err; + + /* Bail (with success) on known-untestable scenarios */ + if (strcmp(opts->fs_type, SVN_FS_TYPE_BDB) == 0) + return svn_error_create(SVN_ERR_TEST_SKIPPED, NULL, + "BDB repositories don't support rep-sharing"); + + /* Create 2 repos with same structure & size but different contents */ + fs_path = "test-rep-sharing-strict-content-check1"; + fs_path2 = "test-rep-sharing-strict-content-check2"; + + SVN_ERR(svn_test__create_fs(&fs, fs_path, opts, subpool)); + + SVN_ERR(svn_fs_begin_txn2(&txn, fs, 0, 0, subpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool)); + SVN_ERR(svn_fs_make_file(txn_root, "/foo", subpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "foo", "quite bad", subpool)); + SVN_ERR(test_commit_txn(&new_rev, txn, NULL, subpool)); + SVN_TEST_INT_ASSERT(new_rev, 1); + + SVN_ERR(svn_test__create_fs(&fs, fs_path2, opts, subpool)); + + SVN_ERR(svn_fs_begin_txn2(&txn, fs, 0, 0, subpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool)); + SVN_ERR(svn_fs_make_file(txn_root, "foo", subpool)); + SVN_ERR(svn_test__set_file_contents(txn_root, "foo", "very good", subpool)); + SVN_ERR(test_commit_txn(&new_rev, txn, NULL, subpool)); + SVN_TEST_INT_ASSERT(new_rev, 1); + + /* Close both repositories. */ + svn_pool_clear(subpool); + + /* Doctor the first repo such that it uses the wrong rep-cache. */ + SVN_ERR(svn_io_copy_file(svn_relpath_join(fs_path2, "rep-cache.db", pool), + svn_relpath_join(fs_path, "rep-cache.db", pool), + FALSE, pool)); + + /* Changing the file contents such that rep-sharing would kick in if + the file contents was not properly compared. */ + SVN_ERR(svn_fs_open2(&fs, fs_path, NULL, subpool, subpool)); + + SVN_ERR(svn_fs_begin_txn2(&txn, fs, 1, 0, subpool)); + SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool)); + err = svn_test__set_file_contents(txn_root, "foo", "very good", subpool); + SVN_TEST_ASSERT_ERROR(err, SVN_ERR_FS_GENERAL); + + svn_pool_destroy(subpool); + + return SVN_NO_ERROR; +} + /* ------------------------------------------------------------------------ */ /* The test table. */ -struct svn_test_descriptor_t test_funcs[] = +static int max_threads = 8; + +static struct svn_test_descriptor_t test_funcs[] = { SVN_TEST_NULL, SVN_TEST_OPTS_PASS(trivial_transaction, @@ -5048,6 +7188,12 @@ struct svn_test_descriptor_t test_funcs[] = "check that transaction names are not reused"), SVN_TEST_OPTS_PASS(write_and_read_file, "write and read a file's contents"), + SVN_TEST_OPTS_PASS(almostmedium_file_integrity, + "create and modify almostmedium file"), + SVN_TEST_OPTS_PASS(medium_file_integrity, + "create and modify medium file"), + SVN_TEST_OPTS_PASS(large_file_integrity, + "create and modify large file"), SVN_TEST_OPTS_PASS(create_mini_tree_transaction, "test basic file and subdirectory creation"), SVN_TEST_OPTS_PASS(create_greek_tree_transaction, @@ -5079,10 +7225,6 @@ struct svn_test_descriptor_t test_funcs[] = "check old revisions"), SVN_TEST_OPTS_PASS(check_all_revisions, "after each commit, check all revisions"), - SVN_TEST_OPTS_PASS(medium_file_integrity, - "create and modify medium file"), - SVN_TEST_OPTS_PASS(large_file_integrity, - "create and modify large file"), SVN_TEST_OPTS_PASS(check_root_revision, "ensure accurate storage of root node"), SVN_TEST_OPTS_PASS(test_node_created_rev, @@ -5111,5 +7253,63 @@ struct svn_test_descriptor_t test_funcs[] = "test svn_fs_delete_fs"), SVN_TEST_OPTS_PASS(filename_trailing_newline, "filenames with trailing \\n might be rejected"), + SVN_TEST_OPTS_PASS(test_fs_info_format, + "test svn_fs_info_format"), + SVN_TEST_OPTS_PASS(commit_timestamp, + "commit timestamp"), + SVN_TEST_OPTS_PASS(test_compat_version, + "test svn_fs__compatible_version"), + SVN_TEST_OPTS_PASS(dir_prop_merge, + "test merge directory properties"), + SVN_TEST_OPTS_XFAIL_OTOH(reopen_modify, + "test reopen and modify txn", + SVN_TEST_PASS_IF_FS_TYPE_IS_NOT("fsfs")), + SVN_TEST_OPTS_PASS(upgrade_while_committing, + "upgrade while committing"), + SVN_TEST_OPTS_PASS(test_paths_changed, + "test svn_fs_paths_changed"), + SVN_TEST_OPTS_PASS(test_delete_replaced_paths_changed, + "test deletion after replace in changed paths list"), + SVN_TEST_OPTS_PASS(purge_txn_test, + "test purging transactions"), + SVN_TEST_OPTS_PASS(compare_contents, + "compare contents of different nodes"), + SVN_TEST_OPTS_PASS(test_path_change_create, + "test svn_fs_path_change2_create"), + SVN_TEST_OPTS_PASS(test_node_created_info, + "test FS 'node created' info"), + SVN_TEST_OPTS_PASS(test_print_modules, + "test FS module listing"), + SVN_TEST_OPTS_PASS(test_zero_copy_processsing, + "test zero copy file processing"), + SVN_TEST_OPTS_PASS(test_dir_optimal_order, + "test svn_fs_dir_optimal_order"), + SVN_TEST_OPTS_PASS(test_config_files, + "get configuration files"), + SVN_TEST_OPTS_PASS(test_delta_file_stream, + "get a delta stream on a file"), + SVN_TEST_OPTS_PASS(test_fs_merge, + "get merging txns with newer revisions"), + SVN_TEST_OPTS_PASS(test_fsfs_config_opts, + "test creating FSFS repository with different opts"), + SVN_TEST_OPTS_PASS(test_txn_pool_lifetime, + "test pool lifetime dependencies with txn roots"), + SVN_TEST_OPTS_PASS(test_modify_txn_being_written, + "test modify txn being written in FSFS"), + SVN_TEST_OPTS_PASS(test_prop_and_text_rep_sharing_collision, + "test property and text rep-sharing collision"), + SVN_TEST_OPTS_PASS(test_internal_txn_props, + "test setting and getting internal txn props"), + SVN_TEST_OPTS_PASS(check_txn_related, + "test svn_fs_check_related for transactions"), + SVN_TEST_OPTS_PASS(freeze_and_commit, + "freeze and commit"), + SVN_TEST_OPTS_PASS(commit_with_locked_rep_cache, + "test commit with locked rep-cache"), + SVN_TEST_OPTS_XFAIL_OTOH(test_rep_sharing_strict_content_check, + "test rep-sharing on content rather than SHA1", + SVN_TEST_PASS_IF_FS_TYPE_IS(SVN_FS_TYPE_FSFS)), SVN_TEST_NULL }; + +SVN_TEST_MAIN |