#!/usr/bin/env python
#
#  svnmucc_tests.py: tests of svnmucc
#
#  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.
######################################################################

import svntest
import re

XFail = svntest.testcase.XFail_deco
Issues = svntest.testcase.Issues_deco
Issue = svntest.testcase.Issue_deco

######################################################################

@Issues(3895,3953)
def reject_bogus_mergeinfo(sbox):
  "reject bogus mergeinfo"

  sbox.build(create_wc=False)

  expected_error = ".*(E200020.*Invalid revision|E175002.*PROPPATCH)"

  # At present this tests the server, but if we ever make svnmucc
  # validate the mergeinfo up front then it will only test the client
  svntest.actions.run_and_verify_svnmucc([], expected_error,
                                         'propset', 'svn:mergeinfo', '/B:0',
                                         '-m', 'log msg',
                                         sbox.repo_url + '/A')

_svnmucc_re = re.compile(b'^(r[0-9]+) committed by jrandom at (.*)$')
_log_re = re.compile('^   ([ADRM] /[^\(]+($| \(from .*:[0-9]+\)$))')
_err_re = re.compile('^svnmucc: (.*)$')

def test_svnmucc(repo_url, expected_path_changes, *varargs):
  """Run svnmucc with the list of SVNMUCC_ARGS arguments.  Verify that
  its run results in a new commit with 'svn log -rHEAD' changed paths
  that match the list of EXPECTED_PATH_CHANGES."""

  # First, run svnmucc.
  exit_code, outlines, errlines = svntest.main.run_svnmucc('-U', repo_url,
                                                           *varargs)
  if errlines:
    raise svntest.main.SVNCommitFailure(str(errlines))
  if len(outlines) != 1 or not _svnmucc_re.match(outlines[0]):
    raise svntest.main.SVNLineUnequal(str(outlines))

  # Now, run 'svn log -vq -rHEAD'
  changed_paths = []
  exit_code, outlines, errlines = \
    svntest.main.run_svn(None, 'log', '-vqrHEAD', repo_url)
  if errlines:
    raise svntest.Failure("Unable to verify commit with 'svn log': %s"
                          % (str(errlines)))
  for line in outlines:
    match = _log_re.match(line)
    if match:
      changed_paths.append(match.group(1).rstrip('\n\r'))

  expected_path_changes.sort()
  changed_paths.sort()
  if changed_paths != expected_path_changes:
    raise svntest.Failure("Logged path changes differ from expectations\n"
                          "   expected: %s\n"
                          "     actual: %s" % (str(expected_path_changes),
                                               str(changed_paths)))

def xtest_svnmucc(repo_url, expected_errors, *varargs):
  """Run svnmucc with the list of SVNMUCC_ARGS arguments.  Verify that
  its run results match the list of EXPECTED_ERRORS."""

  # First, run svnmucc.
  exit_code, outlines, errlines = svntest.main.run_svnmucc('-U', repo_url,
                                                           *varargs)
  errors = []
  for line in errlines:
    match = _err_re.match(line)
    if match:
      errors.append(line.rstrip('\n\r'))
  if errors != expected_errors:
    raise svntest.main.SVNUnmatchedError(str(errors))


def basic_svnmucc(sbox):
  "basic svnmucc tests"

  sbox.build()
  empty_file = sbox.ospath('empty')
  file = sbox.ospath('file')
  svntest.main.file_append(empty_file, '')
  svntest.main.file_append(file, 'file')

  # revision 2
  test_svnmucc(sbox.repo_url,
               ['A /foo'
                ], # ---------
               '-m', 'log msg',
               'mkdir', 'foo')

  # revision 3
  test_svnmucc(sbox.repo_url,
               ['A /z.c',
                ], # ---------
               '-m', 'log msg',
               'put', empty_file, 'z.c')

  # revision 4
  test_svnmucc(sbox.repo_url,
               ['A /foo/z.c (from /z.c:3)',
                'A /foo/bar (from /foo:3)',
                ], # ---------
               '-m', 'log msg',
               'cp', '3', 'z.c', 'foo/z.c',
               'cp', '3', 'foo', 'foo/bar')

  # revision 5
  test_svnmucc(sbox.repo_url,
               ['A /zig (from /foo:4)',
                'D /zig/bar',
                'D /foo',
                'A /zig/zag (from /foo:4)',
                ], # ---------
               '-m', 'log msg',
               'cp', '4', 'foo', 'zig',
               'rm',             'zig/bar',
               'mv',      'foo', 'zig/zag')

  # revision 6
  test_svnmucc(sbox.repo_url,
               ['D /z.c',
                'A /zig/zag/bar/y.c (from /z.c:5)',
                'A /zig/zag/bar/x.c (from /z.c:3)',
                ], # ---------
               '-m', 'log msg',
               'mv',      'z.c', 'zig/zag/bar/y.c',
               'cp', '3', 'z.c', 'zig/zag/bar/x.c')

  # revision 7
  test_svnmucc(sbox.repo_url,
               ['D /zig/zag/bar/y.c',
                'A /zig/zag/bar/y y.c (from /zig/zag/bar/y.c:6)',
                'A /zig/zag/bar/y%20y.c (from /zig/zag/bar/y.c:6)',
                ], # ---------
               '-m', 'log msg',
               'mv',         'zig/zag/bar/y.c', 'zig/zag/bar/y%20y.c',
               'cp', 'HEAD', 'zig/zag/bar/y.c', 'zig/zag/bar/y%2520y.c')

  # revision 8
  test_svnmucc(sbox.repo_url,
               ['D /zig/zag/bar/y y.c',
                'A /zig/zag/bar/z z1.c (from /zig/zag/bar/y y.c:7)',
                'A /zig/zag/bar/z%20z.c (from /zig/zag/bar/y%20y.c:7)',
                'A /zig/zag/bar/z z2.c (from /zig/zag/bar/y y.c:7)',
                ], #---------
               '-m', 'log msg',
               'mv',         'zig/zag/bar/y%20y.c',   'zig/zag/bar/z z1.c',
               'cp', 'HEAD', 'zig/zag/bar/y%2520y.c', 'zig/zag/bar/z%2520z.c',
               'cp', 'HEAD', 'zig/zag/bar/y y.c',     'zig/zag/bar/z z2.c')


  # revision 9
  test_svnmucc(sbox.repo_url,
               ['D /zig/zag',
                'A /zig/foo (from /zig/zag:8)',
                'D /zig/foo/bar/z%20z.c',
                'D /zig/foo/bar/z z2.c',
                'R /zig/foo/bar/z z1.c (from /zig/zag/bar/x.c:6)',
                ], #---------
               '-m', 'log msg',
               'mv',      'zig/zag',         'zig/foo',
               'rm',                         'zig/foo/bar/z z1.c',
               'rm',                         'zig/foo/bar/z%20z2.c',
               'rm',                         'zig/foo/bar/z%2520z.c',
               'cp', '6', 'zig/zag/bar/x.c', 'zig/foo/bar/z%20z1.c')

  # revision 10
  test_svnmucc(sbox.repo_url,
               ['R /zig/foo/bar (from /zig/z.c:9)',
                ], #---------
               '-m', 'log msg',
               'rm',                 'zig/foo/bar',
               'cp', '9', 'zig/z.c', 'zig/foo/bar')

  # revision 11
  test_svnmucc(sbox.repo_url,
               ['R /zig/foo/bar (from /zig/foo/bar:9)',
                'D /zig/foo/bar/z z1.c',
                ], #---------
               '-m', 'log msg',
               'rm',                     'zig/foo/bar',
               'cp', '9', 'zig/foo/bar', 'zig/foo/bar',
               'rm',                     'zig/foo/bar/z%20z1.c')

  # revision 12
  test_svnmucc(sbox.repo_url,
               ['R /zig/foo (from /zig/foo/bar:11)',
                ], #---------
               '-m', 'log msg',
               'rm',                        'zig/foo',
               'cp', 'head', 'zig/foo/bar', 'zig/foo')

  # revision 13
  test_svnmucc(sbox.repo_url,
               ['D /zig',
                'A /foo (from /foo:4)',
                'A /foo/foo (from /foo:4)',
                'A /foo/foo/foo (from /foo:4)',
                'D /foo/foo/bar',
                'R /foo/foo/foo/bar (from /foo:4)',
                ], #---------
               '-m', 'log msg',
               'rm',             'zig',
               'cp', '4', 'foo', 'foo',
               'cp', '4', 'foo', 'foo/foo',
               'cp', '4', 'foo', 'foo/foo/foo',
               'rm',             'foo/foo/bar',
               'rm',             'foo/foo/foo/bar',
               'cp', '4', 'foo', 'foo/foo/foo/bar')

  # revision 14
  test_svnmucc(sbox.repo_url,
               ['A /boozle (from /foo:4)',
                'A /boozle/buz',
                'A /boozle/buz/nuz',
                ], #---------
               '-m', 'log msg',
               'cp',    '4', 'foo', 'boozle',
               'mkdir',             'boozle/buz',
               'mkdir',             'boozle/buz/nuz')

  # revision 15
  test_svnmucc(sbox.repo_url,
               ['A /boozle/buz/svnmucc-test.py',
                'A /boozle/guz (from /boozle/buz:14)',
                'A /boozle/guz/svnmucc-test.py',
                ], #---------
               '-m', 'log msg',
               'put',      empty_file,   'boozle/buz/svnmucc-test.py',
               'cp', '14', 'boozle/buz', 'boozle/guz',
               'put',      empty_file,   'boozle/guz/svnmucc-test.py')

  # revision 16
  test_svnmucc(sbox.repo_url,
               ['M /boozle/buz/svnmucc-test.py',
                'R /boozle/guz/svnmucc-test.py',
                ], #---------
               '-m', 'log msg',
               'put', empty_file, 'boozle/buz/svnmucc-test.py',
               'rm',              'boozle/guz/svnmucc-test.py',
               'put', empty_file, 'boozle/guz/svnmucc-test.py')

  # revision 17
  test_svnmucc(sbox.repo_url,
               ['R /foo/bar (from /foo/foo:16)'
                ], #---------
               '-m', 'log msg',
               'rm',                            'foo/bar',
               'cp', '16', 'foo/foo',           'foo/bar',
               'propset',  'testprop',  'true', 'foo/bar')

  # revision 18
  test_svnmucc(sbox.repo_url,
               ['M /foo/bar'
                ], #---------
               '-m', 'log msg',
               'propdel', 'testprop', 'foo/bar')

  # revision 19
  test_svnmucc(sbox.repo_url,
               ['M /foo/z.c',
                'M /foo/foo',
                ], #---------
               '-m', 'log msg',
               'propset', 'testprop', 'true', 'foo/z.c',
               'propset', 'testprop', 'true', 'foo/foo')

  # revision 20
  test_svnmucc(sbox.repo_url,
               ['M /foo/z.c',
                'M /foo/foo',
                ], #---------
               '-m', 'log msg',
               'propsetf', 'testprop', empty_file, 'foo/z.c',
               'propsetf', 'testprop', empty_file, 'foo/foo')

  # revision 21
  test_svnmucc(sbox.repo_url,
               ['M /foo/z.c',
                ], #---------
               '-m', 'log msg',
               'propset', 'testprop', 'false', 'foo/z.c',
               'put', file, 'foo/z.c')

  # Expected missing revision error
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200004: 'a' is not a revision"
                 ], #---------
                '-m', 'log msg',
                'cp', 'a', 'b')

  # Expected cannot be younger error
  xtest_svnmucc(sbox.repo_url,
                ['svnmucc: E160006: No such revision 42',
                 ], #---------
                '-m', 'log msg',
                'cp', '42', 'a', 'b')

  # Expected already exists error
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E160020: Path 'foo' already exists",
                 ], #---------
                '-m', 'log msg',
                'cp', '17', 'a', 'foo')

  # Expected copy_src already exists error
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E160020: Path 'a/bar' already exists",
                 ], #---------
                '-m', 'log msg',
                'cp', '17', 'foo', 'a',
                'cp', '17', 'foo/foo', 'a/bar')

  # Expected not found error
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E160013: Path 'a' not found in revision 17",
                 ], #---------
                '-m', 'log msg',
                'cp', '17', 'a', 'b')


def propset_root_internal(sbox, target):
  ## propset on ^/
  svntest.actions.run_and_verify_svnmucc(None, [],
                                         '-m', 'log msg',
                                         'propset', 'foo', 'bar',
                                         target)
  svntest.actions.run_and_verify_svn('bar', [],
                                     'propget', '--no-newline', 'foo',
                                     target)

  ## propdel on ^/
  svntest.actions.run_and_verify_svnmucc(None, [],
                                         '-m', 'log msg',
                                         'propdel', 'foo',
                                         target)
  svntest.actions.run_and_verify_svn([],
                                     '.*W200017: Property.*not found',
                                     'propget', '--no-newline', 'foo',
                                     target)

@Issues(3663)
def propset_root(sbox):
  "propset/propdel on repos root"

  sbox.build(create_wc=False)
  propset_root_internal(sbox, sbox.repo_url)
  propset_root_internal(sbox, sbox.repo_url + '/iota')


def too_many_log_messages(sbox):
  "test log message mutual exclusivity checks"

  sbox.build() # would use read-only=True, but need a place to stuff msg_file
  msg_file = sbox.ospath('svnmucc_msg')
  svntest.main.file_append(msg_file, 'some log message')
  err_msg = ["svnmucc: E205000: --message (-m), --file (-F), and "
             "--with-revprop=svn:log are mutually exclusive"]

  xtest_svnmucc(sbox.repo_url, err_msg,
                '--non-interactive',
                '-m', 'log msg',
                '-F', msg_file,
                'mkdir', 'A/subdir')
  xtest_svnmucc(sbox.repo_url, err_msg,
                '--non-interactive',
                '-m', 'log msg',
                '--with-revprop', 'svn:log=proppy log message',
                'mkdir', 'A/subdir')
  xtest_svnmucc(sbox.repo_url, err_msg,
                '--non-interactive',
                '-F', msg_file,
                '--with-revprop', 'svn:log=proppy log message',
                'mkdir', 'A/subdir')
  xtest_svnmucc(sbox.repo_url, err_msg,
                '--non-interactive',
                '-m', 'log msg',
                '-F', msg_file,
                '--with-revprop', 'svn:log=proppy log message',
                'mkdir', 'A/subdir')

@Issues(3418)
def no_log_msg_non_interactive(sbox):
  "test non-interactive without a log message"

  sbox.build(create_wc=False)
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E205001: Cannot invoke editor to get log message "
                 "when non-interactive"
                 ], #---------
                '--non-interactive',
                'mkdir', 'A/subdir')


def nested_replaces(sbox):
  "nested replaces"

  sbox.build(create_wc=False)
  repo_url = sbox.repo_url
  svntest.actions.run_and_verify_svnmucc(None, [],
                           '-U', repo_url, '-m', 'r2: create tree',
                           'rm', 'A',
                           'rm', 'iota',
                           'mkdir', 'A', 'mkdir', 'A/B', 'mkdir', 'A/B/C',
                           'mkdir', 'M', 'mkdir', 'M/N', 'mkdir', 'M/N/O',
                           'mkdir', 'X', 'mkdir', 'X/Y', 'mkdir', 'X/Y/Z')
  svntest.actions.run_and_verify_svnmucc(None, [],
                           '-U', repo_url, '-m', 'r3: nested replaces',
                           *("""
rm A rm M rm X
cp HEAD X/Y/Z A cp HEAD A/B/C M cp HEAD M/N/O X
cp HEAD A/B A/B cp HEAD M/N M/N cp HEAD X/Y X/Y
rm A/B/C rm M/N/O rm X/Y/Z
cp HEAD X A/B/C cp HEAD A M/N/O cp HEAD M X/Y/Z
rm A/B/C/Y
                           """.split()))

  # ### TODO: need a smarter run_and_verify_log() that verifies copyfrom
  excaped = svntest.main.ensure_list(map(re.escape, [
    '   R /A (from /X/Y/Z:2)',
    '   A /A/B (from /A/B:2)',
    '   R /A/B/C (from /X:2)',
    '   R /M (from /A/B/C:2)',
    '   A /M/N (from /M/N:2)',
    '   R /M/N/O (from /A:2)',
    '   R /X (from /M/N/O:2)',
    '   A /X/Y (from /X/Y:2)',
    '   R /X/Y/Z (from /M:2)',
    '   D /A/B/C/Y',
  ]))
  expected_output = svntest.verify.UnorderedRegexListOutput(excaped
                    + ['^--*', '^r3.*', '^--*', '^Changed paths:',])
  svntest.actions.run_and_verify_svn(expected_output, [],
                                     'log', '-qvr3', repo_url)


def prohibited_deletes_and_moves(sbox):
  "test prohibited delete and move operations"

  # These action sequences were allowed in 1.8.13, but are prohibited in 1.9.x
  # and later.  Most of them probably indicate an inadvertent user mistake.
  # See dev@, 2015-05-11, "Re: Issue 4579 / svnmucc fails to process certain
  # deletes", <http://svn.haxx.se/dev/archive-2015-05/0038.shtml>

  sbox.build(read_only = True)
  svntest.main.file_write(sbox.ospath('file'), "New contents")

  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200009: Can't delete node at 'iota'",
                 ], #---------
                '-m', 'r2: modify and delete /iota',
                'put', sbox.ospath('file'), 'iota',
                'rm', 'iota')

  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200009: Can't delete node at 'iota'",
                 ], #---------
                '-m', 'r2: propset and delete /iota',
                'propset', 'prop', 'val', 'iota',
                'rm', 'iota')

  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E160013: Can't delete node at 'iota' as it does "
                 "not exist",
                 ], #---------
                '-m', 'r2: delete and delete /iota',
                'rm', 'iota',
                'rm', 'iota')

  # Subversion 1.8.13 used to move /iota without applying the text change.
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200009: Can't delete node at 'iota'",
                 ], #---------
                '-m', 'r2: modify and move /iota',
                'put', sbox.ospath('file'), 'iota',
                'mv', 'iota', 'iota2')

  # Subversion 1.8.13 used to move /A without applying the inner remove.
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200009: Can't delete node at 'A'",
                 ], #---------
                '-m', 'r2: delete /A/B and move /A',
                'rm', 'A/B',
                'mv', 'A', 'A1')

def svnmucc_type_errors(sbox):
  "test type errors"

  sbox.build(read_only=True)

  sbox.simple_append('file', 'New contents')

  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E160016: Can't operate on 'B' "
                "because 'A' is not a directory"],
                '-m', '',
                'put', sbox.ospath('file'), 'A',
                'mkdir', 'A/B',
                'propset', 'iota', 'iota', 'iota')

  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200009: Can't delete node at 'A'"],
                '-m', '',
                'mkdir', 'A/Z',
                'put', sbox.ospath('file'), 'A')

  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E160020: Path 'Z' already exists, or was created "
                 "by an earlier operation"],
                '-m', '',
                'mkdir', 'A/Z',
                'put', sbox.ospath('file'), 'A/Z')

def svnmucc_propset_and_put(sbox):
  "propset and put"

  sbox.build()

  sbox.simple_append('file', 'New contents')

  # First in the sane order: put, then propset
  xtest_svnmucc(sbox.repo_url,
                [],
                '-m', '',
                'put', sbox.ospath('file'), 't1',
                'propset', 't1', 't1', 't1')

  # And now in an impossible order: propset, then put
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200009: Can't set properties at not existing 't2'"],
                '-m', '',
                'propset', 't2', 't2', 't2',
                'put', sbox.ospath('file'), 't2')

  # And if the target already exists (dir)
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200009: Can't delete node at 'A'"],
                '-m', '',
                'propset', 'A', 'A', 'A',
                'put', sbox.ospath('file'), 'A')

  # And if the target already exists (file) # fixed in r1702467
  xtest_svnmucc(sbox.repo_url,
                [],
                '-m', '',
                'propset', 'iota', 'iota', 'iota',
                'put', sbox.ospath('file'), 'iota')

  # Put same file twice (non existing)
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E160020: Path 't3' already exists, or was created "
                 "by an earlier operation"],
                '-m', '',
                'put', sbox.ospath('file'), 't3',
                'put', sbox.ospath('file'), 't3')

  # Put same file twice (existing)
  xtest_svnmucc(sbox.repo_url,
                ["svnmucc: E200009: Can't update file at 't1'"],
                '-m', '',
                'put', sbox.ospath('file'), 't1',
                'put', sbox.ospath('file'), 't1')


######################################################################

test_list = [ None,
              reject_bogus_mergeinfo,
              basic_svnmucc,
              propset_root,
              too_many_log_messages,
              no_log_msg_non_interactive,
              nested_replaces,
              prohibited_deletes_and_moves,
              svnmucc_type_errors,
              svnmucc_propset_and_put,
            ]

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