diff options
Diffstat (limited to 'subversion/tests/cmdline/svntest/mergetrees.py')
-rwxr-xr-x | subversion/tests/cmdline/svntest/mergetrees.py | 506 |
1 files changed, 506 insertions, 0 deletions
diff --git a/subversion/tests/cmdline/svntest/mergetrees.py b/subversion/tests/cmdline/svntest/mergetrees.py new file mode 100755 index 0000000..e01592a --- /dev/null +++ b/subversion/tests/cmdline/svntest/mergetrees.py @@ -0,0 +1,506 @@ +#!/usr/bin/env python +# +# mergetrees.py: routines that create merge scenarios +# +# 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 shutil, sys, re, os +import time + +# Our testing module +import main, wc, verify, actions, testcase + +from prop_tests import binary_mime_type_on_text_file_warning + +# (abbreviation) +Item = wc.StateItem +Skip = testcase.Skip_deco +SkipUnless = testcase.SkipUnless_deco +XFail = testcase.XFail_deco +Issues = testcase.Issues_deco +Issue = testcase.Issue_deco +Wimp = testcase.Wimp_deco +exp_noop_up_out = actions.expected_noop_update_output + +from svntest.main import SVN_PROP_MERGEINFO + +def expected_merge_output(rev_ranges, additional_lines=[], foreign=False, + elides=False, two_url=False, target=None, + text_conflicts=0, prop_conflicts=0, tree_conflicts=0, + text_resolved=0, prop_resolved=0, tree_resolved=0, + skipped_paths=0): + """Generate an (inefficient) regex representing the expected merge + output and mergeinfo notifications from REV_RANGES and ADDITIONAL_LINES. + + REV_RANGES is a list of revision ranges for which mergeinfo is being + recorded. Each range is of the form [start, end] (where both START and + END are inclusive, unlike in '-rX:Y') or the form [single_rev] (which is + like '-c SINGLE_REV'). If REV_RANGES is None then only the standard + notification for a 3-way merge is expected. + + ADDITIONAL_LINES is a list of strings to match the other lines of output; + these are basically regular expressions except that backslashes will be + escaped herein. If ADDITIONAL_LINES is a single string, it is interpreted + the same as a list containing that string. + + If ELIDES is true, add to the regex an expression representing elision + notification. If TWO_URL is true, tweak the regex to expect the + appropriate mergeinfo notification for a 3-way merge. + + TARGET is the local path to the target, as it should appear in + notifications; if None, it is not checked. + + TEXT_CONFLICTS, PROP_CONFLICTS, TREE_CONFLICTS and SKIPPED_PATHS specify + the number of each kind of conflict to expect. + """ + + if rev_ranges is None: + lines = [main.merge_notify_line(None, None, False, foreign)] + else: + lines = [] + for rng in rev_ranges: + start_rev = rng[0] + if len(rng) > 1: + end_rev = rng[1] + else: + end_rev = None + lines += [main.merge_notify_line(start_rev, end_rev, + True, foreign, target)] + lines += [main.mergeinfo_notify_line(start_rev, end_rev, target)] + + if (elides): + lines += ["--- Eliding mergeinfo from .*\n"] + + if (two_url): + lines += ["--- Recording mergeinfo for merge between repository URLs .*\n"] + + # Address "The Backslash Plague" + # + # If ADDITIONAL_LINES are present there are possibly paths in it with + # multiple components and on Windows these components are separated with + # '\'. These need to be escaped properly in the regexp for the match to + # work correctly. See http://aspn.activestate.com/ASPN/docs/ActivePython + # /2.2/howto/regex/regex.html#SECTION000420000000000000000. + if isinstance(additional_lines, str): + additional_lines = [additional_lines] + if sys.platform == 'win32': + additional_lines = [line.replace("\\", "\\\\") for line in additional_lines] + lines += additional_lines + + lines += main.summary_of_conflicts( + text_conflicts, prop_conflicts, tree_conflicts, + text_resolved, prop_resolved, tree_resolved, + skipped_paths, + as_regex=True) + + return "|".join(lines) + +def check_mergeinfo_recursively(root_path, subpaths_mergeinfo): + """Check that the mergeinfo properties on and under ROOT_PATH are those in + SUBPATHS_MERGEINFO, a {path: mergeinfo-prop-val} dictionary.""" + expected = verify.UnorderedOutput( + [path + ' - ' + subpaths_mergeinfo[path] + '\n' + for path in subpaths_mergeinfo]) + actions.run_and_verify_svn(expected, [], + 'propget', '-R', SVN_PROP_MERGEINFO, + root_path) + +###################################################################### +#---------------------------------------------------------------------- +def set_up_dir_replace(sbox): + """Set up the working copy for directory replace tests, creating + directory 'A/B/F/foo' with files 'new file' and 'new file2' within + it (r2), and merging 'foo' onto 'C' (r3), then deleting 'A/B/F/foo' + (r4).""" + + sbox.build() + wc_dir = sbox.wc_dir + + C_path = sbox.ospath('A/C') + F_path = sbox.ospath('A/B/F') + F_url = sbox.repo_url + '/A/B/F' + + foo_path = os.path.join(F_path, 'foo') + new_file = os.path.join(foo_path, "new file") + new_file2 = os.path.join(foo_path, "new file 2") + + # Make directory foo in F, and add some files within it. + actions.run_and_verify_svn(None, [], 'mkdir', foo_path) + main.file_append(new_file, "Initial text in new file.\n") + main.file_append(new_file2, "Initial text in new file 2.\n") + main.run_svn(None, "add", new_file) + main.run_svn(None, "add", new_file2) + + # Commit all the new content, creating r2. + expected_output = wc.State(wc_dir, { + 'A/B/F/foo' : Item(verb='Adding'), + 'A/B/F/foo/new file' : Item(verb='Adding'), + 'A/B/F/foo/new file 2' : Item(verb='Adding'), + }) + expected_status = actions.get_virginal_state(wc_dir, 1) + expected_status.add({ + 'A/B/F/foo' : Item(status=' ', wc_rev=2), + 'A/B/F/foo/new file' : Item(status=' ', wc_rev=2), + 'A/B/F/foo/new file 2' : Item(status=' ', wc_rev=2), + }) + actions.run_and_verify_commit(wc_dir, expected_output, expected_status) + + # Merge foo onto C + expected_output = wc.State(C_path, { + 'foo' : Item(status='A '), + 'foo/new file' : Item(status='A '), + 'foo/new file 2' : Item(status='A '), + }) + expected_mergeinfo_output = wc.State(C_path, { + '' : Item(status=' U'), + }) + expected_elision_output = wc.State(C_path, { + }) + expected_disk = wc.State('', { + '' : Item(props={SVN_PROP_MERGEINFO : '/A/B/F:2'}), + 'foo' : Item(), + 'foo/new file' : Item("Initial text in new file.\n"), + 'foo/new file 2' : Item("Initial text in new file 2.\n"), + }) + expected_status = wc.State(C_path, { + '' : Item(status=' M', wc_rev=1), + 'foo' : Item(status='A ', wc_rev='-', copied='+'), + 'foo/new file' : Item(status=' ', wc_rev='-', copied='+'), + 'foo/new file 2' : Item(status=' ', wc_rev='-', copied='+'), + }) + expected_skip = wc.State(C_path, { }) + actions.run_and_verify_merge(C_path, '1', '2', F_url, None, + expected_output, + expected_mergeinfo_output, + expected_elision_output, + expected_disk, + expected_status, + expected_skip, + check_props=True) + # Commit merge of foo onto C, creating r3. + expected_output = wc.State(wc_dir, { + 'A/C' : Item(verb='Sending'), + 'A/C/foo' : Item(verb='Adding'), + }) + expected_status = actions.get_virginal_state(wc_dir, 1) + expected_status.add({ + 'A/B/F/foo' : Item(status=' ', wc_rev=2), + 'A/C' : Item(status=' ', wc_rev=3), + 'A/B/F/foo/new file' : Item(status=' ', wc_rev=2), + 'A/B/F/foo/new file 2' : Item(status=' ', wc_rev=2), + 'A/C/foo' : Item(status=' ', wc_rev=3), + 'A/C/foo/new file' : Item(status=' ', wc_rev=3), + 'A/C/foo/new file 2' : Item(status=' ', wc_rev=3), + + }) + actions.run_and_verify_commit(wc_dir, expected_output, expected_status) + + # Delete foo on F, creating r4. + actions.run_and_verify_svn(None, [], 'rm', foo_path) + expected_output = wc.State(wc_dir, { + 'A/B/F/foo' : Item(verb='Deleting'), + }) + expected_status = actions.get_virginal_state(wc_dir, 1) + expected_status.add({ + 'A/C' : Item(status=' ', wc_rev=3), + 'A/C/foo' : Item(status=' ', wc_rev=3), + 'A/C/foo/new file' : Item(status=' ', wc_rev=3), + 'A/C/foo/new file 2' : Item(status=' ', wc_rev=3), + }) + actions.run_and_verify_commit(wc_dir, expected_output, expected_status) + +#---------------------------------------------------------------------- +def set_up_branch(sbox, branch_only = False, nbr_of_branches = 1): + '''Starting with standard greek tree, copy 'A' NBR_OF_BRANCHES times + to A_COPY, A_COPY_2, A_COPY_3, and so on. Then, unless BRANCH_ONLY is + true, make four modifications (setting file contents to "New content") + under A: + r(2 + NBR_OF_BRANCHES) - A/D/H/psi + r(3 + NBR_OF_BRANCHES) - A/D/G/rho + r(4 + NBR_OF_BRANCHES) - A/B/E/beta + r(5 + NBR_OF_BRANCHES) - A/D/H/omega + Return (expected_disk, expected_status).''' + + # With the default parameters, the branching looks like this: + # + # A -1-----3-4-5-6-- + # \ + # A_COPY 2----------- + + wc_dir = sbox.wc_dir + + expected_status = actions.get_virginal_state(wc_dir, 1) + expected_disk = main.greek_state.copy() + + def copy_A(dest_name, rev): + expected = verify.UnorderedOutput( + ["A " + os.path.join(wc_dir, dest_name, "B") + "\n", + "A " + os.path.join(wc_dir, dest_name, "B", "lambda") + "\n", + "A " + os.path.join(wc_dir, dest_name, "B", "E") + "\n", + "A " + os.path.join(wc_dir, dest_name, "B", "E", "alpha") + "\n", + "A " + os.path.join(wc_dir, dest_name, "B", "E", "beta") + "\n", + "A " + os.path.join(wc_dir, dest_name, "B", "F") + "\n", + "A " + os.path.join(wc_dir, dest_name, "mu") + "\n", + "A " + os.path.join(wc_dir, dest_name, "C") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "gamma") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "G") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "G", "pi") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "G", "rho") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "G", "tau") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "H") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "H", "chi") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "H", "omega") + "\n", + "A " + os.path.join(wc_dir, dest_name, "D", "H", "psi") + "\n", + "Checked out revision " + str(rev - 1) + ".\n", + "A " + os.path.join(wc_dir, dest_name) + "\n"]) + expected_status.add({ + dest_name + "/B" : Item(status=' ', wc_rev=rev), + dest_name + "/B/lambda" : Item(status=' ', wc_rev=rev), + dest_name + "/B/E" : Item(status=' ', wc_rev=rev), + dest_name + "/B/E/alpha" : Item(status=' ', wc_rev=rev), + dest_name + "/B/E/beta" : Item(status=' ', wc_rev=rev), + dest_name + "/B/F" : Item(status=' ', wc_rev=rev), + dest_name + "/mu" : Item(status=' ', wc_rev=rev), + dest_name + "/C" : Item(status=' ', wc_rev=rev), + dest_name + "/D" : Item(status=' ', wc_rev=rev), + dest_name + "/D/gamma" : Item(status=' ', wc_rev=rev), + dest_name + "/D/G" : Item(status=' ', wc_rev=rev), + dest_name + "/D/G/pi" : Item(status=' ', wc_rev=rev), + dest_name + "/D/G/rho" : Item(status=' ', wc_rev=rev), + dest_name + "/D/G/tau" : Item(status=' ', wc_rev=rev), + dest_name + "/D/H" : Item(status=' ', wc_rev=rev), + dest_name + "/D/H/chi" : Item(status=' ', wc_rev=rev), + dest_name + "/D/H/omega" : Item(status=' ', wc_rev=rev), + dest_name + "/D/H/psi" : Item(status=' ', wc_rev=rev), + dest_name : Item(status=' ', wc_rev=rev)}) + expected_disk.add({ + dest_name : Item(), + dest_name + '/B' : Item(), + dest_name + '/B/lambda' : Item("This is the file 'lambda'.\n"), + dest_name + '/B/E' : Item(), + dest_name + '/B/E/alpha' : Item("This is the file 'alpha'.\n"), + dest_name + '/B/E/beta' : Item("This is the file 'beta'.\n"), + dest_name + '/B/F' : Item(), + dest_name + '/mu' : Item("This is the file 'mu'.\n"), + dest_name + '/C' : Item(), + dest_name + '/D' : Item(), + dest_name + '/D/gamma' : Item("This is the file 'gamma'.\n"), + dest_name + '/D/G' : Item(), + dest_name + '/D/G/pi' : Item("This is the file 'pi'.\n"), + dest_name + '/D/G/rho' : Item("This is the file 'rho'.\n"), + dest_name + '/D/G/tau' : Item("This is the file 'tau'.\n"), + dest_name + '/D/H' : Item(), + dest_name + '/D/H/chi' : Item("This is the file 'chi'.\n"), + dest_name + '/D/H/omega' : Item("This is the file 'omega'.\n"), + dest_name + '/D/H/psi' : Item("This is the file 'psi'.\n"), + }) + + # Make a branch A_COPY to merge into. + actions.run_and_verify_svn(expected, [], 'copy', + sbox.repo_url + "/A", + os.path.join(wc_dir, + dest_name)) + + expected_output = wc.State(wc_dir, {dest_name : Item(verb='Adding')}) + actions.run_and_verify_commit(wc_dir, expected_output, expected_status) + for i in range(nbr_of_branches): + if i == 0: + copy_A('A_COPY', i + 2) + else: + copy_A('A_COPY_' + str(i + 1), i + 2) + + if branch_only: + return expected_disk, expected_status + + # Make some changes under A which we'll later merge under A_COPY: + + # r(nbr_of_branches + 2) - modify and commit A/D/H/psi + main.file_write(sbox.ospath('A/D/H/psi'), + "New content") + expected_output = wc.State(wc_dir, {'A/D/H/psi' : Item(verb='Sending')}) + expected_status.tweak('A/D/H/psi', wc_rev=nbr_of_branches + 2) + actions.run_and_verify_commit(wc_dir, expected_output, expected_status) + expected_disk.tweak('A/D/H/psi', contents="New content") + + # r(nbr_of_branches + 3) - modify and commit A/D/G/rho + main.file_write(sbox.ospath('A/D/G/rho'), + "New content") + expected_output = wc.State(wc_dir, {'A/D/G/rho' : Item(verb='Sending')}) + expected_status.tweak('A/D/G/rho', wc_rev=nbr_of_branches + 3) + actions.run_and_verify_commit(wc_dir, expected_output, expected_status) + expected_disk.tweak('A/D/G/rho', contents="New content") + + # r(nbr_of_branches + 4) - modify and commit A/B/E/beta + main.file_write(sbox.ospath('A/B/E/beta'), + "New content") + expected_output = wc.State(wc_dir, {'A/B/E/beta' : Item(verb='Sending')}) + expected_status.tweak('A/B/E/beta', wc_rev=nbr_of_branches + 4) + actions.run_and_verify_commit(wc_dir, expected_output, expected_status) + expected_disk.tweak('A/B/E/beta', contents="New content") + + # r(nbr_of_branches + 5) - modify and commit A/D/H/omega + main.file_write(sbox.ospath('A/D/H/omega'), + "New content") + expected_output = wc.State(wc_dir, {'A/D/H/omega' : Item(verb='Sending')}) + expected_status.tweak('A/D/H/omega', wc_rev=nbr_of_branches + 5) + actions.run_and_verify_commit(wc_dir, expected_output, expected_status) + expected_disk.tweak('A/D/H/omega', contents="New content") + + return expected_disk, expected_status + +#---------------------------------------------------------------------- +# Helper functions. These take local paths using '/' separators. + +def local_path(path): + "Convert a path from '/' separators to the local style." + return os.sep.join(path.split('/')) + +def svn_mkfile(path): + "Make and add a file with some default content, and keyword expansion." + path = local_path(path) + dirname, filename = os.path.split(path) + main.file_write(path, "This is the file '" + filename + "'.\n" + + "Last changed in '$Revision$'.\n") + actions.run_and_verify_svn(None, [], 'add', path) + actions.run_and_verify_svn(None, [], 'propset', + 'svn:keywords', 'Revision', path) + +def svn_modfile(path): + "Make text and property mods to a WC file." + path = local_path(path) + main.file_append(path, "An extra line.\n") + actions.run_and_verify_svn(None, [], 'propset', + 'newprop', 'v', path) + +def svn_copy(s_rev, path1, path2): + "Copy a WC path locally." + path1 = local_path(path1) + path2 = local_path(path2) + actions.run_and_verify_svn(None, [], 'copy', '--parents', + '-r', s_rev, path1, path2) + +def svn_merge(rev_range, source, target, lines=None, elides=[], + text_conflicts=0, prop_conflicts=0, tree_conflicts=0, + text_resolved=0, prop_resolved=0, tree_resolved=0, + args=[]): + """Merge a single change from path SOURCE to path TARGET and verify the + output and that there is no error. (The changes made are not verified.) + + REV_RANGE is either a number (to cherry-pick that specific change) or a + two-element list [X,Y] to pick the revision range '-r(X-1):Y'. + + LINES is a list of regular expressions to match other lines of output; if + LINES is 'None' then match all normal (non-conflicting) merges. + + ELIDES is a list of paths on which mergeinfo elision should be reported. + + TEXT_CONFLICTS, PROP_CONFLICTS and TREE_CONFLICTS specify the number of + each kind of conflict to expect. + + ARGS are additional arguments passed to svn merge. + """ + + source = local_path(source) + target = local_path(target) + elides = [local_path(p) for p in elides] + if isinstance(rev_range, int): + mi_rev_range = [rev_range] + rev_arg = '-c' + str(rev_range) + else: + mi_rev_range = rev_range + rev_arg = '-r' + str(rev_range[0] - 1) + ':' + str(rev_range[1]) + if lines is None: + lines = ["(A |D |[UG] | [UG]|[UG][UG]) " + target + ".*\n"] + else: + # Expect mergeinfo on the target; caller must supply matches for any + # subtree mergeinfo paths. + lines.append(" [UG] " + target + "\n") + exp_out = expected_merge_output([mi_rev_range], lines, target=target, + elides=elides, + text_conflicts=text_conflicts, + prop_conflicts=prop_conflicts, + tree_conflicts=tree_conflicts, + text_resolved=text_resolved, + prop_resolved=prop_resolved, + tree_resolved=tree_resolved) + actions.run_and_verify_svn(exp_out, [], + 'merge', rev_arg, source, target, *args) + +#---------------------------------------------------------------------- +# Setup helper for issue #4056 and issue #4057 tests. +def noninheritable_mergeinfo_test_set_up(sbox): + '''Starting with standard greek tree, copy 'A' to 'branch' in r2 and + then made a file edit to A/B/lambda in r3. + Return (expected_output, expected_mergeinfo_output, expected_elision_output, + expected_status, expected_disk, expected_skip) for a merge of + r3 from ^/A/B to branch/B.''' + + sbox.build() + wc_dir = sbox.wc_dir + + lambda_path = sbox.ospath('A/B/lambda') + B_branch_path = sbox.ospath('branch/B') + + # r2 - Branch ^/A to ^/branch. + main.run_svn(None, 'copy', sbox.repo_url + '/A', + sbox.repo_url + '/branch', '-m', 'make a branch') + + # r3 - Make an edit to A/B/lambda. + main.file_write(lambda_path, "trunk edit.\n") + main.run_svn(None, 'commit', '-m', 'file edit', wc_dir) + main.run_svn(None, 'up', wc_dir) + + expected_output = wc.State(B_branch_path, { + 'lambda' : Item(status='U '), + }) + expected_mergeinfo_output = wc.State(B_branch_path, { + '' : Item(status=' U'), + 'lambda' : Item(status=' U'), + }) + expected_elision_output = wc.State(B_branch_path, { + 'lambda' : Item(status=' U'), + }) + expected_status = wc.State(B_branch_path, { + '' : Item(status=' M'), + 'lambda' : Item(status='M '), + 'E' : Item(status=' '), + 'E/alpha' : Item(status=' '), + 'E/beta' : Item(status=' '), + 'F' : Item(status=' '), + }) + expected_status.tweak(wc_rev='3') + expected_disk = wc.State('', { + '' : Item(props={SVN_PROP_MERGEINFO : '/A/B:3'}), + 'lambda' : Item("trunk edit.\n"), + 'E' : Item(), + 'E/alpha' : Item("This is the file 'alpha'.\n"), + 'E/beta' : Item("This is the file 'beta'.\n"), + 'F' : Item(), + }) + expected_skip = wc.State(B_branch_path, {}) + + return expected_output, expected_mergeinfo_output, expected_elision_output, \ + expected_status, expected_disk, expected_skip + |