| #!/usr/bin/env python |
| # |
| # tree_conflict_tests.py: testing tree-conflict cases. |
| # |
| # 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, re, os, stat, traceback |
| |
| # Our testing module |
| import svntest |
| from svntest import main, wc, verify |
| from svntest.actions import run_and_verify_svn |
| from svntest.actions import run_and_verify_commit |
| from svntest.actions import run_and_verify_resolved |
| from svntest.actions import run_and_verify_update |
| from svntest.actions import run_and_verify_status |
| from svntest.actions import run_and_verify_info |
| from svntest.actions import get_virginal_state |
| import shutil |
| import logging |
| |
| # (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 |
| AnyOutput = svntest.verify.AnyOutput |
| RegexOutput = svntest.verify.RegexOutput |
| RegexListOutput = svntest.verify.RegexListOutput |
| UnorderedOutput = svntest.verify.UnorderedOutput |
| AlternateOutput = svntest.verify.AlternateOutput |
| |
| logger = logging.getLogger() |
| |
| ###################################################################### |
| # Tests |
| # |
| # Each test must return on success or raise on failure. |
| |
| |
| #---------------------------------------------------------------------- |
| |
| # The tests in this file are for cases where a tree conflict is to be raised. |
| # (They do not check that conflicts are not raised in other cases.) |
| |
| # Note: Delete, Replace and Move are presently tested together but probably |
| # will eventually need to be tested separately. |
| |
| # A tree conflict being raised means: |
| # - the conflict is reported initially |
| # - the conflict is persistently visible |
| # - the conflict blocks commits until resolved |
| # - the conflict blocks (some?) further merges |
| # Desired: |
| # - interactive conflict resolution |
| |
| # A "tree conflict on file P/F" means: |
| # - the operation reports action code "C" on path P/F |
| # - "svn status" reports status code "C" on path P/F |
| # - "svn info" reports details of the conflict on path P/F |
| # - "svn commit" fails if the user-requested targets include path P/F |
| # - "svn merge/update/switch" fails if it tries to modify P/F in any way |
| |
| # A "tree conflict on dir P/D" means: |
| # - the operation reports action code "C" on path P/D |
| # - "svn status" reports status code "C" on path P/D |
| # - "svn info" reports details of the conflict on P/D |
| # - "svn commit" fails if it includes any part of the P/D sub-tree |
| # - "svn merge/up/sw" fails if it modifies any part of the P/D sub-tree |
| |
| #---------------------------------------------------------------------- |
| |
| # Two sets of paths. The paths to be used for the destination of a copy |
| # or move must differ between the incoming change and the local mods, |
| # otherwise scenarios involving a move onto a move would conflict on the |
| # destination node as well as on the source, and we only want to be testing |
| # one thing at a time in most tests. |
| def incoming_paths(root_dir, parent_dir): |
| """Create a set of paths in which the victims of tree conflicts are |
| children of PARENT_DIR. ROOT_DIR should be a shallower directory |
| in which items "F1" and "D1" can pre-exist and be shared across |
| multiple parent dirs.""" |
| return { |
| 'F1' : os.path.join(root_dir, "F1"), |
| 'F' : os.path.join(parent_dir, "F"), |
| 'F2' : os.path.join(parent_dir, "F2-in"), |
| 'F3' : os.path.join(root_dir, "F3"), |
| 'D1' : os.path.join(root_dir, "D1"), |
| 'D' : os.path.join(parent_dir, "D"), |
| 'D2' : os.path.join(parent_dir, "D2-in"), |
| } |
| def localmod_paths(root_dir, parent_dir): |
| """Create a set of paths in which the victims of tree conflicts are |
| children of PARENT_DIR. ROOT_DIR should be a shallower directory |
| in which items "F1" and "D1" can pre-exist and be shared across |
| multiple parent dirs.""" |
| return { |
| 'F1' : os.path.join(root_dir, "F1"), |
| 'F' : os.path.join(parent_dir, "F"), |
| 'F2' : os.path.join(parent_dir, "F2-local"), |
| 'F3' : os.path.join(root_dir, "F3"), |
| 'D1' : os.path.join(root_dir, "D1"), |
| 'D' : os.path.join(parent_dir, "D"), |
| 'D2' : os.path.join(parent_dir, "D2-local"), |
| } |
| |
| # Perform the action MODACTION on the WC items given by PATHS. The |
| # available actions can be seen within this function. |
| def modify(modaction, paths, is_init=True): |
| F1 = paths['F1'] # existing file to copy from |
| F3 = paths['F3'] # existing file to copy from |
| F = paths['F'] # target file |
| F2 = paths['F2'] # non-existing file to copy/move to |
| D1 = paths['D1'] # existing dir to copy from |
| D = paths['D'] # target dir |
| D2 = paths['D2'] # non-existing dir to copy/move to |
| |
| # print " Mod: '" + modaction + "' '" + P + "'" |
| |
| if modaction == 'ft': # file text-mod |
| assert os.path.exists(F) |
| main.file_append(F, "This is a text-mod of file F.\n") |
| elif modaction == 'fP': # file Prop-mod |
| assert os.path.exists(F) |
| main.run_svn(None, 'pset', 'fprop1', 'A prop set on file F.', F) |
| elif modaction == 'dP': # dir Prop-mod |
| assert os.path.exists(D) |
| main.run_svn(None, 'pset', 'dprop1', 'A prop set on dir D.', D) |
| elif modaction == 'fD': # file Delete |
| assert os.path.exists(F) |
| main.run_svn(None, 'del', F) |
| elif modaction == 'dD': # dir Delete |
| assert os.path.exists(D) |
| main.run_svn(None, 'del', D) |
| elif modaction == 'fA': # file Add (new) |
| assert os.path.exists(F) |
| main.run_svn(None, 'add', F) |
| main.run_svn(None, 'pset', 'fprop2', 'A prop of added file F.', F) |
| elif modaction == 'dA': # dir Add (new) |
| assert os.path.exists(D) |
| main.run_svn(None, 'add', D) |
| main.run_svn(None, 'pset', 'dprop2', 'A prop of added dir D.', D) |
| elif modaction == 'fC': # file Copy (from F1) |
| if is_init: |
| main.run_svn(None, 'copy', F1, F) |
| else: |
| main.run_svn(None, 'copy', F3, F) |
| elif modaction == 'dC': # dir Copy (from D1) |
| main.run_svn(None, 'copy', D1, D) |
| elif modaction == 'fM': # file Move (to F2) |
| main.run_svn(None, 'rename', F, F2) |
| elif modaction == 'dM': # dir Move (to D2) |
| main.run_svn(None, 'rename', D, D2) |
| elif modaction == 'fa': # file add (new) on disk |
| assert not os.path.exists(F) |
| main.file_write(F, "This is file F.\n") |
| elif modaction == 'da': # dir add (new) on disk |
| assert not os.path.exists(D) |
| os.mkdir(D) |
| elif modaction == 'fd': # file delete from disk |
| assert os.path.exists(F) |
| os.remove(F) |
| elif modaction == 'dd': # dir delete from disk |
| assert os.path.exists(D) |
| os.remove(D) |
| else: |
| raise Exception("unknown modaction: '" + modaction + "'") |
| |
| #---------------------------------------------------------------------- |
| |
| # Lists of change scenarios |
| # |
| # Each scenario expresses a change in terms of the client commands |
| # (including "move") that create that change. The change may exist in a |
| # repository, or may be applied to a WC by an "update" or "switch" or |
| # "merge", or may exist in a WC as a local modification. |
| # |
| # In addition, each scenario may include some local-modification actions |
| # that, if performed on the WC after this change, will make the disk state |
| # incompatible with the version-controlled state - e.g. by deleting a file |
| # that metadata says is present or vice-versa. |
| |
| # File names: |
| # F1 = any existing file |
| # F3 = any existing file |
| # F = the file-path being acted on |
| # F2 = any non-existent file-path |
| # D1 = any existing dir |
| # D = the dir-path being acted on |
| # D2 = any non-existent dir-path |
| # P = the parent dir of F and of D |
| |
| # Format of a change scenario: |
| # ( |
| # list of actions to create the file/directory to be changed later, |
| # list of actions to make the change |
| # ) |
| |
| # Action lists to initialise the repository with a file or directory absent |
| # or present, to provide the starting point from which we perform the changes |
| # that are to be tested. |
| absent_f = [] |
| absent_d = [] |
| create_f = ['fa','fA'] |
| create_d = ['da','dA'] |
| |
| # Scenarios that start with no existing versioned item |
| # |
| # CREATE: |
| # file-add(F) = add-new(F) or copy(F1,F)(and modify?) |
| # dir-add(D) = add-new(D)(deep?) or copy(D1,D)(and modify?) |
| |
| f_adds = [ |
| #( absent_f, ['fa','fA'] ), ### local add-without-history: not a tree conflict |
| ( absent_f, ['fC'] ), |
| ( absent_f, ['fC','ft'] ), ### Fails because update seems to assume that the |
| ### local file is unmodified (same as issue 1736?). |
| #( absent_f, ['fC','fP'] ), # don't test all combinations, just because it's slow |
| ] |
| d_adds = [ |
| #( absent_d, ['da','dA'] ), ### local add-without-history: not a tree conflict |
| ( absent_d, ['dC'] ), |
| #( absent_d, ['dC','dP'] ), # not yet |
| ] |
| |
| # Scenarios that start with an existing versioned item |
| # |
| # GO-AWAY: node is no longer at the path where it was. |
| # file-del(F) = del(F) |
| # file-move(F) = move(F,F2) |
| # dir-del(D) = del(D) or move(D,D2) |
| # Note: file-move(F) does not conflict with incoming edit |
| # |
| # REPLACE: node is no longer at the path where it was, but another node is. |
| # file-rpl(F) = file-del(F) + file-add(F) |
| # dir-rpl(D) = dir-del(D) + dir-add(D) |
| # Note: Schedule replace-by-different-node-type is unsupported in WC. |
| # |
| # MODIFY: |
| # file-mod(F) = text-mod(F) and/or prop-mod(F) |
| # dir-mod(D) = prop-mod(D) and/or file-mod(child-F) and/or dir-mod(child-D) |
| |
| f_dels = [ |
| ( create_f, ['fD'] ), |
| ] |
| |
| f_moves = [ |
| ( create_f, ['fM'] ), |
| ] |
| |
| d_dels = [ |
| ( create_d, ['dD'] ), |
| ] |
| |
| d_moves = [ |
| ( create_d, ['dM'] ), |
| ] |
| |
| f_rpls = [ |
| # Don't test all possible combinations, just because it's slow |
| ( create_f, ['fD','fa','fA'] ), |
| ( create_f, ['fM','fC'] ), |
| ] |
| d_rpls = [ |
| # We're not testing directory replacements yet. |
| # Don't test all possible combinations, just because it's slow |
| #( create_d, ['dD','dA'] ), |
| #( create_d, ['dM','dC'] ), |
| # Note that directory replacement differs from file replacement: the |
| # schedule-delete dir is still on disk and is re-used for the re-addition. |
| ] |
| f_rpl_d = [ |
| # File replaced by directory: not yet testable |
| ] |
| d_rpl_f = [ |
| # Directory replaced by file: not yet testable |
| ] |
| |
| f_mods = [ |
| ( create_f, ['ft'] ), |
| ( create_f, ['fP'] ), |
| #( create_f, ['ft','fP'] ), # don't test all combinations, just because it's slow |
| ] |
| d_mods = [ |
| ( create_d, ['dP'] ), |
| # These test actions for operating on a child of the directory are not yet implemented: |
| #( create_d, ['f_fA'] ), |
| #( create_d, ['f_ft'] ), |
| #( create_d, ['f_fP'] ), |
| #( create_d, ['f_fD'] ), |
| #( create_d, ['d_dP'] ), |
| #( create_d, ['d_f_fA'] ), |
| ] |
| |
| #---------------------------------------------------------------------- |
| |
| # Set up all of the given SCENARIOS in their respective unique paths. |
| # This means committing their initialisation actions in r2, and then |
| # committing their change actions in r3 (assuming the repos was at r1). |
| # (See also the somewhat related svntest.actions.build_greek_tree_conflicts() |
| # and tree-conflicts tests using deep_trees in various other .py files.) |
| # SCENARIOS is a list of scenario tuples: (init_actions, change_actions). |
| # WC_DIR is a local path of an existing WC. |
| # BR_DIR is a nonexistent path within WC_DIR. |
| # BR_DIR and any necessary parent directories will be created, and then the |
| # scenario will be set up within it, and committed to the repository. |
| def set_up_repos(wc_dir, br_dir, scenarios): |
| |
| if not os.path.exists(br_dir): |
| main.run_svn(None, "mkdir", "--parents", br_dir) |
| |
| # create the file F1 and dir D1 which the tests regard as pre-existing |
| paths = incoming_paths(wc_dir, wc_dir) # second arg is bogus but unimportant |
| F1 = paths['F1'] # existing file to copy from |
| F3 = paths['F3'] # existing file to copy from |
| main.file_write(F1, "This is initially file F1.\n") |
| main.file_write(F3, "This is initially file F3.\n") |
| main.run_svn(None, 'add', F1, F3) |
| D1 = paths['D1'] # existing dir to copy from |
| main.run_svn(None, 'mkdir', D1) |
| |
| # create the initial parent dirs, and each file or dir unless to-be-added |
| for init_mods, action_mods in scenarios: |
| path = "_".join(action_mods) |
| P = os.path.join(br_dir, path) # parent of items to be tested |
| main.run_svn(None, 'mkdir', '--parents', P) |
| for modaction in init_mods: |
| modify(modaction, incoming_paths(wc_dir, P)) |
| run_and_verify_svn(AnyOutput, [], |
| 'commit', '-m', 'Initial set-up.', wc_dir) |
| # Capture the revision number |
| init_rev = 2 ### hard-coded |
| |
| # modify all files and dirs in their various ways |
| for _path, action_mods in scenarios: |
| path = "_".join(action_mods) |
| P = os.path.join(br_dir, path) # parent |
| for modaction in action_mods: |
| modify(modaction, incoming_paths(wc_dir, P)) |
| |
| # commit all the modifications |
| run_and_verify_svn(AnyOutput, [], |
| 'commit', '-m', 'Action.', wc_dir) |
| # Capture the revision number |
| changed_rev = 3 ### hard-coded |
| |
| return (init_rev, changed_rev) |
| |
| #---------------------------------------------------------------------- |
| |
| # Apply each of the changes in INCOMING_SCENARIOS to each of the local |
| # modifications in LOCALMOD_SCENARIOS. |
| # Ensure that the result in each case includes a tree conflict on the parent. |
| # OPERATION = 'update' or 'switch' or 'merge' |
| # If COMMIT_LOCAL_MODS is true, the LOCALMOD_SCENARIOS will be committed to |
| # the target branch before applying the INCOMING_SCENARIOS. |
| def ensure_tree_conflict(sbox, operation, |
| incoming_scenarios, localmod_scenarios, |
| commit_local_mods=False): |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| |
| def url_of(repo_relative_path): |
| return sbox.repo_url + '/' + repo_relative_path |
| |
| logger.debug("") |
| logger.debug("=== Starting a set of '" + operation + "' tests.") |
| |
| # Path to source branch, relative to wc_dir. |
| # Source is where the "incoming" mods are made. |
| source_br = "branch1" |
| |
| logger.debug("--- Creating changes in repos") |
| source_wc_dir = os.path.join(wc_dir, source_br) |
| source_left_rev, source_right_rev = set_up_repos(wc_dir, source_wc_dir, |
| incoming_scenarios) |
| head_rev = source_right_rev ### assumption |
| |
| # Local mods are the outer loop because cleaning up the WC is slow |
| # ('svn revert' isn't sufficient because it leaves unversioned files) |
| for _loc_init_mods, loc_action in localmod_scenarios: |
| # Determine the branch (directory) in which local mods will be made. |
| if operation == 'update': |
| # Path to target branch (where conflicts are raised), relative to wc_dir. |
| target_br = source_br |
| target_start_rev = source_left_rev |
| else: # switch/merge |
| # Make, and work in, a "branch2" that is a copy of "branch1". |
| target_br = "branch2" |
| run_and_verify_svn(AnyOutput, [], |
| 'copy', '-r', str(source_left_rev), url_of(source_br), |
| url_of(target_br), |
| '-m', 'Create target branch.') |
| head_rev += 1 |
| target_start_rev = head_rev |
| |
| main.run_svn(None, 'checkout', '-r', str(target_start_rev), sbox.repo_url, |
| wc_dir) |
| |
| saved_cwd = os.getcwd() |
| os.chdir(wc_dir) |
| |
| for _inc_init_mods, inc_action in incoming_scenarios: |
| scen_name = "_".join(inc_action) |
| source_url = url_of(source_br + '/' + scen_name) |
| target_path = os.path.join(target_br, scen_name) |
| |
| logger.debug("=== " + str(inc_action) + " onto " + str(loc_action)) |
| |
| logger.debug("--- Making local mods") |
| for modaction in loc_action: |
| modify(modaction, localmod_paths(".", target_path), is_init=False) |
| if commit_local_mods: |
| run_and_verify_svn(AnyOutput, [], |
| 'commit', target_path, |
| '-m', 'Mods in target branch.') |
| head_rev += 1 |
| |
| # For update, verify the pre-condition that WC is out of date. |
| # For switch/merge, there is no such precondition. |
| if operation == 'update': |
| logger.debug("--- Trying to commit (expecting 'out-of-date' error)") |
| run_and_verify_commit(".", None, None, ".*Commit failed.*", |
| target_path) |
| |
| if modaction.startswith('f'): |
| victim_name = 'F' |
| else: |
| victim_name = 'D' |
| victim_path = os.path.join(target_path, victim_name) |
| |
| # Perform the operation that tries to apply incoming changes to the WC. |
| # The command is expected to do something (and give some output), |
| # and it should raise a conflict but not an error. |
| expected_stdout = svntest.verify.ExpectedOutput(" C " + victim_path |
| + "\n", |
| match_all=False) |
| # Do the main action |
| if operation == 'update': |
| logger.debug("--- Updating") |
| run_and_verify_svn(expected_stdout, [], |
| 'update', target_path, '--accept=postpone') |
| elif operation == 'switch': |
| logger.debug("--- Switching") |
| run_and_verify_svn(expected_stdout, [], |
| 'switch', source_url, target_path) |
| elif operation == 'merge': |
| logger.debug("--- Merging") |
| run_and_verify_svn(expected_stdout, [], |
| 'merge', |
| '--allow-mixed-revisions', |
| '--accept=postpone', |
| '-r', str(source_left_rev) + ':' + str(source_right_rev), |
| source_url, target_path) |
| else: |
| raise Exception("unknown operation: '" + operation + "'") |
| |
| logger.debug("--- Checking that 'info' reports the conflict") |
| if operation == 'update' or operation == 'switch': |
| incoming_left_rev = target_start_rev |
| else: |
| incoming_left_rev = source_left_rev |
| if operation == 'update' or operation == 'merge': |
| incoming_right_rev = source_right_rev |
| else: |
| incoming_right_rev = head_rev |
| expected_info = { 'Tree conflict' : '.* upon ' + operation + |
| r'.* \((none|(file|dir).*' + |
| re.escape(victim_name + '@' + str(incoming_left_rev)) + r')' + |
| r'.* \((none|(file|dir).*' + |
| re.escape(victim_name + '@' + str(incoming_right_rev)) + r')' } |
| run_and_verify_info([expected_info], victim_path) |
| |
| logger.debug("--- Trying to commit (expecting 'conflict' error)") |
| ### run_and_verify_commit() requires an "output_tree" argument, but |
| # here we get away with passing None because we know an implementation |
| # detail: namely that it's not going to look at that argument if it |
| # gets the stderr that we're expecting. |
| run_and_verify_commit(".", None, None, ".*conflict.*", victim_path) |
| |
| logger.debug("--- Checking that 'status' reports the conflict") |
| expected_stdout = AlternateOutput([ |
| RegexListOutput([ |
| "^......C.* " + re.escape(victim_path) + "$", |
| "^ > .* upon " + operation] + |
| svntest.main.summary_of_conflicts(tree_conflicts=1)), |
| RegexListOutput([ |
| "^......C.* " + re.escape(victim_path) + "$", |
| "^ > moved to .*", |
| "^ > .* upon " + operation] + |
| svntest.main.summary_of_conflicts(tree_conflicts=1)) |
| ]) |
| run_and_verify_svn(expected_stdout, [], |
| 'status', victim_path) |
| |
| logger.debug("--- Resolving the conflict") |
| # Make sure resolving the parent does nothing. |
| run_and_verify_resolved([], os.path.dirname(victim_path)) |
| # The real resolved call. |
| run_and_verify_resolved([victim_path]) |
| |
| logger.debug("--- Checking that 'status' does not report a conflict") |
| exitcode, stdout, stderr = run_and_verify_svn(None, [], |
| 'status', victim_path) |
| for line in stdout: |
| if line[6] == 'C': # and line.endswith(victim_path + '\n'): |
| raise svntest.Failure("unexpected status C") # on victim_path |
| |
| # logger.debug("--- Committing (should now succeed)") |
| # run_and_verify_svn(None, [], |
| # 'commit', '-m', '', target_path) |
| # target_start_rev += 1 |
| |
| logger.debug("") |
| |
| os.chdir(saved_cwd) |
| |
| # Clean up the target branch and WC |
| main.run_svn(None, 'revert', '-R', wc_dir) |
| main.safe_rmtree(wc_dir) |
| if operation != 'update': |
| run_and_verify_svn(AnyOutput, [], |
| 'delete', url_of(target_br), |
| '-m', 'Delete target branch.') |
| head_rev += 1 |
| |
| #---------------------------------------------------------------------- |
| |
| # Tests for update/switch affecting a file, where the incoming change |
| # conflicts with a scheduled change in the WC. |
| # |
| # WC state: as scheduled (no obstruction) |
| |
| def up_sw_file_mod_onto_del(sbox): |
| "up/sw file: modify onto del/rpl" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'update', f_mods, |
| f_dels + f_rpls) |
| ensure_tree_conflict(sbox2, 'switch', f_mods, |
| f_dels + f_rpls) |
| # Note: See UC1 in notes/tree-conflicts/use-cases.txt. |
| |
| def up_sw_file_del_onto_mod(sbox): |
| "up/sw file: del/rpl/mv onto modify" |
| # Results: tree-conflict on F |
| # no other change to WC (except possibly other half of move) |
| # ### OR (see Nico's email <>): |
| # schedule-delete but leave F on disk (can only apply with |
| # text-mod; prop-mod can't be preserved in this way) |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'update', f_dels + f_moves + f_rpls, |
| f_mods) |
| ensure_tree_conflict(sbox2, 'switch', f_dels + f_moves + f_rpls, |
| f_mods) |
| # Note: See UC2 in notes/tree-conflicts/use-cases.txt. |
| |
| def up_sw_file_del_onto_del(sbox): |
| "up/sw file: del/rpl/mv onto del/rpl/mv" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'update', f_dels + f_moves + f_rpls, |
| f_dels + f_rpls) |
| ensure_tree_conflict(sbox2, 'switch', f_dels + f_moves + f_rpls, |
| f_dels + f_rpls) |
| # Note: See UC3 in notes/tree-conflicts/use-cases.txt. |
| |
| def up_sw_file_add_onto_add(sbox): |
| "up/sw file: add onto add" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'update', f_adds, f_adds) |
| ensure_tree_conflict(sbox2, 'switch', f_adds, f_adds) |
| |
| #---------------------------------------------------------------------- |
| |
| # Tests for update/switch affecting a dir, where the incoming change |
| # conflicts with a scheduled change in the WC. |
| |
| def up_sw_dir_mod_onto_del(sbox): |
| "up/sw dir: modify onto del/rpl/mv" |
| # WC state: any (D necessarily exists; children may have any state) |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'update', d_mods, |
| d_dels + d_rpls) |
| ensure_tree_conflict(sbox2, 'switch', d_mods, |
| d_dels + d_rpls) |
| |
| def up_sw_dir_del_onto_mod(sbox): |
| "up/sw dir: del/rpl/mv onto modify" |
| # WC state: any (D necessarily exists; children may have any state) |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'update', d_dels + d_moves + d_rpls, |
| d_mods) |
| ensure_tree_conflict(sbox2, 'switch', d_dels + d_moves + d_rpls, |
| d_mods) |
| |
| def up_sw_dir_del_onto_del(sbox): |
| "up/sw dir: del/rpl/mv onto del/rpl/mv" |
| # WC state: any (D necessarily exists; children may have any state) |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'update', d_dels + d_moves + d_rpls, |
| d_dels + d_rpls) |
| ensure_tree_conflict(sbox2, 'switch', d_dels + d_moves + d_rpls, |
| d_dels + d_rpls) |
| |
| # This is currently set as XFail over ra_dav because it hits |
| # issue #3314 'DAV can overwrite directories during copy' |
| # |
| # TRUNK@35827.DBG>svn st -v branch1 |
| # 2 2 jrandom branch1 |
| # 2 2 jrandom branch1\dC |
| # A + - 2 jrandom branch1\dC\D |
| # |
| # TRUNK@35827.DBG>svn log -r2:HEAD branch1 -v |
| # ------------------------------------------------------------------------ |
| # r2 | jrandom | 2009-02-12 09:26:52 -0500 (Thu, 12 Feb 2009) | 1 line |
| # Changed paths: |
| # A /D1 |
| # A /F1 |
| # A /branch1 |
| # A /branch1/dC |
| # |
| # Initial set-up. |
| # ------------------------------------------------------------------------ |
| # r3 | jrandom | 2009-02-12 09:26:52 -0500 (Thu, 12 Feb 2009) | 1 line |
| # Changed paths: |
| # A /branch1/dC/D (from /D1:2) |
| # |
| # Action. |
| # ------------------------------------------------------------------------ |
| # |
| # TRUNK@35827.DBG>svn ci -m "Should be ood" branch1 |
| # Adding branch1\dC\D |
| # |
| # Committed revision 4. |
| @Issue(3314) |
| def up_sw_dir_add_onto_add(sbox): |
| "up/sw dir: add onto add" |
| # WC state: as scheduled (no obstruction) |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'update', d_adds, d_adds) |
| ensure_tree_conflict(sbox2, 'switch', d_adds, d_adds) |
| |
| #---------------------------------------------------------------------- |
| |
| # Tests for merge affecting a file, where the incoming change |
| # conflicts with the target. |
| |
| def merge_file_mod_onto_not_file(sbox): |
| "merge file: modify onto not-file" |
| sbox2 = sbox.clone_dependent() |
| # Test merges where the "local mods" are committed to the target branch. |
| ensure_tree_conflict(sbox, 'merge', f_mods, f_dels + f_moves + f_rpl_d, |
| commit_local_mods=True) |
| # Test merges where the "local mods" are uncommitted mods in the WC. |
| ensure_tree_conflict(sbox2, 'merge', f_mods, f_dels + f_moves) |
| # Note: See UC4 in notes/tree-conflicts/use-cases.txt. |
| |
| def merge_file_del_onto_not_same(sbox): |
| "merge file: del/rpl/mv onto not-same" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'merge', f_dels + f_moves + f_rpls, f_mods, |
| commit_local_mods=True) |
| ensure_tree_conflict(sbox2, 'merge', f_dels + f_moves + f_rpls, f_mods) |
| # Note: See UC5 in notes/tree-conflicts/use-cases.txt. |
| |
| def merge_file_del_onto_not_file(sbox): |
| "merge file: del/rpl/mv onto not-file" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'merge', f_dels + f_moves + f_rpls, |
| f_dels + f_moves + f_rpl_d, |
| commit_local_mods=True) |
| ensure_tree_conflict(sbox2, 'merge', f_dels + f_moves + f_rpls, |
| f_dels + f_moves) |
| # Note: See UC6 in notes/tree-conflicts/use-cases.txt. |
| |
| def merge_file_add_onto_not_none(sbox): |
| "merge file: add onto not-none" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'merge', f_adds, f_adds, |
| commit_local_mods=True) |
| ensure_tree_conflict(sbox2, 'merge', f_adds, f_adds) |
| # TODO: Also test directory adds at path "F"? |
| |
| #---------------------------------------------------------------------- |
| |
| # Tests for merge affecting a dir, where the incoming change |
| # conflicts with the target branch. |
| |
| def merge_dir_mod_onto_not_dir(sbox): |
| "merge dir: modify onto not-dir" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'merge', d_mods, d_dels + d_moves + d_rpl_f, |
| commit_local_mods=True) |
| ensure_tree_conflict(sbox2, 'merge', d_mods, d_dels + d_moves) |
| |
| # Test for issue #3150 'tree conflicts with directories as victims'. |
| @Issue(3150) |
| def merge_dir_del_onto_not_same(sbox): |
| "merge dir: del/rpl/mv onto not-same" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'merge', d_dels + d_moves + d_rpls, d_mods, |
| commit_local_mods=True) |
| ensure_tree_conflict(sbox2, 'merge', d_dels + d_moves + d_rpls, d_mods) |
| |
| def merge_dir_del_onto_not_dir(sbox): |
| "merge dir: del/rpl/mv onto not-dir" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'merge', d_dels + d_moves + d_rpls, |
| d_dels + d_moves + d_rpl_f, |
| commit_local_mods=True) |
| ensure_tree_conflict(sbox2, 'merge', d_dels + d_moves + d_rpls, |
| d_dels + d_moves) |
| |
| def merge_dir_add_onto_not_none(sbox): |
| "merge dir: add onto not-none" |
| sbox2 = sbox.clone_dependent() |
| ensure_tree_conflict(sbox, 'merge', d_adds, d_adds, |
| commit_local_mods=True) |
| ensure_tree_conflict(sbox2, 'merge', d_adds, d_adds) |
| # TODO: also try with file adds at path "D"? |
| |
| #---------------------------------------------------------------------- |
| |
| @Issue(3805) |
| def force_del_tc_inside(sbox): |
| "--force del on dir with TCs inside" |
| |
| # A/C <- delete with --force |
| # A + C A/C/dir |
| # A + C A/C/file |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| |
| C = os.path.join(wc_dir, "A", "C") |
| dir = os.path.join(wc_dir, "A", "C", "dir") |
| file = os.path.join(wc_dir, "A", "C", "file") |
| |
| # Add dir |
| main.run_svn(None, 'mkdir', dir) |
| |
| # Add file |
| content = "This is the file 'file'.\n" |
| main.file_append(file, content) |
| main.run_svn(None, 'add', file) |
| |
| main.run_svn(None, 'commit', '-m', 'Add dir and file', wc_dir) |
| |
| # Remove dir and file in r3. |
| main.run_svn(None, 'delete', dir, file) |
| main.run_svn(None, 'commit', '-m', 'Remove dir and file', wc_dir) |
| |
| # Warp back to -r2, dir and file coming back. |
| main.run_svn(None, 'update', '-r2', wc_dir) |
| |
| # Set a meaningless prop on each dir and file |
| run_and_verify_svn(["property 'propname' set on '" + dir + "'\n"], |
| [], 'ps', 'propname', 'propval', dir) |
| run_and_verify_svn(["property 'propname' set on '" + file + "'\n"], |
| [], 'ps', 'propname', 'propval', file) |
| |
| # Update WC to HEAD, tree conflicts result dir and file |
| # because there are local mods on the props. |
| expected_output = wc.State(wc_dir, { |
| 'A/C/dir' : Item(status=' ', treeconflict='C'), |
| 'A/C/file' : Item(status=' ', treeconflict='C'), |
| }) |
| |
| expected_disk = main.greek_state.copy() |
| expected_disk.add({ |
| 'A/C/dir' : Item(props={'propname' : 'propval'}), |
| 'A/C/file' : Item(contents=content, props={'propname' : 'propval'}), |
| }) |
| |
| expected_status = get_virginal_state(wc_dir, 2) |
| expected_status.tweak(wc_rev='3') |
| expected_status.add({ |
| 'A/C/dir' : Item(status='A ', wc_rev='-', copied='+', treeconflict='C'), |
| 'A/C/file' : Item(status='A ', wc_rev='-', copied='+', treeconflict='C'), |
| }) |
| run_and_verify_update(wc_dir, |
| expected_output, expected_disk, expected_status, |
| check_props=True) |
| |
| # Delete A/C with --force, in effect disarming the tree-conflicts. |
| run_and_verify_svn(verify.UnorderedOutput(['D ' + C + '\n', |
| 'D ' + dir + '\n', |
| 'D ' + file + '\n']), |
| [], 'delete', C, '--force') |
| |
| # Verify deletion status |
| # Note: the tree conflicts are removed because we forced the delete. |
| expected_status.tweak('A/C', status='D ') |
| expected_status.remove('A/C/dir', 'A/C/file') |
| |
| run_and_verify_status(wc_dir, expected_status) |
| |
| # Commit, remove the "disarmed" tree-conflict. |
| expected_output = wc.State(wc_dir, { 'A/C' : Item(verb='Deleting') }) |
| |
| expected_status.remove('A/C') |
| |
| run_and_verify_commit(wc_dir, |
| expected_output, expected_status) |
| |
| #---------------------------------------------------------------------- |
| |
| @Issue(3805) |
| def force_del_tc_is_target(sbox): |
| "--force del on tree-conflicted targets" |
| # A/C |
| # A + C A/C/dir <- delete with --force |
| # A + C A/C/file <- delete with --force |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| |
| C = os.path.join(wc_dir, "A", "C") |
| dir = os.path.join(wc_dir, "A", "C", "dir") |
| file = os.path.join(wc_dir, "A", "C", "file") |
| |
| # Add dir |
| main.run_svn(None, 'mkdir', dir) |
| |
| # Add file |
| content = "This is the file 'file'.\n" |
| main.file_append(file, content) |
| main.run_svn(None, 'add', file) |
| |
| main.run_svn(None, 'commit', '-m', 'Add dir and file', wc_dir) |
| |
| # Remove dir and file in r3. |
| main.run_svn(None, 'delete', dir, file) |
| main.run_svn(None, 'commit', '-m', 'Remove dir and file', wc_dir) |
| |
| # Warp back to -r2, dir and file coming back. |
| main.run_svn(None, 'update', '-r2', wc_dir) |
| |
| # Set a meaningless prop on each dir and file |
| run_and_verify_svn(["property 'propname' set on '" + dir + "'\n"], |
| [], 'ps', 'propname', 'propval', dir) |
| run_and_verify_svn(["property 'propname' set on '" + file + "'\n"], |
| [], 'ps', 'propname', 'propval', file) |
| |
| # Update WC to HEAD, tree conflicts result dir and file |
| # because there are local mods on the props. |
| expected_output = wc.State(wc_dir, { |
| 'A/C/dir' : Item(status=' ', treeconflict='C'), |
| 'A/C/file' : Item(status=' ', treeconflict='C'), |
| }) |
| |
| expected_disk = main.greek_state.copy() |
| expected_disk.add({ |
| 'A/C/dir' : Item(props={'propname' : 'propval'}), |
| 'A/C/file' : Item(contents=content, props={'propname' : 'propval'}), |
| }) |
| |
| expected_status = get_virginal_state(wc_dir, 2) |
| expected_status.tweak(wc_rev='3') |
| expected_status.add({ |
| 'A/C/dir' : Item(status='A ', wc_rev='-', copied='+', treeconflict='C'), |
| 'A/C/file' : Item(status='A ', wc_rev='-', copied='+', treeconflict='C'), |
| }) |
| run_and_verify_update(wc_dir, |
| expected_output, expected_disk, expected_status, |
| check_props=True) |
| |
| # Delete nodes with --force, in effect disarming the tree-conflicts. |
| run_and_verify_svn(['D ' + dir + '\n', |
| 'D ' + file + '\n'], |
| [], |
| 'delete', dir, file, '--force') |
| |
| # The rm --force now removes the nodes and the tree conflicts on them |
| expected_status.remove('A/C/dir', 'A/C/file') |
| run_and_verify_status(wc_dir, expected_status) |
| |
| # Commit, remove the "disarmed" tree-conflict. |
| expected_output = wc.State(wc_dir, {}) |
| |
| run_and_verify_commit(wc_dir, |
| expected_output, expected_status) |
| |
| #---------------------------------------------------------------------- |
| |
| # A regression test to check that "rm --keep-local" on a tree-conflicted |
| # node leaves the WC in a valid state in which simple commands such as |
| # "status" do not error out. At one time the command left the WC in an |
| # invalid state. (Before r989189, "rm --keep-local" used to have the effect |
| # of "disarming" the conflict in the sense that "commit" would ignore the |
| # conflict.) |
| |
| def query_absent_tree_conflicted_dir(sbox): |
| "query an unversioned tree-conflicted dir" |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| |
| # Some paths we'll care about |
| C_path = os.path.join(wc_dir, "A", "C") |
| C_C_path = os.path.join(wc_dir, "A", "C", "C") |
| |
| # Add a directory A/C/C as r2. |
| main.run_svn(None, 'mkdir', C_C_path) |
| main.run_svn(None, 'commit', '-m', 'Add directory A/C/C', wc_dir) |
| |
| # Remove that directory A/C/C as r3. |
| main.run_svn(None, 'delete', C_C_path) |
| main.run_svn(None, 'commit', '-m', 'Remove directory A/C/C', wc_dir) |
| |
| # Warp back to -r2 with the directory added. |
| main.run_svn(None, 'update', '-r2', wc_dir) |
| |
| # Set a meaningless prop on A/C/C |
| run_and_verify_svn(["property 'propname' set on '" + C_C_path + "'\n"], |
| [], 'ps', 'propname', 'propval', C_C_path) |
| |
| # Update WC to HEAD, a tree conflict results on A/C/C because of the |
| # working prop on A/C/C. |
| expected_output = wc.State(wc_dir, { |
| 'A/C/C' : Item(status=' ', treeconflict='C'), |
| }) |
| expected_disk = main.greek_state.copy() |
| expected_disk.add({'A/C/C' : Item(props={'propname' : 'propval'})}) |
| expected_status = get_virginal_state(wc_dir, 1) |
| expected_status.tweak(wc_rev='3') |
| expected_status.add({'A/C/C' : Item(status='A ', |
| wc_rev='-', |
| copied='+', |
| treeconflict='C')}) |
| run_and_verify_update(wc_dir, |
| expected_output, expected_disk, expected_status, |
| check_props=True) |
| |
| # Delete A/C with --keep-local. |
| run_and_verify_svn(verify.UnorderedOutput(['D ' + C_C_path + '\n', |
| 'D ' + C_path + '\n']), |
| [], |
| 'delete', C_path, '--keep-local') |
| |
| expected_status.tweak('A/C', status='D ') |
| expected_status.remove('A/C/C') |
| run_and_verify_status(wc_dir, expected_status) |
| |
| # Try to access the absent tree-conflict as explicit target. |
| # These used to fail like this: |
| ## CMD: svn status -v -u -q |
| ## [...] |
| ## subversion/svn/status-cmd.c:248: (apr_err=155035) |
| ## subversion/svn/util.c:953: (apr_err=155035) |
| ## subversion/libsvn_client/status.c:270: (apr_err=155035) |
| ## subversion/libsvn_wc/lock.c:607: (apr_err=155035) |
| ## subversion/libsvn_wc/entries.c:1607: (apr_err=155035) |
| ## subversion/libsvn_wc/wc_db.c:3288: (apr_err=155035) |
| ## svn: Expected node '/.../tree_conflict_tests-20/A/C' to be added. |
| |
| # A/C/C is now unversioned, using status: |
| expected_output = wc.State(wc_dir, { |
| }) |
| run_and_verify_status(C_C_path, expected_output) |
| |
| # using info: |
| run_and_verify_svn(None, ".*W155010.*The node.*was not found.*", |
| 'info', C_C_path) |
| |
| #---------------------------------------------------------------------- |
| |
| @Issue(3608) |
| def up_add_onto_add_revert(sbox): |
| "issue #3608: reverting an add onto add conflict" |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| wc2_dir = sbox.add_wc_path('wc2') |
| svntest.actions.run_and_verify_svn(None, [], 'checkout', |
| sbox.repo_url, wc2_dir) |
| |
| file1 = os.path.join(wc_dir, 'newfile') |
| file2 = os.path.join(wc2_dir, 'newfile') |
| |
| dir1 = os.path.join(wc_dir, 'NewDir') |
| dir2 = os.path.join(wc2_dir, 'NewDir') |
| |
| main.run_svn(None, 'cp', os.path.join(wc_dir, 'iota'), file1) |
| main.run_svn(None, 'cp', os.path.join(wc2_dir, 'iota'), file2) |
| |
| main.run_svn(None, 'cp', os.path.join(wc_dir, 'A/C'), dir1) |
| main.run_svn(None, 'cp', os.path.join(wc2_dir, 'A/C'), dir2) |
| |
| sbox.simple_commit(message='Added file') |
| |
| expected_disk = main.greek_state.copy() |
| expected_disk.add({ |
| 'newfile' : Item(contents="This is the file 'iota'.\n"), |
| 'NewDir' : Item(), |
| }) |
| |
| expected_status = get_virginal_state(wc2_dir, 2) |
| expected_status.add({ |
| 'newfile' : Item(status='R ', copied='+', treeconflict='C', wc_rev='-'), |
| 'NewDir' : Item(status='R ', copied='+', treeconflict='C', wc_rev='-'), |
| }) |
| |
| run_and_verify_update(wc2_dir, |
| None, expected_disk, expected_status, |
| check_props=True) |
| |
| # Currently (r927086), this removes dir2 and file2 in a way that |
| # they don't reappear after update. |
| main.run_svn(None, 'revert', file2) |
| main.run_svn(None, 'revert', dir2) |
| |
| expected_status = get_virginal_state(wc2_dir, 2) |
| expected_status.add({ |
| 'newfile' : Item(status=' ', wc_rev='2'), |
| 'NewDir' : Item(status=' ', wc_rev='2'), |
| }) |
| |
| # Expected behavior is that after revert + update the tree matches |
| # the repository |
| run_and_verify_update(wc2_dir, |
| None, expected_disk, expected_status, |
| check_props=True) |
| |
| |
| #---------------------------------------------------------------------- |
| # Regression test for issue #3525 and #3533 |
| # |
| @Issues(3525,3533) |
| def lock_update_only(sbox): |
| "lock status update shouldn't flag tree conflict" |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| |
| # Make a second copy of the working copy |
| wc_b = sbox.add_wc_path('_b') |
| svntest.actions.duplicate_dir(wc_dir, wc_b) |
| |
| fname = 'iota' |
| file_path = os.path.join(sbox.wc_dir, fname) |
| file_path_b = os.path.join(wc_b, fname) |
| |
| # Lock a file as wc_author, and schedule the file for deletion. |
| svntest.actions.run_and_verify_svn(".*locked by user", [], 'lock', |
| '-m', '', file_path) |
| svntest.main.run_svn(None, 'delete', file_path) |
| |
| # In our other working copy, steal that lock. |
| svntest.actions.run_and_verify_svn(".*locked by user", [], 'lock', |
| '-m', '', '--force', file_path) |
| |
| # Now update the first working copy. It should appear as a no-op. |
| expected_disk = main.greek_state.copy() |
| expected_disk.remove('iota') |
| expected_status = get_virginal_state(wc_dir, 1) |
| expected_status.tweak('iota', status='D ', writelocked='K') |
| run_and_verify_update(wc_dir, |
| None, expected_disk, expected_status, |
| check_props=True) |
| |
| |
| #---------------------------------------------------------------------- |
| @Issue(3469) |
| def at_directory_external(sbox): |
| "tree conflict at directory external" |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| |
| # r2: create a directory external: ^/E -> ^/A |
| svntest.main.run_svn(None, 'ps', 'svn:externals', '^/A E', wc_dir) |
| svntest.main.run_svn(None, 'commit', '-m', 'ps', wc_dir) |
| svntest.main.run_svn(None, 'update', wc_dir) |
| |
| # r3: modify ^/A/B/E/alpha |
| with open(sbox.ospath('A/B/E/alpha'), 'a') as f: |
| f.write('This is still A/B/E/alpha.\n') |
| svntest.main.run_svn(None, 'commit', '-m', 'file mod', wc_dir) |
| svntest.main.run_svn(None, 'update', wc_dir) |
| merge_rev = svntest.main.youngest(sbox.repo_dir) |
| |
| # r4: create ^/A/B/E/alpha2 |
| with open(sbox.ospath('A/B/E/alpha2'), 'a') as f: |
| f.write("This is the file 'alpha2'.\n") |
| svntest.main.run_svn(None, 'add', sbox.ospath('A/B/E/alpha2')) |
| svntest.main.run_svn(None, 'commit', '-m', 'file add', wc_dir) |
| svntest.main.run_svn(None, 'update', wc_dir) |
| merge_rev2 = svntest.main.youngest(sbox.repo_dir) |
| |
| # r5: merge those |
| svntest.main.run_svn(None, "merge", '-c', merge_rev, '^/A/B', wc_dir) |
| svntest.main.run_svn(None, "merge", '-c', merge_rev2, '^/A/B', wc_dir) |
| |
| #---------------------------------------------------------------------- |
| @Issue(3779) |
| ### This test currently passes on the current behaviour. |
| ### However in many cases it is unclear whether the current behaviour is |
| ### correct. Review is still required. |
| def actual_only_node_behaviour(sbox): |
| "test behaviour with actual-only nodes" |
| |
| sbox.build() |
| A_url = sbox.repo_url + '/A' |
| A_copy_url = sbox.repo_url + '/A_copy' |
| wc_dir = sbox.wc_dir |
| foo_path = sbox.ospath('A/foo', wc_dir) |
| |
| # r2: copy ^/A -> ^/A_copy |
| sbox.simple_repo_copy('A', 'A_copy') |
| |
| # r3: add a file foo on ^/A_copy branch |
| wc2_dir = sbox.add_wc_path('wc2') |
| foo2_path = sbox.ospath('foo', wc2_dir) |
| svntest.main.run_svn(None, "checkout", A_copy_url, wc2_dir) |
| svntest.main.file_write(foo2_path, "This is initially file foo.\n") |
| svntest.main.run_svn(None, "add", foo2_path) |
| svntest.main.run_svn(None, "commit", '-m', svntest.main.make_log_msg(), |
| foo2_path) |
| |
| # r4: make a change to foo |
| svntest.main.file_append(foo2_path, "This is a new line in file foo.\n") |
| svntest.main.run_svn(None, "commit", '-m', svntest.main.make_log_msg(), |
| wc2_dir) |
| |
| # cherry-pick r4 to ^/A -- the resulting tree conflict creates |
| # an actual-only node for 'A/foo' |
| sbox.simple_update() |
| svntest.main.run_svn(None, "merge", '-c', '4', A_copy_url, |
| os.path.join(wc_dir, 'A')) |
| |
| # Attempt running various commands on foo and verify expected behavior |
| |
| # add |
| expected_stdout = None |
| expected_stderr = ".*foo.*is an existing item in conflict.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "add", foo_path) |
| |
| # add (with an existing obstruction of foo) |
| svntest.main.file_write(foo_path, "This is an obstruction of foo.\n") |
| expected_stdout = None |
| expected_stderr = ".*foo.*is an existing item in conflict.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "add", foo_path) |
| os.remove(foo_path) # remove obstruction |
| |
| # blame (praise, annotate, ann) |
| expected_stdout = None |
| expected_stderr = ".*foo.*not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "blame", foo_path) |
| |
| # cat |
| expected_stdout = None |
| expected_stderr = ".*foo.*not under version control.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "cat", foo_path) |
| |
| # cat -rBASE |
| expected_stdout = None |
| expected_stderr = ".*foo.*not under version control.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "cat", "-r", "BASE", foo_path) |
| # changelist (cl) |
| expected_stdout = None |
| expected_stderr = ".*svn: warning: W155010: The node '.*foo' was not found." |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "changelist", "my_changelist", foo_path) |
| |
| # checkout (co) |
| ### this does not error out -- needs review |
| expected_stdout = None |
| expected_stderr = [] |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "checkout", A_copy_url, foo_path) |
| ### for now, ignore the fact that checkout succeeds and remove the nested |
| ### working copy so we can test more commands |
| def onerror(function, path, execinfo): |
| os.chmod(path, stat.S_IREAD | stat.S_IWRITE) |
| os.remove(path) |
| shutil.rmtree(foo_path, onerror=onerror) |
| |
| # cleanup |
| expected_stdout = None |
| expected_stderr = ".*foo.*is not a working copy directory" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "cleanup", foo_path) |
| # commit (ci) |
| expected_stdout = None |
| expected_stderr = ".*foo.*remains in conflict.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "commit", foo_path) |
| # copy (cp) |
| expected_stdout = None |
| expected_stderr = ".*foo.*does not exist.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "copy", foo_path, foo_path + ".copy") |
| |
| # delete (del, remove, rm) |
| expected_stdout = None |
| expected_stderr = ".*foo.*is not under version control.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "delete", foo_path) |
| |
| # diff (di) |
| expected_stdout = None |
| expected_stderr = ".*E155.*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "diff", foo_path) |
| # export |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "export", foo_path, sbox.get_tempname()) |
| # import |
| expected_stdout = None |
| expected_stderr = ".*(foo.*does not exist|Can't stat.*foo).*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "import", '-m', svntest.main.make_log_msg(), |
| foo_path, sbox.repo_url + '/foo_imported') |
| |
| # info |
| expected_info = { |
| 'Tree conflict': 'local missing or deleted or moved away, incoming file edit upon merge.*', |
| 'Name': 'foo', |
| 'Schedule': 'normal', |
| 'Node Kind': 'none', |
| 'Path': re.escape(sbox.ospath('A/foo')), |
| } |
| run_and_verify_info([expected_info], foo_path) |
| |
| # list (ls) |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "list", foo_path) |
| |
| # lock |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "lock", foo_path) |
| # log |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "log", foo_path) |
| # merge |
| # note: this is intentionally a no-op merge that does not record mergeinfo |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "merge", '--ignore-ancestry', '-c', '4', |
| A_copy_url + '/mu', foo_path) |
| |
| # mergeinfo |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "mergeinfo", A_copy_url + '/foo', foo_path) |
| # mkdir |
| expected_stdout = None |
| expected_stderr = ".*foo.*is an existing item in conflict.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "mkdir", foo_path) |
| |
| # move (mv, rename, ren) |
| expected_stdout = None |
| expected_stderr = ".*foo.*does not exist.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "move", foo_path, foo_path + ".moved") |
| # patch |
| expected_stdout = None |
| expected_stderr = ".*foo.*does not exist.*" |
| patch_path = sbox.get_tempname() |
| f = open(patch_path, 'w') |
| patch_data = [ |
| "--- foo (revision 2)\n" |
| "+++ foo (working copy)\n" |
| "@@ -1 +1,2 @@\n" |
| " foo\n" |
| " +foo\n" |
| ] |
| for line in patch_data: |
| f.write(line) |
| f.close() |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "patch", patch_path, sbox.ospath("A/foo")) |
| |
| # propdel (pdel, pd) |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "propdel", "svn:eol-style", foo_path) |
| |
| # propget (pget, pg) |
| expected_stdout = None |
| expected_stderr = ".*foo.*is not under version control.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "propget", "svn:eol-style", foo_path) |
| |
| # proplist (plist, pl) |
| expected_stdout = None |
| expected_stderr = ".*foo.*is not under version control.*" |
| svntest.actions.run_and_verify_svn(expected_stdout, expected_stderr, |
| "proplist", foo_path) |
| |
| # propset (pset, ps) |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "propset", "svn:eol-style", "native", foo_path) |
| |
| # relocate |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "relocate", A_copy_url + "/foo", foo_path) |
| |
| # resolve |
| expected_stdout = "Tree conflict at.*foo.*marked as resolved" |
| expected_stderr = [] |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "resolve", "--accept", "working", foo_path) |
| |
| # revert the entire working copy and repeat the merge so we can test |
| # more commands |
| svntest.main.run_svn(None, "revert", "-R", wc_dir) |
| svntest.main.run_svn(None, "merge", '-c', '4', A_copy_url, |
| os.path.join(wc_dir, 'A')) |
| |
| # revert |
| expected_stdout = "Reverted.*foo.*" |
| expected_stderr = [] |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "revert", foo_path) |
| |
| # revert the entire working copy and repeat the merge so we can test |
| # more commands |
| svntest.main.run_svn(None, "revert", "-R", wc_dir) |
| svntest.main.run_svn(None, "merge", '-c', '4', A_copy_url, |
| os.path.join(wc_dir, 'A')) |
| |
| # revert |
| expected_stdout = "Reverted.*foo.*" |
| expected_stderr = [] |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "revert", "-R", foo_path) |
| |
| # revert the entire working copy and repeat the merge so we can test |
| # more commands |
| svntest.main.run_svn(None, "revert", "-R", wc_dir) |
| svntest.main.run_svn(None, "merge", '-c', '4', A_copy_url, |
| os.path.join(wc_dir, 'A')) |
| |
| # status (stat, st) |
| expected_status = wc.State(foo_path, { |
| '' : Item(status='! ', treeconflict='C'), |
| }) |
| run_and_verify_status(foo_path, expected_status) |
| |
| # switch (sw) |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "switch", A_copy_url + "/foo", foo_path) |
| |
| # unlock |
| expected_stdout = None |
| expected_stderr = ".*foo.*was not found.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "unlock", foo_path) |
| |
| # update (up) |
| # This doesn't skip because the update is anchored at the parent of A, |
| # the parent of A is not in conflict, and the update doesn't attempt to |
| # change foo itself. |
| expected_stdout = [ |
| "Updating '" + foo_path + "':\n", "At revision 4.\n"] |
| expected_stderr = [] |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "update", foo_path) |
| |
| # upgrade |
| expected_stdout = None |
| expected_stderr = ".*Can't upgrade.*foo.*" |
| run_and_verify_svn(expected_stdout, expected_stderr, |
| "upgrade", foo_path) |
| |
| #---------------------------------------------------------------------- |
| # Regression test for an issue #3526 variant |
| # |
| @Issues(3526) |
| def update_dir_with_not_present(sbox): |
| "lock status update shouldn't flag tree conflict" |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| |
| newtxt = sbox.ospath('A/B/new.txt') |
| |
| main.file_write(newtxt, 'new.txt') |
| sbox.simple_add('A/B/new.txt') |
| sbox.simple_commit() |
| |
| sbox.simple_move('A/B/new.txt', 'A/C/newer.txt') |
| sbox.simple_commit() |
| sbox.simple_rm('A/B') |
| |
| # We can't commit this without updating (ra_svn produces its own error) |
| run_and_verify_svn(None, |
| "svn: (E155011|E160028|E170004): (Dir|Item).*B.*out of date", |
| 'ci', '-m', '', wc_dir) |
| |
| # So we run update |
| run_and_verify_svn(None, [], |
| 'up', wc_dir) |
| |
| # And now we can commit |
| run_and_verify_svn(None, [], |
| 'ci', '-m', '', wc_dir) |
| |
| def update_delete_mixed_rev(sbox): |
| "update that deletes mixed-rev" |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| sbox.simple_move('A/B/E/alpha', 'A/B/E/alpha2') |
| sbox.simple_commit() |
| sbox.simple_update() |
| sbox.simple_rm('A/B') |
| sbox.simple_commit() |
| sbox.simple_update(revision=1) |
| sbox.simple_update(target='A/B/E', revision=2) |
| sbox.simple_mkdir('A/B/E2') |
| |
| # Update raises a tree conflict on A/B due to local mod A/B/E2 |
| expected_output = wc.State(wc_dir, { |
| 'A/B' : Item(status=' ', treeconflict='C'), |
| }) |
| expected_disk = main.greek_state.copy() |
| expected_disk.add({ |
| 'A/B/E2' : Item(), |
| 'A/B/E/alpha2' : Item(contents='This is the file \'alpha\'.\n'), |
| }) |
| expected_disk.remove('A/B/E/alpha') |
| expected_status = get_virginal_state(wc_dir, 3) |
| expected_status.remove('A/B/E/alpha') |
| expected_status.add({ |
| 'A/B/E2' : Item(status='A ', wc_rev='-'), |
| 'A/B/E/alpha2' : Item(status=' ', copied='+', wc_rev='-'), |
| }) |
| expected_status.tweak('A/B', |
| status='A ', copied='+', treeconflict='C', wc_rev='-') |
| expected_status.tweak('A/B/F', 'A/B/E', 'A/B/E/beta', 'A/B/lambda', |
| copied='+', wc_rev='-') |
| |
| # The entries world doesn't see a changed revision as another add |
| # while the WC-NG world does... |
| expected_status.tweak('A/B/E', status='A ', entry_status=' ') |
| run_and_verify_update(wc_dir, |
| expected_output, expected_disk, expected_status, |
| check_props=True) |
| |
| # Resolving to working state should give a mixed-revision copy that |
| # gets committed as multiple copies |
| run_and_verify_resolved([sbox.ospath('A/B')], sbox.ospath('A/B')) |
| expected_output = wc.State(wc_dir, { |
| 'A/B' : Item(verb='Adding'), |
| 'A/B/E' : Item(verb='Replacing'), |
| 'A/B/E2' : Item(verb='Adding'), |
| }) |
| expected_status.tweak('A/B', 'A/B/E', 'A/B/E2', 'A/B/F', 'A/B/E/alpha2', |
| 'A/B/E/beta', 'A/B/lambda', |
| status=' ', wc_rev=4, copied=None, treeconflict=None) |
| run_and_verify_commit(wc_dir, |
| expected_output, expected_status) |
| |
| expected_info = { |
| 'Name': 'alpha2', |
| 'Node Kind': 'file', |
| } |
| run_and_verify_info([expected_info], sbox.repo_url + '/A/B/E/alpha2') |
| |
| # NB: This test will run forever if the bug it is testing for is present! |
| def local_missing_dir_endless_loop(sbox): |
| "endless loop when resolving local-missing dir" |
| |
| sbox.build() |
| wc_dir = sbox.wc_dir |
| sbox.simple_copy('A', 'A1') |
| sbox.simple_commit() |
| sbox.simple_update() |
| sbox.simple_move('A/B', 'A/B2') |
| sbox.simple_commit() |
| sbox.simple_update() |
| main.file_append(sbox.ospath("A/B2/lambda"), "This is more content.\n") |
| sbox.simple_commit() |
| sbox.simple_update() |
| |
| # Create a config which enables the interactive conflict resolver |
| config_contents = '''\ |
| [auth] |
| password-stores = |
| |
| [miscellany] |
| interactive-conflicts = true |
| ''' |
| config_dir = sbox.create_config_dir(config_contents) |
| |
| # Bug: 'svn' keeps retrying interactive conflict resolution while the library |
| # keeps signalling 'SVN_ERR_WC_CONFLICT_RESOLVER_FAILURE' -> endless loop |
| main.run_svn("Tree conflict on '%s'" % sbox.ospath("A1/B2"), |
| 'merge', '-c4', '^/A', sbox.ospath('A1'), |
| '--config-dir', config_dir, '--force-interactive') |
| |
| # If everything works as expected the resolver will recommended a |
| # resolution option and 'svn' will resolve the conflict automatically. |
| # Verify that 'A1/B/lambda' contains the merged content: |
| contents = open(sbox.ospath('A1/B/lambda'), 'r').readlines() |
| svntest.verify.compare_and_display_lines( |
| "A1/B/lambda has unexpectected contents", sbox.ospath("A1/B/lambda"), |
| [ "This is the file 'lambda'.\n", "This is more content.\n"], contents) |
| |
| |
| ####################################################################### |
| # Run the tests |
| |
| |
| # list all tests here, starting with None: |
| test_list = [ None, |
| up_sw_file_mod_onto_del, |
| up_sw_file_del_onto_mod, |
| up_sw_file_del_onto_del, |
| up_sw_file_add_onto_add, |
| up_sw_dir_mod_onto_del, |
| up_sw_dir_del_onto_mod, |
| up_sw_dir_del_onto_del, |
| up_sw_dir_add_onto_add, |
| merge_file_mod_onto_not_file, |
| merge_file_del_onto_not_same, |
| merge_file_del_onto_not_file, |
| merge_file_add_onto_not_none, |
| merge_dir_mod_onto_not_dir, |
| merge_dir_del_onto_not_same, |
| merge_dir_del_onto_not_dir, |
| merge_dir_add_onto_not_none, |
| force_del_tc_inside, |
| force_del_tc_is_target, |
| query_absent_tree_conflicted_dir, |
| up_add_onto_add_revert, |
| lock_update_only, |
| at_directory_external, |
| actual_only_node_behaviour, |
| update_dir_with_not_present, |
| update_delete_mixed_rev, |
| local_missing_dir_endless_loop, |
| ] |
| |
| if __name__ == '__main__': |
| svntest.main.run_tests(test_list) |
| # NOTREACHED |
| |
| |
| ### End of file. |