/* lock-test.c --- tests for the filesystem locking functions * * ==================================================================== * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * ==================================================================== */ #include #include #include #include "../svn_test.h" #include "svn_error.h" #include "svn_fs.h" #include "svn_hash.h" #include "../svn_test_fs.h" /*-----------------------------------------------------------------*/ /** Helper functions **/ /* Implementations of the svn_fs_get_locks_callback_t interface and baton, for verifying expected output from svn_fs_get_locks(). */ struct get_locks_baton_t { apr_hash_t *locks; }; static svn_error_t * get_locks_callback(void *baton, svn_lock_t *lock, apr_pool_t *pool) { struct get_locks_baton_t *b = baton; apr_pool_t *hash_pool = apr_hash_pool_get(b->locks); svn_string_t *lock_path = svn_string_create(lock->path, hash_pool); if (!apr_hash_get(b->locks, lock_path->data, lock_path->len)) { apr_hash_set(b->locks, lock_path->data, lock_path->len, svn_lock_dup(lock, hash_pool)); return SVN_NO_ERROR; } else { return svn_error_createf(SVN_ERR_TEST_FAILED, NULL, "Lock for path '%s' is being reported twice.", lock->path); } } /* A factory function. */ static struct get_locks_baton_t * make_get_locks_baton(apr_pool_t *pool) { struct get_locks_baton_t *baton = apr_pcalloc(pool, sizeof(*baton)); baton->locks = apr_hash_make(pool); return baton; } /* And verification function(s). */ static svn_error_t * verify_matching_lock_paths(struct get_locks_baton_t *baton, const char *expected_paths[], apr_size_t num_expected_paths, apr_pool_t *pool) { apr_size_t i; if (num_expected_paths != apr_hash_count(baton->locks)) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Unexpected number of locks."); for (i = 0; i < num_expected_paths; i++) { const char *path = expected_paths[i]; if (! apr_hash_get(baton->locks, path, APR_HASH_KEY_STRING)) return svn_error_createf(SVN_ERR_TEST_FAILED, NULL, "Missing lock for path '%s'", path); } return SVN_NO_ERROR; } /* Create a filesystem in a directory called NAME, and populate it with * the standard Greek tree. Set *FS_P to the new filesystem object and * *NEWREV_P to the head revision number. Unwanted outputs may be NULL. */ static svn_error_t * create_greek_fs(svn_fs_t **fs_p, svn_revnum_t *newrev_p, const char *name, 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; const char *conflict; svn_revnum_t newrev; /* Prepare a filesystem and a new txn. */ SVN_ERR(svn_test__create_fs(&fs, name, opts, pool)); SVN_ERR(svn_fs_begin_txn2(&txn, fs, 0, SVN_FS_TXN_CHECK_LOCKS, pool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); /* Create the greek tree and commit it. */ SVN_ERR(svn_test__create_greek_tree(txn_root, pool)); SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(newrev)); if (fs_p) *fs_p = fs; if (newrev_p) *newrev_p = newrev; return SVN_NO_ERROR; } /*-----------------------------------------------------------------*/ /** The actual lock-tests called by `make check` **/ /* Test that we can create a lock--nothing more. */ static svn_error_t * lock_only(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_access_t *access; svn_lock_t *mylock; SVN_ERR(create_greek_fs(&fs, NULL, "test-repo-lock-only", opts, pool)); /* We are now 'bubba'. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Lock /A/D/G/rho. */ SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); return SVN_NO_ERROR; } /* Test that we can create, fetch, and destroy a lock. It exercises each of the five public fs locking functions. */ static svn_error_t * lookup_lock_by_path(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_access_t *access; svn_lock_t *mylock, *somelock; SVN_ERR(create_greek_fs(&fs, NULL, "test-repo-lookup-lock-by-path", opts, pool)); /* We are now 'bubba'. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Lock /A/D/G/rho. */ SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); /* Can we look up the lock by path? */ SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if ((! somelock) || (strcmp(somelock->token, mylock->token) != 0)) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Couldn't look up a lock by pathname."); return SVN_NO_ERROR; } /* Test that we can create a lock outside of the fs and attach it to a path. */ static svn_error_t * attach_lock(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_access_t *access; svn_lock_t *somelock; svn_lock_t *mylock; const char *token; SVN_ERR(create_greek_fs(&fs, NULL, "test-repo-attach-lock", opts, pool)); /* We are now 'bubba'. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); SVN_ERR(svn_fs_generate_lock_token(&token, fs, pool)); SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", token, "This is a comment. Yay comment!", 0, apr_time_now() + apr_time_from_sec(3), SVN_INVALID_REVNUM, FALSE, pool)); /* Can we look up the lock by path? */ SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if ((! somelock) || (strcmp(somelock->token, mylock->token) != 0)) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Couldn't look up a lock by pathname."); /* Unlock /A/D/G/rho, and verify that it's gone. */ SVN_ERR(svn_fs_unlock(fs, mylock->path, mylock->token, 0, pool)); SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if (somelock) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Removed a lock, but it's still there."); return SVN_NO_ERROR; } /* Test that we can get all locks under a directory. */ static svn_error_t * get_locks(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_access_t *access; svn_lock_t *mylock; struct get_locks_baton_t *get_locks_baton; apr_size_t i, num_expected_paths; SVN_ERR(create_greek_fs(&fs, NULL, "test-repo-get-locks", opts, pool)); /* We are now 'bubba'. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Lock our paths; verify from "/". */ { static const char *expected_paths[] = { "/A/D/G/pi", "/A/D/G/rho", "/A/D/G/tau", "/A/D/H/psi", "/A/D/H/chi", "/A/D/H/omega", "/A/B/E/alpha", "/A/B/E/beta", }; num_expected_paths = sizeof(expected_paths) / sizeof(const char *); for (i = 0; i < num_expected_paths; i++) { SVN_ERR(svn_fs_lock(&mylock, fs, expected_paths[i], NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); } get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } /* Verify from "/A/B". */ { static const char *expected_paths[] = { "/A/B/E/alpha", "/A/B/E/beta", }; num_expected_paths = sizeof(expected_paths) / sizeof(const char *); get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "A/B", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } /* Verify from "/A/D". */ { static const char *expected_paths[] = { "/A/D/G/pi", "/A/D/G/rho", "/A/D/G/tau", "/A/D/H/psi", "/A/D/H/chi", "/A/D/H/omega", }; num_expected_paths = sizeof(expected_paths) / sizeof(const char *); get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "A/D", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } /* Verify from "/A/D/G". */ { static const char *expected_paths[] = { "/A/D/G/pi", "/A/D/G/rho", "/A/D/G/tau", }; num_expected_paths = sizeof(expected_paths) / sizeof(const char *); get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "A/D/G", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } /* Verify from "/A/D/H/omega". */ { static const char *expected_paths[] = { "/A/D/H/omega", }; num_expected_paths = sizeof(expected_paths) / sizeof(const char *); get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "A/D/H/omega", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } /* Verify from "/iota" (which wasn't locked... tricky...). */ { static const char *expected_paths[] = { 0 }; num_expected_paths = 0; get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "iota", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } /* A path that is longer and alphabetically earlier than some locked paths, this exercises the r1205848 BDB lock code. */ { static const char *expected_paths[] = { 0 }; num_expected_paths = 0; get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "A/D/H/ABCDEFGHIJKLMNOPQR", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } return SVN_NO_ERROR; } /* Test that we can create, fetch, and destroy a lock. It exercises each of the five public fs locking functions. */ static svn_error_t * basic_lock(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_access_t *access; svn_lock_t *mylock, *somelock; SVN_ERR(create_greek_fs(&fs, NULL, "test-repo-basic-lock", opts, pool)); /* We are now 'bubba'. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Lock /A/D/G/rho. */ SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); /* Can we look up the lock by path? */ SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if ((! somelock) || (strcmp(somelock->token, mylock->token) != 0)) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Couldn't look up a lock by pathname."); /* Unlock /A/D/G/rho, and verify that it's gone. */ SVN_ERR(svn_fs_unlock(fs, mylock->path, mylock->token, 0, pool)); SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if (somelock) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Removed a lock, but it's still there."); return SVN_NO_ERROR; } /* Test that locks are enforced -- specifically that both a username and token are required to make use of the lock. */ static svn_error_t * lock_credentials(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; const char *conflict; svn_revnum_t newrev; svn_fs_access_t *access; svn_lock_t *mylock; svn_error_t *err; SVN_ERR(create_greek_fs(&fs, &newrev, "test-repo-lock-credentials", opts, pool)); /* We are now 'bubba'. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Lock /A/D/G/rho. */ SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); /* Push the proper lock-token into the fs access context. */ SVN_ERR(svn_fs_access_add_lock_token(access, mylock->token)); /* Make a new transaction and change rho. */ SVN_ERR(svn_fs_begin_txn2(&txn, fs, newrev, SVN_FS_TXN_CHECK_LOCKS, pool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_test__set_file_contents(txn_root, "/A/D/G/rho", "new contents", pool)); /* We are no longer 'bubba'. We're nobody. */ SVN_ERR(svn_fs_set_access(fs, NULL)); /* Try to commit the file change. Should fail, because we're nobody. */ err = svn_fs_commit_txn(&conflict, &newrev, txn, pool); SVN_TEST_ASSERT(! SVN_IS_VALID_REVNUM(newrev)); if (! err) return svn_error_create (SVN_ERR_TEST_FAILED, NULL, "Uhoh, able to commit locked file without any fs username."); svn_error_clear(err); /* We are now 'hortense'. */ SVN_ERR(svn_fs_create_access(&access, "hortense", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Try to commit the file change. Should fail, because we're 'hortense'. */ err = svn_fs_commit_txn(&conflict, &newrev, txn, pool); SVN_TEST_ASSERT(! SVN_IS_VALID_REVNUM(newrev)); if (! err) return svn_error_create (SVN_ERR_TEST_FAILED, NULL, "Uhoh, able to commit locked file as non-owner."); svn_error_clear(err); /* Be 'bubba' again. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Try to commit the file change. Should fail, because there's no token. */ err = svn_fs_commit_txn(&conflict, &newrev, txn, pool); SVN_TEST_ASSERT(! SVN_IS_VALID_REVNUM(newrev)); if (! err) return svn_error_create (SVN_ERR_TEST_FAILED, NULL, "Uhoh, able to commit locked file with no lock token."); svn_error_clear(err); /* Push the proper lock-token into the fs access context. */ SVN_ERR(svn_fs_access_add_lock_token(access, mylock->token)); /* Commit should now succeed. */ SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(newrev)); return SVN_NO_ERROR; } /* Test that locks are enforced at commit time. Somebody might lock something behind your back, right before you run svn_fs_commit_txn(). Also, this test verifies that recursive lock-checks on directories is working properly. */ static svn_error_t * final_lock_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; const char *conflict; svn_revnum_t newrev; svn_fs_access_t *access; svn_lock_t *mylock; svn_error_t *err; SVN_ERR(create_greek_fs(&fs, &newrev, "test-repo-final-lock-check", opts, pool)); /* Make a new transaction and delete "/A" */ SVN_ERR(svn_fs_begin_txn2(&txn, fs, newrev, SVN_FS_TXN_CHECK_LOCKS, pool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_fs_delete(txn_root, "/A", pool)); /* Become 'bubba' and lock "/A/D/G/rho". */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); /* We are no longer 'bubba'. We're nobody. */ SVN_ERR(svn_fs_set_access(fs, NULL)); /* Try to commit the transaction. Should fail, because a child of the deleted directory is locked by someone else. */ err = svn_fs_commit_txn(&conflict, &newrev, txn, pool); SVN_TEST_ASSERT(! SVN_IS_VALID_REVNUM(newrev)); if (! err) return svn_error_create (SVN_ERR_TEST_FAILED, NULL, "Uhoh, able to commit dir deletion when a child is locked."); svn_error_clear(err); /* Supply correct username and token; commit should work. */ SVN_ERR(svn_fs_set_access(fs, access)); SVN_ERR(svn_fs_access_add_lock_token(access, mylock->token)); SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(newrev)); return SVN_NO_ERROR; } /* If a directory's child is locked by someone else, we should still be able to commit a propchange on the directory. */ static svn_error_t * lock_dir_propchange(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; const char *conflict; svn_revnum_t newrev; svn_fs_access_t *access; svn_lock_t *mylock; SVN_ERR(create_greek_fs(&fs, &newrev, "test-repo-lock-dir-propchange", opts, pool)); /* Become 'bubba' and lock "/A/D/G/rho". */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); /* We are no longer 'bubba'. We're nobody. */ SVN_ERR(svn_fs_set_access(fs, NULL)); /* Make a new transaction and make a propchange on "/A" */ SVN_ERR(svn_fs_begin_txn2(&txn, fs, newrev, SVN_FS_TXN_CHECK_LOCKS, pool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_fs_change_node_prop(txn_root, "/A", "foo", svn_string_create("bar", pool), pool)); /* Commit should succeed; this means we're doing a non-recursive lock-check on directory, rather than a recursive one. */ SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(newrev)); return SVN_NO_ERROR; } /* Test that locks auto-expire correctly. */ static svn_error_t * lock_expiration(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; const char *conflict; svn_revnum_t newrev; svn_fs_access_t *access; svn_lock_t *mylock; svn_error_t *err; struct get_locks_baton_t *get_locks_baton; SVN_ERR(create_greek_fs(&fs, &newrev, "test-repo-lock-expiration", opts, pool)); /* Make a new transaction and change rho. */ SVN_ERR(svn_fs_begin_txn2(&txn, fs, newrev, SVN_FS_TXN_CHECK_LOCKS, pool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_test__set_file_contents(txn_root, "/A/D/G/rho", "new contents", pool)); /* We are now 'bubba'. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Lock /A/D/G/rho, with an expiration 2 seconds from now. */ SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, apr_time_now() + apr_time_from_sec(2), SVN_INVALID_REVNUM, FALSE, pool)); /* Become nobody. */ SVN_ERR(svn_fs_set_access(fs, NULL)); /* Try to commit. Should fail because we're 'nobody', and the lock hasn't expired yet. */ err = svn_fs_commit_txn(&conflict, &newrev, txn, pool); SVN_TEST_ASSERT(! SVN_IS_VALID_REVNUM(newrev)); if (! err) return svn_error_create (SVN_ERR_TEST_FAILED, NULL, "Uhoh, able to commit a file that has a non-expired lock."); svn_error_clear(err); /* Check that the lock is there, by getting it via the paths parent. */ { static const char *expected_paths [] = { "/A/D/G/rho" }; apr_size_t num_expected_paths = (sizeof(expected_paths) / sizeof(expected_paths[0])); get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "/A/D/G", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } /* Sleep 2 seconds, so the lock auto-expires. Anonymous commit should then succeed. */ apr_sleep(apr_time_from_sec(3)); /* Verify that the lock auto-expired even in the recursive case. */ { static const char *expected_paths [] = { 0 }; apr_size_t num_expected_paths = 0; get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "/A/D/G", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(newrev)); return SVN_NO_ERROR; } /* Test that a lock can be broken, stolen, or refreshed */ static svn_error_t * lock_break_steal_refresh(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_access_t *access; svn_lock_t *mylock, *somelock; SVN_ERR(create_greek_fs(&fs, NULL, "test-repo-steal-refresh", opts, pool)); /* Become 'bubba' and lock "/A/D/G/rho". */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); /* Become 'hortense' and break bubba's lock, then verify it's gone. */ SVN_ERR(svn_fs_create_access(&access, "hortense", pool)); SVN_ERR(svn_fs_set_access(fs, access)); SVN_ERR(svn_fs_unlock(fs, mylock->path, mylock->token, 1 /* FORCE BREAK */, pool)); SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if (somelock) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Tried to break a lock, but it's still there."); /* As hortense, create a new lock, and verify that we own it. */ SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, SVN_INVALID_REVNUM, FALSE, pool)); SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if (strcmp(somelock->owner, mylock->owner) != 0) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Made a lock, but we don't seem to own it."); /* As bubba, steal hortense's lock, creating a new one that expires. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, apr_time_now() + apr_time_from_sec(300), /* 5 min. */ SVN_INVALID_REVNUM, TRUE /* FORCE STEAL */, pool)); SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if (strcmp(somelock->owner, mylock->owner) != 0) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Made a lock, but we don't seem to own it."); if (! somelock->expiration_date) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Made expiring lock, but seems not to expire."); /* Refresh the lock, so that it never expires. */ SVN_ERR(svn_fs_lock(&somelock, fs, somelock->path, somelock->token, somelock->comment, 0, 0, SVN_INVALID_REVNUM, TRUE /* FORCE STEAL */, pool)); SVN_ERR(svn_fs_get_lock(&somelock, fs, "/A/D/G/rho", pool)); if (somelock->expiration_date) return svn_error_create(SVN_ERR_TEST_FAILED, NULL, "Made non-expirirng lock, but it expires."); return SVN_NO_ERROR; } /* Test that svn_fs_lock() and svn_fs_attach_lock() can do out-of-dateness checks.. */ static svn_error_t * lock_out_of_date(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; const char *conflict; svn_revnum_t newrev; svn_fs_access_t *access; svn_lock_t *mylock; svn_error_t *err; SVN_ERR(create_greek_fs(&fs, &newrev, "test-repo-lock-out-of-date", opts, pool)); /* Commit a small change to /A/D/G/rho, creating revision 2. */ SVN_ERR(svn_fs_begin_txn2(&txn, fs, newrev, SVN_FS_TXN_CHECK_LOCKS, pool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_test__set_file_contents(txn_root, "/A/D/G/rho", "new contents", pool)); SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(newrev)); /* We are now 'bubba'. */ SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Try to lock /A/D/G/rho, but claim that we still have r1 of the file. */ err = svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, 1, FALSE, pool); if (! err) return svn_error_create (SVN_ERR_TEST_FAILED, NULL, "Uhoh, able to lock an out-of-date file."); svn_error_clear(err); /* Attempt lock again, this time claiming to have r2. */ SVN_ERR(svn_fs_lock(&mylock, fs, "/A/D/G/rho", NULL, "", 0, 0, 2, FALSE, pool)); /* 'Refresh' the lock, claiming to have r1... should fail. */ err = svn_fs_lock(&mylock, fs, mylock->path, mylock->token, mylock->comment, 0, apr_time_now() + apr_time_from_sec(50), 1, TRUE /* FORCE STEAL */, pool); if (! err) return svn_error_create (SVN_ERR_TEST_FAILED, NULL, "Uhoh, able to refresh a lock on an out-of-date file."); svn_error_clear(err); return SVN_NO_ERROR; } struct lock_result_t { const svn_lock_t *lock; svn_error_t *fs_err; }; static svn_error_t * expect_lock(const char *path, apr_hash_t *results, svn_fs_t *fs, apr_pool_t *scratch_pool) { svn_lock_t *lock; struct lock_result_t *result = svn_hash_gets(results, path); SVN_TEST_ASSERT(result && result->lock && !result->fs_err); SVN_ERR(svn_fs_get_lock(&lock, fs, path, scratch_pool)); SVN_TEST_ASSERT(lock); return SVN_NO_ERROR; } static svn_error_t * expect_error(const char *path, apr_hash_t *results, svn_fs_t *fs, apr_pool_t *scratch_pool) { svn_lock_t *lock; struct lock_result_t *result = svn_hash_gets(results, path); SVN_TEST_ASSERT(result && !result->lock && result->fs_err); svn_error_clear(result->fs_err); SVN_ERR(svn_fs_get_lock(&lock, fs, path, scratch_pool)); SVN_TEST_ASSERT(!lock); return SVN_NO_ERROR; } static svn_error_t * expect_unlock(const char *path, apr_hash_t *results, svn_fs_t *fs, apr_pool_t *scratch_pool) { svn_lock_t *lock; struct lock_result_t *result = svn_hash_gets(results, path); SVN_TEST_ASSERT(result && !result->fs_err); SVN_ERR(svn_fs_get_lock(&lock, fs, path, scratch_pool)); SVN_TEST_ASSERT(!lock); return SVN_NO_ERROR; } static svn_error_t * expect_unlock_error(const char *path, apr_hash_t *results, svn_fs_t *fs, apr_pool_t *scratch_pool) { svn_lock_t *lock; struct lock_result_t *result = svn_hash_gets(results, path); SVN_TEST_ASSERT(result && result->fs_err); svn_error_clear(result->fs_err); SVN_ERR(svn_fs_get_lock(&lock, fs, path, scratch_pool)); SVN_TEST_ASSERT(lock); return SVN_NO_ERROR; } struct lock_many_baton_t { apr_hash_t *results; apr_pool_t *pool; int count; }; /* Implements svn_fs_lock_callback_t. */ static svn_error_t * lock_many_cb(void *lock_baton, const char *path, const svn_lock_t *lock, svn_error_t *fs_err, apr_pool_t *pool) { struct lock_many_baton_t *b = lock_baton; struct lock_result_t *result = apr_palloc(b->pool, sizeof(struct lock_result_t)); result->lock = lock; result->fs_err = svn_error_dup(fs_err); svn_hash_sets(b->results, apr_pstrdup(b->pool, path), result); if (b->count) if (!--(b->count)) return svn_error_create(SVN_ERR_FS_GENERAL, NULL, "lock_many_cb"); return SVN_NO_ERROR; } static svn_error_t * lock_multiple_paths(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_txn_t *txn; svn_fs_root_t *root, *txn_root; const char *conflict; svn_revnum_t newrev; svn_fs_access_t *access; svn_fs_lock_target_t *target; struct lock_many_baton_t baton; apr_hash_t *lock_paths, *unlock_paths; apr_hash_index_t *hi; SVN_ERR(create_greek_fs(&fs, &newrev, "test-lock-multiple-paths", opts, pool)); SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); SVN_ERR(svn_fs_revision_root(&root, fs, newrev, pool)); SVN_ERR(svn_fs_begin_txn2(&txn, fs, newrev, SVN_FS_TXN_CHECK_LOCKS, pool)); SVN_ERR(svn_fs_txn_root(&txn_root, txn, pool)); SVN_ERR(svn_fs_make_dir(txn_root, "/A/BB", pool)); SVN_ERR(svn_fs_make_dir(txn_root, "/A/BBB", pool)); SVN_ERR(svn_fs_copy(root, "/A/mu", txn_root, "/A/BB/mu", pool)); SVN_ERR(svn_fs_copy(root, "/A/mu", txn_root, "/A/BBB/mu", pool)); SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); baton.results = apr_hash_make(pool); baton.pool = pool; baton.count = 0; lock_paths = apr_hash_make(pool); unlock_paths = apr_hash_make(pool); target = svn_fs_lock_target_create(NULL, newrev, pool); svn_hash_sets(lock_paths, "/A/B/E/alpha", target); svn_hash_sets(lock_paths, "/A/B/E/beta", target); svn_hash_sets(lock_paths, "/A/B/E/zulu", target); svn_hash_sets(lock_paths, "/A/BB/mu", target); svn_hash_sets(lock_paths, "/A/BBB/mu", target); svn_hash_sets(lock_paths, "/A/D/G/pi", target); svn_hash_sets(lock_paths, "/A/D/G/rho", target); svn_hash_sets(lock_paths, "/A/mu", target); svn_hash_sets(lock_paths, "/X/zulu", target); /* Lock some paths. */ apr_hash_clear(baton.results); SVN_ERR(svn_fs_lock_many(fs, lock_paths, "comment", 0, 0, 0, lock_many_cb, &baton, pool, pool)); SVN_ERR(expect_lock("/A/B/E/alpha", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/B/E/beta", baton.results, fs, pool)); SVN_ERR(expect_error("/A/B/E/zulu", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/BB/mu", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/BBB/mu", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/D/G/pi", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/D/G/rho", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/mu", baton.results, fs, pool)); SVN_ERR(expect_error("/X/zulu", baton.results, fs, pool)); /* Unlock without force and wrong tokens. */ for (hi = apr_hash_first(pool, lock_paths); hi; hi = apr_hash_next(hi)) svn_hash_sets(unlock_paths, apr_hash_this_key(hi), "wrong-token"); apr_hash_clear(baton.results); SVN_ERR(svn_fs_unlock_many(fs, unlock_paths, FALSE, lock_many_cb, &baton, pool, pool)); SVN_ERR(expect_unlock_error("/A/B/E/alpha", baton.results, fs, pool)); SVN_ERR(expect_unlock_error("/A/B/E/beta", baton.results, fs, pool)); SVN_ERR(expect_error("/A/B/E/zulu", baton.results, fs, pool)); SVN_ERR(expect_unlock_error("/A/BB/mu", baton.results, fs, pool)); SVN_ERR(expect_unlock_error("/A/BBB/mu", baton.results, fs, pool)); SVN_ERR(expect_unlock_error("/A/D/G/pi", baton.results, fs, pool)); SVN_ERR(expect_unlock_error("/A/D/G/rho", baton.results, fs, pool)); SVN_ERR(expect_unlock_error("/A/mu", baton.results, fs, pool)); SVN_ERR(expect_error("/X/zulu", baton.results, fs, pool)); /* Force unlock. */ for (hi = apr_hash_first(pool, lock_paths); hi; hi = apr_hash_next(hi)) svn_hash_sets(unlock_paths, apr_hash_this_key(hi), ""); apr_hash_clear(baton.results); SVN_ERR(svn_fs_unlock_many(fs, unlock_paths, TRUE, lock_many_cb, &baton, pool, pool)); SVN_ERR(expect_unlock("/A/B/E/alpha", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/B/E/beta", baton.results, fs, pool)); SVN_ERR(expect_error("/A/B/E/zulu", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/BB/mu", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/BBB/mu", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/D/G/pi", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/D/G/rho", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/mu", baton.results, fs, pool)); SVN_ERR(expect_error("/X/zulu", baton.results, fs, pool)); /* Lock again. */ apr_hash_clear(baton.results); SVN_ERR(svn_fs_lock_many(fs, lock_paths, "comment", 0, 0, 0, lock_many_cb, &baton, pool, pool)); SVN_ERR(expect_lock("/A/B/E/alpha", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/B/E/beta", baton.results, fs, pool)); SVN_ERR(expect_error("/A/B/E/zulu", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/BB/mu", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/BBB/mu", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/D/G/pi", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/D/G/rho", baton.results, fs, pool)); SVN_ERR(expect_lock("/A/mu", baton.results, fs, pool)); SVN_ERR(expect_error("/X/zulu", baton.results, fs, pool)); /* Unlock without force. */ for (hi = apr_hash_first(pool, baton.results); hi; hi = apr_hash_next(hi)) { struct lock_result_t *result = apr_hash_this_val(hi); svn_hash_sets(unlock_paths, apr_hash_this_key(hi), result->lock ? result->lock->token : "non-existent-token"); } apr_hash_clear(baton.results); SVN_ERR(svn_fs_unlock_many(fs, unlock_paths, FALSE, lock_many_cb, &baton, pool, pool)); SVN_ERR(expect_unlock("/A/B/E/alpha", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/B/E/beta", baton.results, fs, pool)); SVN_ERR(expect_error("/A/B/E/zulu", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/BB/mu", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/BBB/mu", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/D/G/pi", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/D/G/rho", baton.results, fs, pool)); SVN_ERR(expect_unlock("/A/mu", baton.results, fs, pool)); SVN_ERR(expect_error("/X/zulu", baton.results, fs, pool)); return SVN_NO_ERROR; } static svn_error_t * lock_cb_error(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_revnum_t newrev; svn_fs_access_t *access; svn_fs_lock_target_t *target; struct lock_many_baton_t baton; apr_hash_t *lock_paths, *unlock_paths; svn_lock_t *lock; SVN_ERR(create_greek_fs(&fs, &newrev, "test-lock-cb-error", opts, pool)); SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); baton.results = apr_hash_make(pool); baton.pool = pool; baton.count = 1; lock_paths = apr_hash_make(pool); unlock_paths = apr_hash_make(pool); target = svn_fs_lock_target_create(NULL, newrev, pool); svn_hash_sets(lock_paths, "/A/B/E/alpha", target); svn_hash_sets(lock_paths, "/A/B/E/beta", target); apr_hash_clear(baton.results); SVN_TEST_ASSERT_ERROR(svn_fs_lock_many(fs, lock_paths, "comment", 0, 0, 0, lock_many_cb, &baton, pool, pool), SVN_ERR_FS_GENERAL); SVN_TEST_ASSERT(apr_hash_count(baton.results) == 1); SVN_TEST_ASSERT(svn_hash_gets(baton.results, "/A/B/E/alpha") || svn_hash_gets(baton.results, "/A/B/E/beta")); SVN_ERR(svn_fs_get_lock(&lock, fs, "/A/B/E/alpha", pool)); SVN_TEST_ASSERT(lock); svn_hash_sets(unlock_paths, "/A/B/E/alpha", lock->token); SVN_ERR(svn_fs_get_lock(&lock, fs, "/A/B/E/beta", pool)); SVN_TEST_ASSERT(lock); svn_hash_sets(unlock_paths, "/A/B/E/beta", lock->token); baton.count = 1; apr_hash_clear(baton.results); SVN_TEST_ASSERT_ERROR(svn_fs_unlock_many(fs, unlock_paths, FALSE, lock_many_cb, &baton, pool, pool), SVN_ERR_FS_GENERAL); SVN_TEST_ASSERT(apr_hash_count(baton.results) == 1); SVN_TEST_ASSERT(svn_hash_gets(baton.results, "/A/B/E/alpha") || svn_hash_gets(baton.results, "/A/B/E/beta")); SVN_ERR(svn_fs_get_lock(&lock, fs, "/A/B/E/alpha", pool)); SVN_TEST_ASSERT(!lock); SVN_ERR(svn_fs_get_lock(&lock, fs, "/A/B/E/beta", pool)); SVN_TEST_ASSERT(!lock); return SVN_NO_ERROR; } static svn_error_t * obtain_write_lock_failure(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_revnum_t newrev; svn_fs_access_t *access; svn_fs_lock_target_t *target; struct lock_many_baton_t baton; apr_hash_t *lock_paths, *unlock_paths; /* The test makes sense only for FSFS. */ 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"); SVN_ERR(create_greek_fs(&fs, &newrev, "test-obtain-write-lock-failure", opts, pool)); SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Make a read only 'write-lock' file. This prevents any write operations from being executed. */ SVN_ERR(svn_io_set_file_read_only("test-obtain-write-lock-failure/write-lock", FALSE, pool)); baton.results = apr_hash_make(pool); baton.pool = pool; baton.count = 0; /* Trying to lock some paths. We don't really care about error; the test shouldn't crash. */ target = svn_fs_lock_target_create(NULL, newrev, pool); lock_paths = apr_hash_make(pool); svn_hash_sets(lock_paths, "/iota", target); svn_hash_sets(lock_paths, "/A/mu", target); apr_hash_clear(baton.results); SVN_TEST_ASSERT_ANY_ERROR(svn_fs_lock_many(fs, lock_paths, "comment", 0, 0, 0, lock_many_cb, &baton, pool, pool)); /* Trying to unlock some paths. We don't really care about error; the test shouldn't crash. */ unlock_paths = apr_hash_make(pool); svn_hash_sets(unlock_paths, "/iota", ""); svn_hash_sets(unlock_paths, "/A/mu", ""); apr_hash_clear(baton.results); SVN_TEST_ASSERT_ANY_ERROR(svn_fs_unlock_many(fs, unlock_paths, TRUE, lock_many_cb, &baton, pool, pool)); return SVN_NO_ERROR; } static svn_error_t * parent_and_child_lock(const svn_test_opts_t *opts, apr_pool_t *pool) { svn_fs_t *fs; svn_fs_access_t *access; svn_fs_txn_t *txn; svn_fs_root_t *root; const char *conflict; svn_revnum_t newrev; svn_lock_t *lock; struct get_locks_baton_t *get_locks_baton; apr_size_t num_expected_paths; SVN_ERR(svn_test__create_fs(&fs, "test-parent-and-child-lock", opts, pool)); SVN_ERR(svn_fs_create_access(&access, "bubba", pool)); SVN_ERR(svn_fs_set_access(fs, access)); /* Make a file '/A'. */ SVN_ERR(svn_fs_begin_txn(&txn, fs, 0, pool)); SVN_ERR(svn_fs_txn_root(&root, txn, pool)); SVN_ERR(svn_fs_make_file(root, "/A", pool)); SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); /* Obtain a lock on '/A'. */ SVN_ERR(svn_fs_lock(&lock, fs, "/A", NULL, NULL, FALSE, 0, newrev, FALSE, pool)); /* Add a lock token to FS access context. */ SVN_ERR(svn_fs_access_add_lock_token(access, lock->token)); /* Make some weird change: replace file '/A' by a directory with a child. Issue 2507 means that the result is that the directory /A remains locked. */ SVN_ERR(svn_fs_begin_txn(&txn, fs, newrev, pool)); SVN_ERR(svn_fs_txn_root(&root, txn, pool)); SVN_ERR(svn_fs_delete(root, "/A", pool)); SVN_ERR(svn_fs_make_dir(root, "/A", pool)); SVN_ERR(svn_fs_make_file(root, "/A/b", pool)); SVN_ERR(svn_fs_commit_txn(&conflict, &newrev, txn, pool)); /* Obtain a lock on '/A/b'. Issue 2507 means that the lock index for / refers to both /A and /A/b, and that the lock index for /A refers to /A/b. */ SVN_ERR(svn_fs_lock(&lock, fs, "/A/b", NULL, NULL, FALSE, 0, newrev, FALSE, pool)); /* Verify the locked paths. The lock for /A/b should not be reported twice even though issue 2507 means we access the index for / and the index for /A both of which refer to /A/b. */ { static const char *expected_paths[] = { "/A", "/A/b", }; num_expected_paths = sizeof(expected_paths) / sizeof(const char *); get_locks_baton = make_get_locks_baton(pool); SVN_ERR(svn_fs_get_locks(fs, "/", get_locks_callback, get_locks_baton, pool)); SVN_ERR(verify_matching_lock_paths(get_locks_baton, expected_paths, num_expected_paths, pool)); } return SVN_NO_ERROR; } /* ------------------------------------------------------------------------ */ /* The test table. */ static int max_threads = 2; static struct svn_test_descriptor_t test_funcs[] = { SVN_TEST_NULL, SVN_TEST_OPTS_PASS(lock_expiration, "test that locks can expire"), SVN_TEST_OPTS_PASS(lock_only, "lock only"), SVN_TEST_OPTS_PASS(lookup_lock_by_path, "lookup lock by path"), SVN_TEST_OPTS_PASS(attach_lock, "attach lock"), SVN_TEST_OPTS_PASS(get_locks, "get locks"), SVN_TEST_OPTS_PASS(basic_lock, "basic locking"), SVN_TEST_OPTS_PASS(lock_credentials, "test that locking requires proper credentials"), SVN_TEST_OPTS_PASS(final_lock_check, "test that locking is enforced in final commit step"), SVN_TEST_OPTS_PASS(lock_dir_propchange, "dir propchange can be committed with locked child"), SVN_TEST_OPTS_PASS(lock_break_steal_refresh, "breaking, stealing, refreshing a lock"), SVN_TEST_OPTS_PASS(lock_out_of_date, "check out-of-dateness before locking"), SVN_TEST_OPTS_PASS(lock_multiple_paths, "lock multiple paths"), SVN_TEST_OPTS_PASS(lock_cb_error, "lock callback error"), SVN_TEST_OPTS_PASS(obtain_write_lock_failure, "lock/unlock when 'write-lock' couldn't be obtained"), SVN_TEST_OPTS_PASS(parent_and_child_lock, "lock parent and it's child"), SVN_TEST_NULL }; SVN_TEST_MAIN