The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/* changes-test.c --- test `changes' interfaces
 *
 * ====================================================================
 *    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 <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <stdio.h>

#include <apr.h>

#include "../svn_test.h"

#include "svn_pools.h"
#include "svn_error.h"
#include "private/svn_skel.h"

#include "../svn_test_fs.h"
#include "../../libsvn_fs_base/util/fs_skels.h"
#include "../../libsvn_fs_base/bdb/changes-table.h"



/* Helper functions/variables.  */
static const char *standard_txns[]
  = { "0", "1", "2", "3", "4", "5", "6" };
static const char *standard_changes[19][6]
     /* KEY   PATH   NODEREVID  KIND     TEXT PROP */
  = { { "0",  "/foo",  "1.0.0",  "add",     0,  0  },
      { "0",  "/foo",  "1.0.0",  "modify",  0, "1" },
      { "0",  "/bar",  "2.0.0",  "add",     0,  0  },
      { "0",  "/bar",  "2.0.0",  "modify", "1", 0  },
      { "0",  "/bar",  "2.0.0",  "modify",  0, "1" },
      { "0",  "/baz",  "3.0.0",  "add",     0,  0  },
      { "0",  "/baz",  "3.0.0",  "modify", "1", 0  },
      { "1",  "/foo",  "1.0.1",  "modify", "1", 0  },
      { "2",  "/foo",  "1.0.2",  "modify",  0, "1" },
      { "2",  "/bar",  "2.0.2",  "modify", "1", 0  },
      { "3",  "/baz",  "3.0.3",  "modify", "1", 0  },
      { "4",  "/fob",  "4.0.4",  "add",     0,  0  },
      { "4",  "/fob",  "4.0.4",  "modify", "1", 0  },
      { "5",  "/baz",  "3.0.3",  "delete",  0,  0  },
      { "5",  "/baz",  "5.0.5",  "add",     0, "1" },
      { "5",  "/baz",  "5.0.5",  "modify", "1", 0  },
      { "6",  "/fob",  "4.0.6",  "modify", "1", 0  },
      { "6",  "/fob",  "4.0.6",  "reset",   0,  0  },
      { "6",  "/fob",  "4.0.6",  "modify",  0, "1" } };


static svn_fs_path_change_kind_t string_to_kind(const char *str)
{
  if (strcmp(str, "add") == 0)
    return svn_fs_path_change_add;
  if (strcmp(str, "delete") == 0)
    return svn_fs_path_change_delete;
  if (strcmp(str, "replace") == 0)
    return svn_fs_path_change_replace;
  if (strcmp(str, "modify") == 0)
    return svn_fs_path_change_modify;
  if (strcmp(str, "reset") == 0)
    return svn_fs_path_change_reset;
  return 0;
}


/* Common args structure for several different txn_body_* functions. */
struct changes_args
{
  svn_fs_t *fs;
  const char *key;
  change_t *change;
  apr_array_header_t *raw_changes;
  apr_hash_t *changes;
};


static svn_error_t *
txn_body_changes_add(void *baton, trail_t *trail)
{
  struct changes_args *b = baton;
  return svn_fs_bdb__changes_add(b->fs, b->key, b->change,
                                 trail, trail->pool);
}


static svn_error_t *
add_standard_changes(svn_fs_t *fs,
                     apr_pool_t *pool)
{
  int i;
  struct changes_args args;
  int num_changes = sizeof(standard_changes) / sizeof(const char *) / 6;

  for (i = 0; i < num_changes; i++)
    {
      change_t change;

      /* Set up the current change item. */
      change.path = standard_changes[i][1];
      change.noderev_id = svn_fs_parse_id(standard_changes[i][2],
                                          strlen(standard_changes[i][2]),
                                          pool);
      change.kind = string_to_kind(standard_changes[i][3]);
      change.text_mod = standard_changes[i][4] ? 1 : 0;
      change.prop_mod = standard_changes[i][5] ? 1 : 0;

      /* Set up transaction baton. */
      args.fs = fs;
      args.key = standard_changes[i][0];
      args.change = &change;

      /* Write new changes to the changes table. */
      SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_add, &args,
                                     TRUE, pool));
    }

  return SVN_NO_ERROR;
}


static svn_error_t *
txn_body_changes_fetch_raw(void *baton, trail_t *trail)
{
  struct changes_args *b = baton;
  return svn_fs_bdb__changes_fetch_raw(&(b->raw_changes), b->fs, b->key,
                                       trail, trail->pool);
}


static svn_error_t *
txn_body_changes_fetch(void *baton, trail_t *trail)
{
  struct changes_args *b = baton;
  return svn_fs_bdb__changes_fetch(&(b->changes), b->fs, b->key,
                                   trail, trail->pool);
}


static svn_error_t *
txn_body_changes_delete(void *baton, trail_t *trail)
{
  struct changes_args *b = baton;
  return svn_fs_bdb__changes_delete(b->fs, b->key, trail, trail->pool);
}



/* The tests.  */

static svn_error_t *
changes_add(const svn_test_opts_t *opts,
            apr_pool_t *pool)
{
  svn_fs_t *fs;

  /* Create a new fs and repos */
  SVN_ERR(svn_test__create_bdb_fs(&fs, "test-repo-changes-add", opts,
                                  pool));

  /* Add the standard slew of changes. */
  SVN_ERR(add_standard_changes(fs, pool));

  return SVN_NO_ERROR;
}


static svn_error_t *
changes_fetch_raw(const svn_test_opts_t *opts,
                  apr_pool_t *pool)
{
  svn_fs_t *fs;
  int i;
  int num_txns = sizeof(standard_txns) / sizeof(const char *);
  int cur_change_index = 0;
  struct changes_args args;

  /* Create a new fs and repos */
  SVN_ERR(svn_test__create_bdb_fs(&fs, "test-repo-changes-fetch", opts,
                                  pool));

  /* First, verify that we can request changes for an arbitrary key
     without error. */
  args.fs = fs;
  args.key = "blahbliggityblah";
  SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_fetch_raw, &args,
                                 FALSE, pool));
  if ((! args.raw_changes) || (args.raw_changes->nelts))
    return svn_error_create(SVN_ERR_TEST_FAILED, NULL,
                            "expected empty changes array");

  /* Add the standard slew of changes. */
  SVN_ERR(add_standard_changes(fs, pool));

  /* For each transaction, fetch that transaction's changes, and
     compare those changes against the standard changes list.  Order
     matters throughout all the changes code, so we shouldn't have to
     worry about ordering of the arrays.  */
  for (i = 0; i < num_txns; i++)
    {
      const char *txn_id = standard_txns[i];
      int j;

      /* Setup the trail baton. */
      args.fs = fs;
      args.key = txn_id;

      /* And get those changes. */
      SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_fetch_raw,
                                     &args, FALSE, pool));
      if (! args.raw_changes)
        return svn_error_createf(SVN_ERR_TEST_FAILED, NULL,
                                 "got no changes for key '%s'", txn_id);

      for (j = 0; j < args.raw_changes->nelts; j++)
        {
          svn_string_t *noderev_id;
          svn_fs_path_change_kind_t kind;
          change_t *change = APR_ARRAY_IDX(args.raw_changes, j, change_t *);
          int mod_bit = 0;

          /* Verify that the TXN_ID matches. */
          if (strcmp(standard_changes[cur_change_index][0], txn_id))
            return svn_error_createf
              (SVN_ERR_TEST_FAILED, NULL,
               "missing some changes for key '%s'", txn_id);

          /* Verify that the PATH matches. */
          if (strcmp(standard_changes[cur_change_index][1], change->path))
            return svn_error_createf
              (SVN_ERR_TEST_FAILED, NULL,
               "paths differ in change for key '%s'", txn_id);

          /* Verify that the NODE-REV-ID matches. */
          noderev_id = svn_fs_unparse_id(change->noderev_id, pool);
          if (strcmp(standard_changes[cur_change_index][2], noderev_id->data))
            return svn_error_createf
              (SVN_ERR_TEST_FAILED, NULL,
               "node revision ids differ in change for key '%s'", txn_id);

          /* Verify that the change KIND matches. */
          kind = string_to_kind(standard_changes[cur_change_index][3]);
          if (kind != change->kind)
            return svn_error_createf
              (SVN_ERR_TEST_FAILED, NULL,
               "change kinds differ in change for key '%s'", txn_id);

          /* Verify that the change TEXT-MOD bit matches. */
          mod_bit = standard_changes[cur_change_index][4] ? 1 : 0;
          if (mod_bit != change->text_mod)
            return svn_error_createf
              (SVN_ERR_TEST_FAILED, NULL,
               "change text-mod bits differ in change for key '%s'", txn_id);

          /* Verify that the change PROP-MOD bit matches. */
          mod_bit = standard_changes[cur_change_index][5] ? 1 : 0;
          if (mod_bit != change->prop_mod)
            return svn_error_createf
              (SVN_ERR_TEST_FAILED, NULL,
               "change prop-mod bits differ in change for key '%s'", txn_id);

          cur_change_index++;
        }
    }

  return SVN_NO_ERROR;
}


static svn_error_t *
changes_delete(const svn_test_opts_t *opts,
               apr_pool_t *pool)
{
  svn_fs_t *fs;
  int i;
  int num_txns = sizeof(standard_txns) / sizeof(const char *);
  struct changes_args args;

  /* Create a new fs and repos */
  SVN_ERR(svn_test__create_bdb_fs(&fs, "test-repo-changes-delete", opts,
                                  pool));

  /* Add the standard slew of changes. */
  SVN_ERR(add_standard_changes(fs, pool));

  /* Now, delete all the changes we know about, verifying their removal. */
  for (i = 0; i < num_txns; i++)
    {
      args.fs = fs;
      args.key = standard_txns[i];
      SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_delete,
                                     &args, FALSE, pool));
      args.changes = 0;
      SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_fetch_raw,
                                     &args, FALSE, pool));
      if ((! args.raw_changes) || (args.raw_changes->nelts))
        return svn_error_createf
          (SVN_ERR_TEST_FAILED, NULL,
           "expected empty changes array for txn '%s'", args.key);
    }

  return SVN_NO_ERROR;
}


static apr_hash_t *
get_ideal_changes(const char *txn_id,
                  apr_pool_t *pool)
{
  apr_hash_t *ideal = apr_hash_make(pool);
  svn_fs_path_change_t *change;
  if (strcmp(txn_id, "0") == 0)
    {
      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("1.0.0", 5, pool);
      change->change_kind = svn_fs_path_change_add;
      change->text_mod = 0;
      change->prop_mod = 1;
      apr_hash_set(ideal, "/foo", APR_HASH_KEY_STRING, change);

      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("2.0.0", 5, pool);
      change->change_kind = svn_fs_path_change_add;
      change->text_mod = 1;
      change->prop_mod = 1;
      apr_hash_set(ideal, "/bar", APR_HASH_KEY_STRING, change);

      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("3.0.0", 5, pool);
      change->change_kind = svn_fs_path_change_add;
      change->text_mod = 1;
      change->prop_mod = 0;
      apr_hash_set(ideal, "/baz", APR_HASH_KEY_STRING, change);
    }
  if (strcmp(txn_id, "1") == 0)
    {
      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("1.0.1", 5, pool);
      change->change_kind = svn_fs_path_change_modify;
      change->text_mod = 1;
      change->prop_mod = 0;
      apr_hash_set(ideal, "/foo", APR_HASH_KEY_STRING, change);
    }
  if (strcmp(txn_id, "2") == 0)
    {
      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("1.0.2", 5, pool);
      change->change_kind = svn_fs_path_change_modify;
      change->text_mod = 0;
      change->prop_mod = 1;
      apr_hash_set(ideal, "/foo", APR_HASH_KEY_STRING, change);

      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("2.0.2", 5, pool);
      change->change_kind = svn_fs_path_change_modify;
      change->text_mod = 1;
      change->prop_mod = 0;
      apr_hash_set(ideal, "/bar", APR_HASH_KEY_STRING, change);
    }
  if (strcmp(txn_id, "3") == 0)
    {
      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("3.0.3", 5, pool);
      change->change_kind = svn_fs_path_change_modify;
      change->text_mod = 1;
      change->prop_mod = 0;
      apr_hash_set(ideal, "/baz", APR_HASH_KEY_STRING, change);
    }
  if (strcmp(txn_id, "4") == 0)
    {
      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("4.0.4", 5, pool);
      change->change_kind = svn_fs_path_change_add;
      change->text_mod = 1;
      change->prop_mod = 0;
      apr_hash_set(ideal, "/fob", APR_HASH_KEY_STRING, change);
    }
  if (strcmp(txn_id, "5") == 0)
    {
      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("5.0.5", 5, pool);
      change->change_kind = svn_fs_path_change_replace;
      change->text_mod = 1;
      change->prop_mod = 1;
      apr_hash_set(ideal, "/baz", APR_HASH_KEY_STRING, change);
    }
  if (strcmp(txn_id, "6") == 0)
    {
      change = apr_palloc(pool, sizeof(*change));
      change->node_rev_id = svn_fs_parse_id("4.0.6", 5, pool);
      change->change_kind = svn_fs_path_change_modify;
      change->text_mod = 0;
      change->prop_mod = 1;
      apr_hash_set(ideal, "/fob", APR_HASH_KEY_STRING, change);
    }
  return ideal;
}


static svn_error_t *
compare_changes(apr_hash_t *ideals,
                apr_hash_t *changes,
                const svn_test_opts_t *opts,
                const char *txn_id,
                apr_pool_t *pool)
{
  apr_hash_index_t *hi;

  for (hi = apr_hash_first(pool, ideals); hi; hi = apr_hash_next(hi))
    {
      const void *key;
      void *val;
      svn_fs_path_change_t *ideal_change, *change;
      const char *path;

      /* KEY will be the path, VAL the change. */
      apr_hash_this(hi, &key, NULL, &val);
      path = (const char *) key;
      ideal_change = val;

      /* Now get the change that refers to PATH in the actual
         changes hash. */
      change = apr_hash_get(changes, path, APR_HASH_KEY_STRING);
      if (! change)
        return svn_error_createf
          (SVN_ERR_TEST_FAILED, NULL,
           "missing expected change for path '%s' in txn_id '%s'",
           path, txn_id);

      /* Verify that the NODE-REV-ID matches. */
      if (svn_fs_compare_ids(change->node_rev_id,
                             ideal_change->node_rev_id))
        return svn_error_createf
          (SVN_ERR_TEST_FAILED, NULL,
           "node revision ids differ in change for key '%s'", txn_id);

      /* Verify that the change KIND matches. */
      if (change->change_kind != ideal_change->change_kind)
        return svn_error_createf
          (SVN_ERR_TEST_FAILED, NULL,
           "change kinds differ in change for key '%s'", txn_id);

      /* Verify that the change TEXT-MOD bit matches. */
      if (change->text_mod != ideal_change->text_mod)
        return svn_error_createf
          (SVN_ERR_TEST_FAILED, NULL,
           "change text-mod bits differ in change for key '%s'", txn_id);

      /* Verify that the change PROP-MOD bit matches. */
      if (change->prop_mod != ideal_change->prop_mod)
        return svn_error_createf
          (SVN_ERR_TEST_FAILED, NULL,
           "change prop-mod bits differ in change for key '%s'", txn_id);
    }

  return SVN_NO_ERROR;
}


static svn_error_t *
changes_fetch(const svn_test_opts_t *opts,
              apr_pool_t *pool)
{
  svn_fs_t *fs;
  int i;
  int num_txns = sizeof(standard_txns) / sizeof(const char *);
  struct changes_args args;

  /* Create a new fs and repos */
  SVN_ERR(svn_test__create_bdb_fs(&fs, "test-repo-changes-fetch", opts,
                                  pool));

  /* First, verify that we can request changes for an arbitrary key
     without error. */
  args.fs = fs;
  args.key = "blahbliggityblah";
  SVN_ERR(svn_fs_base__retry_txn(fs, txn_body_changes_fetch, &args,
                                 FALSE, pool));
  if ((! args.changes) || (apr_hash_count(args.changes)))
    return svn_error_create(SVN_ERR_TEST_FAILED, NULL,
                            "expected empty changes hash");

  /* Add the standard slew of changes. */
  SVN_ERR(add_standard_changes(fs, pool));

  /* For each transaction, fetch that transaction's changes, and
     compare those changes against our ideal compressed changes
     hash. */
  for (i = 0; i < num_txns; i++)
    {
      const char *txn_id = standard_txns[i];
      apr_hash_t *ideals;

      /* Get the ideal changes hash. */
      ideals = get_ideal_changes(txn_id, pool);

      /* Setup the trail baton. */
      args.fs = fs;
      args.key = txn_id;

      /* And get those changes via in the internal interface, and
         verify that they are accurate. */
      SVN_ERR(svn_fs_base__retry_txn(fs, txn_body_changes_fetch, &args,
                                     FALSE, pool));
      if (! args.changes)
        return svn_error_createf
          (SVN_ERR_TEST_FAILED, NULL,
           "got no changes for key '%s'", txn_id);
      if (apr_hash_count(ideals) != apr_hash_count(args.changes))
        return svn_error_createf
          (SVN_ERR_TEST_FAILED, NULL,
           "unexpected number of changes for key '%s'", txn_id);
      SVN_ERR(compare_changes(ideals, args.changes, opts, txn_id, pool));
    }

  return SVN_NO_ERROR;
}


static svn_error_t *
changes_fetch_ordering(const svn_test_opts_t *opts,
                       apr_pool_t *pool)
{
  svn_fs_t *fs;
  svn_revnum_t youngest_rev = 0;
  const char *txn_name;
  svn_fs_txn_t *txn;
  svn_fs_root_t *txn_root, *rev_root;
  struct changes_args args;
  apr_pool_t *subpool = svn_pool_create(pool);
  apr_hash_index_t *hi;

  /* Create a new fs and repos */
  SVN_ERR(svn_test__create_bdb_fs
          (&fs, "test-repo-changes-fetch-ordering", opts,
           pool));

  /*** REVISION 1: Make some files and dirs. ***/
  SVN_ERR(svn_fs_begin_txn(&txn, fs, youngest_rev, subpool));
  SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool));
  {
    static svn_test__txn_script_command_t script_entries[] = {
      { 'a', "dir1",        0 },
      { 'a', "file1",       "This is the file 'file1'.\n" },
      { 'a', "dir1/file2",  "This is the file 'file2'.\n" },
      { 'a', "dir1/file3",  "This is the file 'file3'.\n" },
      { 'a', "dir1/file4",  "This is the file 'file4'.\n" },
    };
    SVN_ERR(svn_test__txn_script_exec(txn_root, script_entries, 5, subpool));
  }
  SVN_ERR(svn_fs_commit_txn(NULL, &youngest_rev, txn, subpool));
  SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev));
  svn_pool_clear(subpool);

  /*** REVISION 2: Delete and add some stuff, non-depth-first. ***/
  SVN_ERR(svn_fs_begin_txn(&txn, fs, youngest_rev, subpool));
  /* Don't use subpool, txn_name is used after subpool is cleared */
  SVN_ERR(svn_fs_txn_name(&txn_name, txn, pool));
  SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool));
  {
    static svn_test__txn_script_command_t script_entries[] = {
      { 'd', "file1",       "This is the file 'file1'.\n" },
      { 'd', "dir1/file2",  "This is the file 'file2'.\n" },
      { 'd', "dir1/file3",  "This is the file 'file3'.\n" },
      { 'a', "dir1/file5",  "This is the file 'file4'.\n" },
      { 'a', "dir1/dir2",   0 },
      { 'd', "dir1",        0 },
      { 'a', "dir3",        0 },
    };
    SVN_ERR(svn_test__txn_script_exec(txn_root, script_entries, 7, subpool));
  }
  SVN_ERR(svn_fs_commit_txn(NULL, &youngest_rev, txn, subpool));
  SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev));
  svn_pool_clear(subpool);

  /*** TEST:  We should have only three changes, the deletion of 'file1'
       the deletion of 'dir1', and the addition of 'dir3'. ***/
  args.fs = fs;
  args.key = txn_name;
  SVN_ERR(svn_fs_base__retry_txn(fs, txn_body_changes_fetch, &args,
                                 FALSE, subpool));
  if ((! args.changes) || (apr_hash_count(args.changes) != 3))
    return svn_error_create(SVN_ERR_TEST_FAILED, NULL,
                            "expected changes");
  for (hi = apr_hash_first(subpool, args.changes);
       hi; hi = apr_hash_next(hi))
    {
      const void *key;
      void *val;
      svn_fs_path_change_t *change;

      /* KEY will be the path, VAL the change. */
      apr_hash_this(hi, &key, NULL, &val);
      change = val;

      if ((change->change_kind == svn_fs_path_change_add)
          && (strcmp(key, "/dir3") == 0))
        ;
      else if ((change->change_kind == svn_fs_path_change_delete)
               && ((strcmp(key, "/dir1") == 0)
                   || (strcmp(key, "/file1") == 0)))
        ;
      else
        return svn_error_create(SVN_ERR_TEST_FAILED, NULL,
                                "got wrong changes");
    }

  /*** REVISION 3: Do the same stuff as in revision 1. ***/
  SVN_ERR(svn_fs_begin_txn(&txn, fs, youngest_rev, subpool));
  SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool));
  {
    static svn_test__txn_script_command_t script_entries[] = {
      { 'a', "dir1",        0 },
      { 'a', "file1",       "This is the file 'file1'.\n" },
      { 'a', "dir1/file2",  "This is the file 'file2'.\n" },
      { 'a', "dir1/file3",  "This is the file 'file3'.\n" },
      { 'a', "dir1/file4",  "This is the file 'file4'.\n" },
    };
    SVN_ERR(svn_test__txn_script_exec(txn_root, script_entries, 5, subpool));
  }
  SVN_ERR(svn_fs_commit_txn(NULL, &youngest_rev, txn, subpool));
  SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev));
  svn_pool_clear(subpool);

  /*** REVISION 4: Do the same stuff as in revision 2, but use a copy
       overwrite of the top directory (instead of a delete) to test
       that the 'replace' change type works, too.  (And add 'dir4'
       instead of 'dir3', since 'dir3' still exists).  ***/
  SVN_ERR(svn_fs_begin_txn(&txn, fs, youngest_rev, subpool));
  /* Don't use subpool, txn_name is used after subpool is cleared */
  SVN_ERR(svn_fs_txn_name(&txn_name, txn, pool));
  SVN_ERR(svn_fs_txn_root(&txn_root, txn, subpool));
  SVN_ERR(svn_fs_revision_root(&rev_root, fs, 1, subpool));
  {
    static svn_test__txn_script_command_t script_entries[] = {
      { 'd', "file1",       "This is the file 'file1'.\n" },
      { 'd', "dir1/file2",  "This is the file 'file2'.\n" },
      { 'd', "dir1/file3",  "This is the file 'file3'.\n" },
      { 'a', "dir1/file5",  "This is the file 'file4'.\n" },
      { 'a', "dir1/dir2",   0 },
    };
    SVN_ERR(svn_test__txn_script_exec(txn_root, script_entries, 5, subpool));
    SVN_ERR(svn_fs_copy(rev_root, "dir1", txn_root, "dir1", subpool));
    SVN_ERR(svn_fs_make_dir(txn_root, "dir4", subpool));
  }
  SVN_ERR(svn_fs_commit_txn(NULL, &youngest_rev, txn, subpool));
  SVN_TEST_ASSERT(SVN_IS_VALID_REVNUM(youngest_rev));
  svn_pool_clear(subpool);

  /*** TEST:  We should have only three changes, the deletion of 'file1'
       the replacement of 'dir1', and the addition of 'dir4'. ***/
  args.fs = fs;
  args.key = txn_name;
  SVN_ERR(svn_fs_base__retry_txn(fs, txn_body_changes_fetch, &args,
                                 FALSE, subpool));
  if ((! args.changes) || (apr_hash_count(args.changes) != 3))
    return svn_error_create(SVN_ERR_TEST_FAILED, NULL,
                            "expected changes");
  for (hi = apr_hash_first(subpool, args.changes);
       hi; hi = apr_hash_next(hi))
    {
      const void *key;
      void *val;
      svn_fs_path_change_t *change;

      /* KEY will be the path, VAL the change. */
      apr_hash_this(hi, &key, NULL, &val);
      change = val;

      if ((change->change_kind == svn_fs_path_change_add)
          && (strcmp(key, "/dir4") == 0))
        ;
      else if ((change->change_kind == svn_fs_path_change_replace)
               && (strcmp(key, "/dir1") == 0))
        ;
      else if ((change->change_kind == svn_fs_path_change_delete)
               && (strcmp(key, "/file1") == 0))
        ;
      else
        return svn_error_create(SVN_ERR_TEST_FAILED, NULL,
                                "got wrong changes");
    }

  return SVN_NO_ERROR;
}


static svn_error_t *
changes_bad_sequences(const svn_test_opts_t *opts,
                      apr_pool_t *pool)
{
  svn_fs_t *fs;
  apr_pool_t *subpool = svn_pool_create(pool);
  svn_error_t *err;

  /* Create a new fs and repos */
  SVN_ERR(svn_test__create_bdb_fs
          (&fs, "test-repo-changes-bad-sequences", opts,
           pool));

  /* Test changes bogus because a path's node-rev-ID changed
     unexpectedly. */
  svn_pool_clear(subpool);
  {
    static const char *bogus_changes[][6]
         /* KEY   PATH   NODEREVID  KIND       TEXT PROP */
      = { { "x",  "/foo",  "1.0.0",  "add",     0 ,  0  },
          { "x",  "/foo",  "1.0.0",  "modify",  0 , "1" },
          { "x",  "/foo",  "2.0.0",  "modify", "1", "1" } };
    int num_changes = sizeof(bogus_changes) / sizeof(const char *) / 6;
    struct changes_args args;
    int i;

    for (i = 0; i < num_changes; i++)
      {
        change_t change;

        /* Set up the current change item. */
        change.path = bogus_changes[i][1];
        change.noderev_id = svn_fs_parse_id(bogus_changes[i][2],
                                            strlen(bogus_changes[i][2]),
                                            subpool);
        change.kind = string_to_kind(bogus_changes[i][3]);
        change.text_mod = bogus_changes[i][4] ? 1 : 0;
        change.prop_mod = bogus_changes[i][5] ? 1 : 0;

        /* Set up transaction baton. */
        args.fs = fs;
        args.key = "x";
        args.change = &change;

        /* Write new changes to the changes table. */
        SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_add, &args,
                                       TRUE, subpool));
      }

    /* Now read 'em back, looking for an error. */
    args.fs = fs;
    args.key = "x";
    err = svn_fs_base__retry_txn(args.fs, txn_body_changes_fetch, &args,
                                 TRUE, subpool);
    if (!err)
      {
        return svn_error_create(SVN_ERR_TEST_FAILED, 0,
                                "Expected SVN_ERR_FS_CORRUPT, got no error.");
      }
    else if (err->apr_err != SVN_ERR_FS_CORRUPT)
      {
        return svn_error_create(SVN_ERR_TEST_FAILED, err,
                                "Expected SVN_ERR_FS_CORRUPT, got a different error.");
      }
    else
      {
        svn_error_clear(err);
      }

    /* Post-test cleanup. */
    SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_delete, &args,
                                   TRUE, subpool));
  }

  /* Test changes bogus because there's a change other than an
     add-type changes on a deleted path. */
  svn_pool_clear(subpool);
  {
    static const char *bogus_changes[][6]
         /* KEY   PATH   NODEREVID  KIND       TEXT PROP */
      = { { "x",  "/foo",  "1.0.0",  "delete",  0 ,  0  },
          { "x",  "/foo",  "1.0.0",  "modify", "1",  0  } };
    int num_changes = sizeof(bogus_changes) / sizeof(const char *) / 6;
    struct changes_args args;
    int i;

    for (i = 0; i < num_changes; i++)
      {
        change_t change;

        /* Set up the current change item. */
        change.path = bogus_changes[i][1];
        change.noderev_id = svn_fs_parse_id(bogus_changes[i][2],
                                            strlen(bogus_changes[i][2]),
                                            subpool);
        change.kind = string_to_kind(bogus_changes[i][3]);
        change.text_mod = bogus_changes[i][4] ? 1 : 0;
        change.prop_mod = bogus_changes[i][5] ? 1 : 0;

        /* Set up transaction baton. */
        args.fs = fs;
        args.key = "x";
        args.change = &change;

        /* Write new changes to the changes table. */
        SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_add, &args,
                                       TRUE, subpool));
      }

    /* Now read 'em back, looking for an error. */
    args.fs = fs;
    args.key = "x";
    err = svn_fs_base__retry_txn(args.fs, txn_body_changes_fetch, &args,
                                 TRUE, subpool);
    if (!err)
      {
        return svn_error_create(SVN_ERR_TEST_FAILED, 0,
                                "Expected SVN_ERR_FS_CORRUPT, got no error.");
      }
    else if (err->apr_err != SVN_ERR_FS_CORRUPT)
      {
        return svn_error_create(SVN_ERR_TEST_FAILED, err,
                                "Expected SVN_ERR_FS_CORRUPT, got a different error.");
      }
    else
      {
        svn_error_clear(err);
      }

    /* Post-test cleanup. */
    SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_delete, &args,
                                   TRUE, subpool));
  }

  /* Test changes bogus because there's an add on a path that's got
     previous non-delete changes on it. */
  svn_pool_clear(subpool);
  {
    static const char *bogus_changes[][6]
         /* KEY   PATH   NODEREVID  KIND       TEXT PROP */
      = { { "x",  "/foo",  "1.0.0",  "modify", "1",  0  },
          { "x",  "/foo",  "1.0.0",  "add",    "1",  0  } };
    int num_changes = sizeof(bogus_changes) / sizeof(const char *) / 6;
    struct changes_args args;
    int i;

    for (i = 0; i < num_changes; i++)
      {
        change_t change;

        /* Set up the current change item. */
        change.path = bogus_changes[i][1];
        change.noderev_id = svn_fs_parse_id(bogus_changes[i][2],
                                            strlen(bogus_changes[i][2]),
                                            subpool);
        change.kind = string_to_kind(bogus_changes[i][3]);
        change.text_mod = bogus_changes[i][4] ? 1 : 0;
        change.prop_mod = bogus_changes[i][5] ? 1 : 0;

        /* Set up transaction baton. */
        args.fs = fs;
        args.key = "x";
        args.change = &change;

        /* Write new changes to the changes table. */
        SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_add, &args,
                                       TRUE, subpool));
      }

    /* Now read 'em back, looking for an error. */
    args.fs = fs;
    args.key = "x";
    err = svn_fs_base__retry_txn(args.fs, txn_body_changes_fetch, &args,
                                 TRUE, subpool);
    if (!err)
      {
        return svn_error_create(SVN_ERR_TEST_FAILED, 0,
                                "Expected SVN_ERR_FS_CORRUPT, got no error.");
      }
    else if (err->apr_err != SVN_ERR_FS_CORRUPT)
      {
        return svn_error_create(SVN_ERR_TEST_FAILED, err,
                                "Expected SVN_ERR_FS_CORRUPT, got a different error.");
      }
    else
      {
        svn_error_clear(err);
      }

    /* Post-test cleanup. */
    SVN_ERR(svn_fs_base__retry_txn(args.fs, txn_body_changes_delete, &args,
                                   TRUE, subpool));
  }

  return SVN_NO_ERROR;
}



/* The test table.  */

struct svn_test_descriptor_t test_funcs[] =
  {
    SVN_TEST_NULL,
    SVN_TEST_OPTS_PASS(changes_add,
                       "add changes to the changes table"),
    SVN_TEST_OPTS_PASS(changes_fetch_raw,
                       "fetch raw changes from the changes table"),
    SVN_TEST_OPTS_PASS(changes_delete,
                       "delete changes from the changes table"),
    SVN_TEST_OPTS_PASS(changes_fetch,
                       "fetch compressed changes from the changes table"),
    SVN_TEST_OPTS_PASS(changes_fetch_ordering,
                       "verify ordered-ness of fetched compressed changes"),
    SVN_TEST_OPTS_PASS(changes_bad_sequences,
                       "verify that bad change sequences raise errors"),
    SVN_TEST_NULL
  };