#!/usr/bin/env python
#
#  svnrdump_tests.py: Tests svnrdump's remote repository dumping capabilities.
#
#  Subversion is a tool for revision control.
#  See http://subversion.apache.org for more information.
#
# ====================================================================
#    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.
######################################################################

# General modules
import sys, os
import re

# Our testing module
import svntest
from svntest.verify import SVNUnexpectedStdout, SVNUnexpectedStderr
from svntest.verify import SVNExpectedStderr
from svntest.main import write_restrictive_svnserve_conf
from svntest.main import server_has_partial_replay

# (abbreviation)
Skip = svntest.testcase.Skip_deco
SkipUnless = svntest.testcase.SkipUnless_deco
XFail = svntest.testcase.XFail_deco
Issues = svntest.testcase.Issues_deco
Issue = svntest.testcase.Issue_deco
Wimp = svntest.testcase.Wimp_deco
Item = svntest.wc.StateItem

## Mismatched headers during dumping operation
# Text-copy-source-* and *-sha1 headers are not provided by the RA
# layer. `svnadmin dump` is able to provide them because it works on
# the FS layer. Also, svnrdump attaches "Prop-delta: true" with
# everything whether it's really a delta or a new prop (delta from
# /dev/null). This is really harmless, but `svnadmin dump` contains
# the logic for differentiating between these two cases.

mismatched_headers_re = re.compile(
    b"Prop-delta: .*|Text-content-sha1: .*|Text-copy-source-md5: .*|" +
    b"Text-copy-source-sha1: .*|Text-delta-base-sha1: .*"
)

######################################################################
# Helper routines

def compare_repos_dumps(sbox, other_dumpfile,
                        bypass_prop_validation=False):
  """Compare two dumpfiles, one created from SBOX, and other given
  by OTHER_DUMPFILE.  The dumpfiles do not need to match linewise, as the
  OTHER_DUMPFILE contents will first be loaded into a repository and then
  re-dumped to do the match, which should generate the same dumpfile as
  dumping SBOX."""


  sbox_dumpfile = svntest.actions.run_and_verify_dump(sbox.repo_dir)

  # Load and dump the other dumpfile (using svnadmin)
  other_sbox = sbox.clone_dependent()
  other_sbox.build(create_wc=False, empty=True)
  svntest.actions.run_and_verify_load(other_sbox.repo_dir, other_dumpfile,
                                      bypass_prop_validation)
  other_dumpfile = svntest.actions.run_and_verify_dump(other_sbox.repo_dir)

  ### This call kind-of assumes EXPECTED is first and ACTUAL is second.
  svntest.verify.compare_dump_files(
    None, None, other_dumpfile, sbox_dumpfile)

def run_dump_test(sbox, dumpfile_name, expected_dumpfile_name = None,
                  subdir = None, bypass_prop_validation = False,
                  ignore_base_checksums = False, extra_options = []):

  """Load a dumpfile using 'svnadmin load', dump it with 'svnrdump
  dump' and check that the same dumpfile is produced or that
  expected_dumpfile_name is produced if provided. Additionally, the
  subdir argument appends itself to the URL.  EXTRA_OPTIONS is an
  array of optional additional options to pass to 'svnrdump dump'."""

  # Create an empty sandbox repository
  sbox.build(create_wc=False, empty=True)

  # This directory contains all the dump files
  svnrdump_tests_dir = os.path.join(os.path.dirname(sys.argv[0]),
                                   'svnrdump_tests_data')

  # Load the specified dump file into the sbox repository using
  # svnadmin load
  original_dumpfile = open(os.path.join(svnrdump_tests_dir,
                                        dumpfile_name),
                           'rb').readlines()
  svntest.actions.run_and_verify_load(sbox.repo_dir, original_dumpfile,
                                      bypass_prop_validation)

  repo_url = sbox.repo_url
  if subdir:
    repo_url = repo_url + subdir

  # Create a dump file using svnrdump
  opts = extra_options + ['-q', 'dump', repo_url]
  svnrdump_dumpfile = \
      svntest.actions.run_and_verify_svnrdump(None, svntest.verify.AnyOutput,
                                              [], 0, *opts)

  if expected_dumpfile_name:
    expected_dumpfile = open(os.path.join(svnrdump_tests_dir,
                                          expected_dumpfile_name),
                             'rb').readlines()
    # Compare the output from stdout
    if ignore_base_checksums:
      expected_dumpfile = [l for l in expected_dumpfile
                                    if not l.startswith(b'Text-delta-base-md5')]
      svnrdump_dumpfile = [l for l in svnrdump_dumpfile
                                    if not l.startswith(b'Text-delta-base-md5')]
    expected_dumpfile = [l for l in expected_dumpfile
                                  if not mismatched_headers_re.match(l)]
    svnrdump_dumpfile = [l for l in svnrdump_dumpfile
                                  if not mismatched_headers_re.match(l)]

    expected_dumpfile = svntest.verify.UnorderedOutput(expected_dumpfile)

    svntest.verify.compare_and_display_lines(
      "Dump files", "DUMP", expected_dumpfile, svnrdump_dumpfile,
      None)

  else:
    # The expected dumpfile is the result of dumping SBOX.
    compare_repos_dumps(sbox, svnrdump_dumpfile, bypass_prop_validation)

def run_load_test(sbox, dumpfile_name, expected_dumpfile_name = None,
                  expect_deltas = True):
  """Load a dumpfile using 'svnrdump load', dump it with 'svnadmin
  dump' and check that the same dumpfile is produced"""

  # Create an empty sandbox repository
  sbox.build(create_wc=False, empty=True)

  # Create the revprop-change hook for this test
  svntest.actions.enable_revprop_changes(sbox.repo_dir)

  # This directory contains all the dump files
  svnrdump_tests_dir = os.path.join(os.path.dirname(sys.argv[0]),
                                   'svnrdump_tests_data')

  # Load the specified dump file into the sbox repository using
  # svnrdump load
  original_dumpfile = open(os.path.join(svnrdump_tests_dir,
                                        dumpfile_name),
                           'rb').readlines()

  # Set the UUID of the sbox repository to the UUID specified in the
  # dumpfile ### RA layer doesn't have a set_uuid functionality
  uuid = original_dumpfile[2].split(b' ')[1][:-1].decode()
  svntest.actions.run_and_verify_svnadmin2(None, None, 0,
                                           'setuuid', sbox.repo_dir,
                                           uuid)

  svntest.actions.run_and_verify_svnrdump(original_dumpfile,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)

  # Re-dump the rdump-loaded repo using svnadmin dump
  resulted_dumpfile = svntest.actions.run_and_verify_dump(sbox.repo_dir,
                                                          expect_deltas)

  if expected_dumpfile_name:
    expected_dumpfile = open(os.path.join(svnrdump_tests_dir,
                                          expected_dumpfile_name),
                             'rb').readlines()

    # Compare the output from stdout
    svntest.verify.compare_and_display_lines(
      "Dump files", "DUMP", expected_dumpfile, resulted_dumpfile)

  else:
    expected_dumpfile = original_dumpfile
    compare_repos_dumps(sbox, expected_dumpfile)

######################################################################
# Tests

def basic_dump(sbox):
  "dump: standard sbox repos"
  sbox.build(read_only = True, create_wc = False)

  out = \
      svntest.actions.run_and_verify_svnrdump(None, svntest.verify.AnyOutput,
                                              [], 0, '-q', 'dump',
                                              sbox.repo_url)

  if not out[0].startswith(b'SVN-fs-dump-format-version:'):
    raise svntest.Failure('No valid output')

def revision_0_dump(sbox):
  "dump: revision zero"
  run_dump_test(sbox, "revision-0.dump")

def revision_0_load(sbox):
  "load: revision zero"
  run_load_test(sbox, "revision-0.dump")

# skeleton.dump repository layout
#
#   Projects/       (Added r1)
#     README        (Added r2)
#     Project-X     (Added r3)
#     Project-Y     (Added r4)
#     Project-Z     (Added r5)
#     docs/         (Added r6)
#       README      (Added r6)

def skeleton_dump(sbox):
  "dump: skeleton repository"
  run_dump_test(sbox, "skeleton.dump")

def skeleton_load(sbox):
  "load: skeleton repository"
  run_load_test(sbox, "skeleton.dump")

def sparse_propchanges_dump(sbox):
  "dump: sparse file/dir propchanges"
  run_dump_test(sbox, "sparse-propchanges.dump")

@Issue(3902)
def sparse_propchanges_load(sbox):
  "load: sparse file/dir propchanges"
  run_load_test(sbox, "sparse-propchanges.dump")

def copy_and_modify_dump(sbox):
  "dump: copy and modify"
  run_dump_test(sbox, "copy-and-modify.dump")

def copy_and_modify_load(sbox):
  "load: copy and modify"
  run_load_test(sbox, "copy-and-modify.dump")

def no_author_dump(sbox):
  "dump: copy revs with no svn:author revprops"
  run_dump_test(sbox, "no-author.dump")

def no_author_load(sbox):
  "load: copy revs with no svn:author revprops"
  run_load_test(sbox, "no-author.dump")

def copy_from_previous_version_and_modify_dump(sbox):
  "dump: copy from previous version and modify"
  run_dump_test(sbox, "copy-from-previous-version-and-modify.dump")

def copy_from_previous_version_and_modify_load(sbox):
  "load: copy from previous version and modify"
  run_load_test(sbox, "copy-from-previous-version-and-modify.dump")

def modified_in_place_dump(sbox):
  "dump: modified in place"
  run_dump_test(sbox, "modified-in-place.dump")

def modified_in_place_load(sbox):
  "load: modified in place"
  run_load_test(sbox, "modified-in-place.dump")

def move_and_modify_in_the_same_revision_dump(sbox):
  "dump: move parent & modify child file in same rev"
  run_dump_test(sbox, "move-and-modify.dump")

def move_and_modify_in_the_same_revision_load(sbox):
  "load: move parent & modify child file in same rev"
  run_load_test(sbox, "move-and-modify.dump")

def tag_empty_trunk_dump(sbox):
  "dump: tag empty trunk"
  run_dump_test(sbox, "tag-empty-trunk.dump")

def tag_empty_trunk_load(sbox):
  "load: tag empty trunk"
  run_load_test(sbox, "tag-empty-trunk.dump")

def tag_trunk_with_file_dump(sbox):
  "dump: tag trunk containing a file"
  run_dump_test(sbox, "tag-trunk-with-file.dump")

def tag_trunk_with_file_load(sbox):
  "load: tag trunk containing a file"
  run_load_test(sbox, "tag-trunk-with-file.dump")

def tag_trunk_with_file2_dump(sbox):
  "dump: tag trunk containing a file (#2)"
  run_dump_test(sbox, "tag-trunk-with-file2.dump")

def tag_trunk_with_file2_load(sbox):
  "load: tag trunk containing a file (#2)"
  run_load_test(sbox, "tag-trunk-with-file2.dump")

def dir_prop_change_dump(sbox):
  "dump: directory property changes"
  run_dump_test(sbox, "dir-prop-change.dump")

def dir_prop_change_load(sbox):
  "load: directory property changes"
  run_load_test(sbox, "dir-prop-change.dump")

def copy_parent_modify_prop_dump(sbox):
  "dump: copy parent and modify prop"
  run_dump_test(sbox, "copy-parent-modify-prop.dump")

def copy_parent_modify_prop_load(sbox):
  "load: copy parent and modify prop"
  run_load_test(sbox, "copy-parent-modify-prop.dump")

def copy_revprops_dump(sbox):
  "dump: copy revprops other than svn:*"
  run_dump_test(sbox, "revprops.dump")

def copy_revprops_load(sbox):
  "load: copy revprops other than svn:*"
  run_load_test(sbox, "revprops.dump")

def only_trunk_dump(sbox):
  "dump: subdirectory"
  run_dump_test(sbox, "trunk-only.dump", subdir="/trunk",
                expected_dumpfile_name="trunk-only.expected.dump")

def only_trunk_A_with_changes_dump(sbox):
  "dump: subdirectory with changes on root"
  run_dump_test(sbox, "trunk-A-changes.dump", subdir="/trunk/A",
           expected_dumpfile_name="trunk-A-changes.expected.dump")

def url_encoding_dump(sbox):
  "dump: url encoding issues"
  run_dump_test(sbox, "url-encoding-bug.dump")

def url_encoding_load(sbox):
  "load: url encoding issues"
  run_load_test(sbox, "url-encoding-bug.dump")

def copy_bad_line_endings_dump(sbox):
  "dump: inconsistent line endings in svn:* props"
  run_dump_test(sbox, "copy-bad-line-endings.dump",
                expected_dumpfile_name="copy-bad-line-endings.expected.dump",
                bypass_prop_validation=True)

@Issue(4263)
def copy_bad_line_endings_load(sbox):
  "load: inconsistent line endings in svn:* props"
  run_load_test(sbox, "copy-bad-line-endings.dump",
                expected_dumpfile_name="copy-bad-line-endings.expected.dump")

def copy_bad_line_endings2_dump(sbox):
  "dump: non-LF line endings in svn:* props"
  run_dump_test(sbox, "copy-bad-line-endings2.dump",
                expected_dumpfile_name="copy-bad-line-endings2.expected.dump",
                bypass_prop_validation=True, ignore_base_checksums=True)

def commit_a_copy_of_root_dump(sbox):
  "dump: commit a copy of root"
  run_dump_test(sbox, "repo-with-copy-of-root-dir.dump")

def commit_a_copy_of_root_load(sbox):
  "load: commit a copy of root"
  run_load_test(sbox, "repo-with-copy-of-root-dir.dump")

def descend_into_replace_dump(sbox):
  "dump: descending into replaced dir looks in src"
  run_dump_test(sbox, "descend-into-replace.dump", subdir='/trunk/H',
                expected_dumpfile_name = "descend-into-replace.expected.dump")

def descend_into_replace_load(sbox):
  "load: descending into replaced dir looks in src"
  run_load_test(sbox, "descend-into-replace.dump")

@Issue(3847)
def add_multi_prop_dump(sbox):
  "dump: add with multiple props"
  run_dump_test(sbox, "add-multi-prop.dump")

@Issue(3844)
def multi_prop_edit_load(sbox):
  "load: multiple prop edits on a file"
  run_load_test(sbox, "multi-prop-edits.dump", None, False)

#----------------------------------------------------------------------
# This test replicates svnadmin_tests.py 16 'reflect dropped renumbered
# revs in svn:mergeinfo' but uses 'svnrdump load' in place of
# 'svnadmin load'.
@Issue(3890)
def reflect_dropped_renumbered_revs(sbox):
  "svnrdump renumbers dropped revs in mergeinfo"

  # Create an empty sandbox repository
  sbox.build(create_wc=False, empty=True)

  # Create the revprop-change hook for this test
  svntest.actions.enable_revprop_changes(sbox.repo_dir)

  # Load the specified dump file into the sbox repository using
  # svnrdump load
  dump_file = open(os.path.join(os.path.dirname(sys.argv[0]),
                                                'svnrdump_tests_data',
                                                'with_merges.dump'),
                   'rb')
  svnrdump_dumpfile = dump_file.readlines()
  dump_file.close()

  # svnrdump load the dump file.
  svntest.actions.run_and_verify_svnrdump(svnrdump_dumpfile,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)

  # Create the 'toplevel' directory in repository and then load the same
  # dumpfile into that subtree.
  svntest.actions.run_and_verify_svn(['Committing transaction...\n',
                                            'Committed revision 10.\n'],
                                    [], "mkdir", sbox.repo_url + "/toplevel",
                                     "-m", "Create toplevel dir to load into")
  svntest.actions.run_and_verify_svnrdump(svnrdump_dumpfile,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load',
                                          sbox.repo_url + "/toplevel")
  # Verify the svn:mergeinfo properties
  url = sbox.repo_url
  expected_output = svntest.verify.UnorderedOutput([
    url + "/trunk - /branch1:4-8\n",
    url + "/toplevel/trunk - /toplevel/branch1:14-18\n",
    ])
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'propget', 'svn:mergeinfo', '-R',
                                     sbox.repo_url)

#----------------------------------------------------------------------
# Full or incremental dump-load cycles should result in the same
# mergeinfo in the loaded repository.
#
# Given a repository 'SOURCE-REPOS' with mergeinfo, and a repository
# 'TARGET-REPOS' (which may or may not be empty), either of the following
# methods to move 'SOURCE-REPOS' to 'TARGET-REPOS' should result in
# the same mergeinfo on 'TARGET-REPOS':
#
#   1) Dump -r1:HEAD from 'SOURCE-REPOS' and load it in one shot to
#      'TARGET-REPOS'.
#
#   2) Dump 'SOURCE-REPOS' in a series of incremental dumps and load
#      each of them to 'TARGET-REPOS'.
#
# See https://issues.apache.org/jira/browse/SVN-3020#desc13
#
# This test replicates svnadmin_tests.py 20 'don't filter mergeinfo revs
# from incremental dump' but uses 'svnrdump [dump|load]' in place of
# 'svnadmin [dump|load]'.
@Issue(3890)
def dont_drop_valid_mergeinfo_during_incremental_svnrdump_loads(sbox):
  "don't drop mergeinfo revs in incremental svnrdump"

  # Create an empty repos.
  sbox.build(empty=True)

  # Create the revprop-change hook for this test
  svntest.actions.enable_revprop_changes(sbox.repo_dir)

  # PART 1: Load a full dump to an empty repository.
  #
  # The test repository used here, 'mergeinfo_included_full.dump', is
  # this repos:
  #                       __________________________________________
  #                      |                                         |
  #                      |             ____________________________|_____
  #                      |            |                            |     |
  # trunk---r2---r3-----r5---r6-------r8---r9--------------->      |     |
  #   r1             |        |     |       |                      |     |
  # initial          |        |     |       |______                |     |
  # import         copy       |   copy             |            merge   merge
  #                  |        |     |            merge           (r5)   (r8)
  #                  |        |     |            (r9)              |     |
  #                  |        |     |              |               |     |
  #                  |        |     V              V               |     |
  #                  |        | branches/B2-------r11---r12---->   |     |
  #                  |        |     r7              |____|         |     |
  #                  |        |                        |           |     |
  #                  |      merge                      |___        |     |
  #                  |      (r6)                           |       |     |
  #                  |        |_________________           |       |     |
  #                  |                          |        merge     |     |
  #                  |                          |      (r11-12)    |     |
  #                  |                          |          |       |     |
  #                  V                          V          V       |     |
  #              branches/B1-------------------r10--------r13-->   |     |
  #                  r4                                            |     |
  #                   |                                            V     V
  #                  branches/B1/B/E------------------------------r14---r15->
  #
  #
  # The mergeinfo on this repos@15 is:
  #
  #   Properties on 'branches/B1':
  #     svn:mergeinfo
  #       /branches/B2:11-12
  #       /trunk:6,9
  #   Properties on 'branches/B1/B/E':
  #     svn:mergeinfo
  #       /branches/B2/B/E:11-12
  #       /trunk/B/E:5-6,8-9
  #   Properties on 'branches/B2':
  #     svn:mergeinfo
  #       /trunk:9
  dump_fp = open(os.path.join(os.path.dirname(sys.argv[0]),
                              'svnrdump_tests_data',
                              'mergeinfo_included_full.dump'),
                 'rb')
  dumpfile_full = dump_fp.readlines()
  dump_fp.close()

  svntest.actions.run_and_verify_svnrdump(dumpfile_full,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)

  # Check that the mergeinfo is as expected.
  url = sbox.repo_url + '/branches/'
  expected_output = svntest.verify.UnorderedOutput([
    url + "B1 - /branches/B2:11-12\n",
    "/trunk:6,9\n",
    url + "B2 - /trunk:9\n",
    url + "B1/B/E - /branches/B2/B/E:11-12\n",
    "/trunk/B/E:5-6,8-9\n"])
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'propget', 'svn:mergeinfo', '-R',
                                     sbox.repo_url)

  # PART 2: Load a series of incremental dumps to an empty repository.
  #
  # Incrementally dump the repository into three dump files:
  dump_file_r1_10 = sbox.get_tempname("r1-10-dump")
  output = svntest.actions.run_and_verify_svnrdump(None,
                                                   svntest.verify.AnyOutput,
                                                   [], 0, '-q', 'dump', '-r1:10',
                                                   sbox.repo_url)
  dump_fp = open(dump_file_r1_10, 'wb')
  dump_fp.writelines(output)
  dump_fp.close()

  dump_file_r11_13 = sbox.get_tempname("r11-13-dump")
  output = svntest.actions.run_and_verify_svnrdump(None,
                                                   svntest.verify.AnyOutput,
                                                   [], 0, '-q', 'dump',
                                                   '--incremental', '-r11:13',
                                                   sbox.repo_url)
  dump_fp = open(dump_file_r11_13, 'wb')
  dump_fp.writelines(output)
  dump_fp.close()

  dump_file_r14_15 = sbox.get_tempname("r14-15-dump")
  output = svntest.actions.run_and_verify_svnrdump(None,
                                                   svntest.verify.AnyOutput,
                                                   [], 0, '-q', 'dump',
                                                   '--incremental', '-r14:15',
                                                   sbox.repo_url)
  dump_fp = open(dump_file_r14_15, 'wb')
  dump_fp.writelines(output)
  dump_fp.close()

  # Blow away the current repos and create an empty one in its place.
  svntest.main.safe_rmtree(sbox.repo_dir, True) # Fix race with bdb in svnserve
  sbox.build(empty=True)

  # Create the revprop-change hook for this test
  svntest.actions.enable_revprop_changes(sbox.repo_dir)

  # Load the three incremental dump files in sequence.
  dump_fp = open(dump_file_r1_10, 'rb')
  svntest.actions.run_and_verify_svnrdump(dump_fp.readlines(),
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)
  dump_fp.close()
  dump_fp = open(dump_file_r11_13, 'rb')
  svntest.actions.run_and_verify_svnrdump(dump_fp.readlines(),
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)
  dump_fp.close()
  dump_fp = open(dump_file_r14_15, 'rb')
  svntest.actions.run_and_verify_svnrdump(dump_fp.readlines(),
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)
  dump_fp.close()

  # Check the mergeinfo, we use the same expected output as before,
  # as it (duh!) should be exactly the same as when we loaded the
  # repos in one shot.
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'propget', 'svn:mergeinfo', '-R',
                                     sbox.repo_url)

  # Now repeat the above two scenarios, but with an initially non-empty target
  # repository.  First, try the full dump-load in one shot.
  #
  # PART 3: Load a full dump to an non-empty repository.
  #
  # Reset our sandbox.
  svntest.main.safe_rmtree(sbox.repo_dir, True) # Fix race with bdb in svnserve
  sbox.build(empty=True)

  # Create the revprop-change hook for this test
  svntest.actions.enable_revprop_changes(sbox.repo_dir)

  # Load this skeleton repos into the empty target:
  #
  #   Projects/       (Added r1)
  #     README        (Added r2)
  #     Project-X     (Added r3)
  #     Project-Y     (Added r4)
  #     Project-Z     (Added r5)
  #     docs/         (Added r6)
  #       README      (Added r6)
  dump_fp = open(os.path.join(os.path.dirname(sys.argv[0]),
                              'svnrdump_tests_data',
                              'skeleton.dump'),
                 'rb')
  dumpfile_skeleton = dump_fp.readlines()
  dump_fp.close()
  svntest.actions.run_and_verify_svnrdump(dumpfile_skeleton,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)

  # Load 'svnadmin_tests_data/mergeinfo_included_full.dump' in one shot:
  svntest.actions.run_and_verify_svnrdump(dumpfile_full,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load',
                                          sbox.repo_url + '/Projects/Project-X')

  # Check that the mergeinfo is as expected.  This is exactly the
  # same expected mergeinfo we previously checked, except that the
  # revisions are all offset +6 to reflect the revions already in
  # the skeleton target before we began loading and the leading source
  # paths are adjusted by the --parent-dir:
  #
  #   Properties on 'branches/B1':
  #     svn:mergeinfo
  #       /Projects/Project-X/branches/B2:17-18
  #       /Projects/Project-X/trunk:12,15
  #   Properties on 'branches/B1/B/E':
  #     svn:mergeinfo
  #       /Projects/Project-X/branches/B2/B/E:17-18
  #       /Projects/Project-X/trunk/B/E:11-12,14-15
  #   Properties on 'branches/B2':
  #     svn:mergeinfo
  #       /Projects/Project-X/trunk:15
  url = sbox.repo_url + '/Projects/Project-X/branches/'
  expected_output = svntest.verify.UnorderedOutput([
    url + "B1 - /Projects/Project-X/branches/B2:17-18\n",
    "/Projects/Project-X/trunk:12,15\n",
    url + "B2 - /Projects/Project-X/trunk:15\n",
    url + "B1/B/E - /Projects/Project-X/branches/B2/B/E:17-18\n",
    "/Projects/Project-X/trunk/B/E:11-12,14-15\n"])
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'propget', 'svn:mergeinfo', '-R',
                                     sbox.repo_url)

  # PART 4: Load a a series of incremental dumps to an non-empty repository.
  #
  # Reset our sandbox.
  svntest.main.safe_rmtree(sbox.repo_dir, True) # Fix race with bdb in svnserve
  sbox.build(empty=True)

  # Create the revprop-change hook for this test
  svntest.actions.enable_revprop_changes(sbox.repo_dir)

  # Load the skeleton repos into the empty target:
  svntest.actions.run_and_verify_svnrdump(dumpfile_skeleton,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)

  # Load the three incremental dump files in sequence.
  #
  # The first load fails the same as PART 3.
  dump_fp = open(dump_file_r1_10, 'rb')
  svntest.actions.run_and_verify_svnrdump(dump_fp.readlines(),
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load',
                                          sbox.repo_url + '/Projects/Project-X')
  dump_fp.close()
  dump_fp = open(dump_file_r11_13, 'rb')
  svntest.actions.run_and_verify_svnrdump(dump_fp.readlines(),
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load',
                                          sbox.repo_url + '/Projects/Project-X')
  dump_fp.close()
  dump_fp = open(dump_file_r14_15, 'rb')
  svntest.actions.run_and_verify_svnrdump(dump_fp.readlines(),
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load',
                                          sbox.repo_url + '/Projects/Project-X')
  dump_fp.close()

  # Check the resulting mergeinfo.  We expect the exact same results
  # as Part 3.
  # See https://issues.apache.org/jira/browse/SVN-3020#desc16.
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'propget', 'svn:mergeinfo', '-R',
                                     sbox.repo_url)

#----------------------------------------------------------------------
@Issue(3890)
def svnrdump_load_partial_incremental_dump(sbox):
  "svnrdump load partial incremental dump"

  # Create an empty sandbox repository
  sbox.build(create_wc=False, empty=True)

  # Create the revprop-change hook for this test
  svntest.actions.enable_revprop_changes(sbox.repo_dir)

  # Create the 'A' directory in repository and then load the partial
  # incremental dump into the root of the repository.
  svntest.actions.run_and_verify_svn(['Committing transaction...\n',
                                            'Committed revision 1.\n'],
                                    [], "mkdir", sbox.repo_url + "/A",
                                     "-m", "Create toplevel dir to load into")

  # Load the specified dump file into the sbox repository using
  # svnrdump load
  dump_file = open(os.path.join(os.path.dirname(sys.argv[0]),
                                                'svnrdump_tests_data',
                                                'partial_incremental.dump'),
                   'rb')
  svnrdump_dumpfile = dump_file.readlines()
  dump_file.close()
  svntest.actions.run_and_verify_svnrdump(svnrdump_dumpfile,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', sbox.repo_url)


#----------------------------------------------------------------------
@Issue(4101)
def range_dump(sbox):
  "dump: using -rX:Y"
  run_dump_test(sbox, "trunk-only.dump",
                expected_dumpfile_name="root-range.expected.dump",
                extra_options=['-r2:HEAD'])

@Issue(4101)
def only_trunk_range_dump(sbox):
  "dump: subdirectory using -rX:Y"
  run_dump_test(sbox, "trunk-only.dump", subdir="/trunk",
                expected_dumpfile_name="trunk-only-range.expected.dump",
                extra_options=['-r1:HEAD'])

@Issue(4101)
def only_trunk_A_range_dump(sbox):
  "dump: deeper subdirectory using -rX:Y"
  run_dump_test(sbox, "trunk-only.dump", subdir="/trunk/A",
                expected_dumpfile_name="trunk-A-range.expected.dump",
                extra_options=['-r2:HEAD'])


#----------------------------------------------------------------------

@Issue(4490)
def load_prop_change_in_non_deltas_dump(sbox):
  "load: prop change in non-deltas dump"
  # 'svnrdump load' crashed when processing a node record with a non-delta
  # properties block if the node previously had any svn:* properties.

  sbox.build()
  sbox.simple_propset('svn:eol-style', 'native', 'iota', 'A/mu', 'A/B/lambda')
  sbox.simple_commit()

  # Any prop change on a node that had an svn:* prop triggered the crash,
  # so test an svn:* prop deletion and also some other prop changes.
  sbox.simple_propdel('svn:eol-style', 'iota')
  sbox.simple_propset('svn:eol-style', 'LF', 'A/mu')
  sbox.simple_propset('p1', 'v1', 'A/B/lambda')
  sbox.simple_commit()

  # Create a non-deltas dump. Use 'svnadmin', as svnrdump doesn't have that
  # option.
  dump = svntest.actions.run_and_verify_dump(sbox.repo_dir, deltas=False)

  # Try to load that dump.
  sbox.build(create_wc=False, empty=True)
  svntest.actions.enable_revprop_changes(sbox.repo_dir)
  svntest.actions.run_and_verify_svnrdump(dump,
                                          [], [], 0,
                                          '-q', 'load', sbox.repo_url)

#----------------------------------------------------------------------

@Issue(4476)
def dump_mergeinfo_contains_r0(sbox):
  "dump: mergeinfo that contains r0"
  ### We pass the original dump file name as 'expected_dumpfile_name' because
  ### run_dump_test is currently broken when we don't.
  run_dump_test(sbox, "mergeinfo-contains-r0.dump",
                bypass_prop_validation=True)

#----------------------------------------------------------------------

@XFail()
@Issue(4476)
def load_mergeinfo_contains_r0(sbox):
  "load: mergeinfo that contains r0"
  run_load_test(sbox, "mergeinfo-contains-r0.dump",
                expected_dumpfile_name="mergeinfo-contains-r0.expected.dump")

#----------------------------------------------------------------------

# Regression test for issue 4551 "svnrdump load commits wrong properties,
# or fails, on a non-deltas dumpfile". In this test, the copy source does
# not exist and the failure mode is to error out.
@Issue(4551)
def load_non_deltas_copy_with_props(sbox):
  "load non-deltas copy with props"
  sbox.build()

  # Case (1): Copies that do not replace anything: the copy target path
  # at (new rev - 1) does not exist

  # Set properties on each node to be copied
  sbox.simple_propset('p', 'v', 'A/mu', 'A/B', 'A/B/E')
  sbox.simple_propset('q', 'v', 'A/mu', 'A/B', 'A/B/E')
  sbox.simple_commit()
  sbox.simple_update()  # avoid mixed-rev

  # Do the copies
  sbox.simple_copy('A/mu@2', 'A/mu_COPY')
  sbox.simple_copy('A/B@2', 'A/B_COPY')
  # Also add new nodes inside the copied dir, to test more code paths
  sbox.simple_copy('A/B/E@2', 'A/B_COPY/copied')
  sbox.simple_mkdir('A/B_COPY/added')
  sbox.simple_copy('A/B/E@2', 'A/B_COPY/added/copied')
  # On each copied node, delete a prop
  sbox.simple_propdel('p', 'A/mu_COPY', 'A/B_COPY', 'A/B_COPY/E',
                           'A/B_COPY/copied', 'A/B_COPY/added/copied')

  sbox.simple_commit()

  # Dump with 'svnadmin' (non-deltas mode)
  dumpfile = svntest.actions.run_and_verify_dump(sbox.repo_dir, deltas=False)

  # Load with 'svnrdump'. This used to throw an error:
  # svnrdump: E160013: File not found: revision 2, path '/A/B_COPY'
  new_repo_dir, new_repo_url = sbox.add_repo_path('new_repo')
  svntest.main.create_repos(new_repo_dir)
  svntest.actions.enable_revprop_changes(new_repo_dir)
  svntest.actions.run_and_verify_svnrdump(dumpfile,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', new_repo_url)

  # Check that property 'p' really was deleted on each copied node
  for tgt_path in ['A/mu_COPY', 'A/B_COPY', 'A/B_COPY/E',
                   'A/B_COPY/copied', 'A/B_COPY/added/copied']:
    tgt_url = new_repo_url + '/' + tgt_path
    _, out, _ = svntest.main.run_svn(None, 'proplist', tgt_url)
    expected = ["Properties on '%s':" % (tgt_url,),
                'q']
    actual = map(str.strip, out)
    svntest.verify.compare_and_display_lines(None, 'PROPS', expected, actual)

# Regression test for issue 4551 "svnrdump load commits wrong properties,
# or fails, on a non-deltas dumpfile". In this test, the copy source does
# exist and the failure mode is to fail to delete a property.
@Issue(4551)
def load_non_deltas_replace_copy_with_props(sbox):
  "load non-deltas replace&copy with props"
  sbox.build()

  # Case (2): Copies that replace something: the copy target path
  # at (new rev - 1) exists and has no property named 'p'

  # Set props on the copy source nodes (a file, a dir, a child of the dir)
  sbox.simple_propset('p', 'v', 'A/mu', 'A/B', 'A/B/E')
  sbox.simple_propset('q', 'v', 'A/mu', 'A/B', 'A/B/E')
  sbox.simple_commit()
  sbox.simple_update()  # avoid mixed-rev

  # Do the copies, replacing something
  sbox.simple_rm('A/D/gamma', 'A/C')
  sbox.simple_copy('A/mu@2', 'A/D/gamma')
  sbox.simple_copy('A/B@2', 'A/C')
  # On the copy, delete a prop that wasn't present on the node that it replaced
  sbox.simple_propdel('p', 'A/D/gamma', 'A/C', 'A/C/E')

  sbox.simple_commit()

  # Dump with 'svnadmin' (non-deltas mode)
  dumpfile = svntest.actions.run_and_verify_dump(sbox.repo_dir, deltas=False)

  # Load with 'svnrdump'
  new_repo_dir, new_repo_url = sbox.add_repo_path('new_repo')
  svntest.main.create_repos(new_repo_dir)
  svntest.actions.enable_revprop_changes(new_repo_dir)
  svntest.actions.run_and_verify_svnrdump(dumpfile,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', new_repo_url)

  # Check that property 'p' really was deleted on each copied node
  # This used to fail, finding that property 'p' was still present
  for tgt_path in ['A/D/gamma', 'A/C', 'A/C/E']:
    tgt_url = new_repo_url + '/' + tgt_path
    _, out, _ = svntest.main.run_svn(None, 'proplist', tgt_url)
    expected = ["Properties on '%s':" % (tgt_url,),
                'q']
    actual = map(str.strip, out)
    svntest.verify.compare_and_display_lines(None, 'PROPS', expected, actual)

# Regression test for issue #4552 "svnrdump writes duplicate headers for a
# replace-with-copy". 'svnrdump dump' wrote the Node-path and Node-kind
# headers twice for the 'delete' record of a replace-with-copy.
@Issue(4552)
def dump_replace_with_copy(sbox):
  "dump replace with copy"
  sbox.build()

  # Copy file/dir, replacing something
  sbox.simple_rm('A/D/gamma', 'A/C')
  sbox.simple_copy('A/mu@1', 'A/D/gamma')
  sbox.simple_copy('A/B@1', 'A/C')
  sbox.simple_commit()

  # Dump with 'svnrdump'
  dumpfile = svntest.actions.run_and_verify_svnrdump(
                               None, svntest.verify.AnyOutput, [], 0,
                               'dump', '--quiet', '--incremental', '-r2',
                               sbox.repo_url)

  # Check the 'delete' record headers: expect this parse to fail if headers
  # are duplicated
  svntest.verify.DumpParser(dumpfile).parse()

# Regression test for issue 4551 "svnrdump load commits wrong properties,
# or fails, on a non-deltas dumpfile". In this test, a node's props are
# modified, and the failure mode is that RA-serf would end up deleting
# properties that should remain on the node.
@Issue(4551)
def load_non_deltas_with_props(sbox):
  "load non-deltas with props"
  sbox.build()

  # Case (3): A node's props are modified, and at least one of its previous
  # props remains after the modification. svnrdump made two prop mod method
  # calls for the same property (delete, then set). RA-serf's commit editor
  # didn't expect this and performed the deletes after the non-deletes, and
  # so ended up deleting a property that should not be deleted.

  # Set properties on each node to be modified
  sbox.simple_propset('p', 'v', 'A/mu')
  sbox.simple_propset('q', 'v', 'A/mu', 'A/B')
  sbox.simple_commit()

  # Do the modifications: a different kind of mod on each node
  sbox.simple_propdel('p', 'A/mu')
  sbox.simple_propset('q', 'v2', 'A/B')
  sbox.simple_commit()

  # Dump with 'svnadmin' (non-deltas mode)
  dumpfile = svntest.actions.run_and_verify_dump(sbox.repo_dir, deltas=False)

  # Load with 'svnrdump'
  new_repo_dir, new_repo_url = sbox.add_repo_path('new_repo')
  svntest.main.create_repos(new_repo_dir)
  svntest.actions.enable_revprop_changes(new_repo_dir)
  svntest.actions.run_and_verify_svnrdump(dumpfile,
                                          svntest.verify.AnyOutput,
                                          [], 0, 'load', new_repo_url)

  # Check that property 'q' remains on each modified node
  for tgt_path in ['A/mu', 'A/B']:
    tgt_url = new_repo_url + '/' + tgt_path
    _, out, _ = svntest.main.run_svn(None, 'proplist', tgt_url)
    expected = ["Properties on '%s':" % (tgt_url,),
                'q']
    actual = map(str.strip, out)
    svntest.verify.compare_and_display_lines(None, 'PROPS', expected, actual)

########################################################################
# Run the tests


# list all tests here, starting with None:
test_list = [ None,
              basic_dump,
              revision_0_dump,
              revision_0_load,
              skeleton_dump,
              skeleton_load,
              sparse_propchanges_dump,
              sparse_propchanges_load,
              copy_and_modify_dump,
              copy_and_modify_load,
              copy_from_previous_version_and_modify_dump,
              copy_from_previous_version_and_modify_load,
              modified_in_place_dump,
              modified_in_place_load,
              tag_empty_trunk_dump,
              tag_empty_trunk_load,
              tag_trunk_with_file_dump,
              tag_trunk_with_file_load,
              tag_trunk_with_file2_dump,
              tag_trunk_with_file2_load,
              dir_prop_change_dump,
              dir_prop_change_load,
              copy_parent_modify_prop_dump,
              copy_parent_modify_prop_load,
              url_encoding_dump,
              url_encoding_load,
              copy_revprops_dump,
              copy_revprops_load,
              only_trunk_dump,
              only_trunk_A_with_changes_dump,
              no_author_dump,
              no_author_load,
              move_and_modify_in_the_same_revision_dump,
              move_and_modify_in_the_same_revision_load,
              copy_bad_line_endings_dump,
              copy_bad_line_endings_load,
              copy_bad_line_endings2_dump,
              commit_a_copy_of_root_dump,
              commit_a_copy_of_root_load,
              descend_into_replace_dump,
              descend_into_replace_load,
              add_multi_prop_dump,
              multi_prop_edit_load,
              reflect_dropped_renumbered_revs,
              dont_drop_valid_mergeinfo_during_incremental_svnrdump_loads,
              svnrdump_load_partial_incremental_dump,
              range_dump,
              only_trunk_range_dump,
              only_trunk_A_range_dump,
              load_prop_change_in_non_deltas_dump,
              dump_mergeinfo_contains_r0,
              load_mergeinfo_contains_r0,
              load_non_deltas_copy_with_props,
              load_non_deltas_replace_copy_with_props,
              dump_replace_with_copy,
              load_non_deltas_with_props,
             ]

if __name__ == '__main__':
  svntest.main.run_tests(test_list)
  # NOTREACHED


### End of file.
