1#!/usr/bin/env python
2#
3#  svnmucc_tests.py: tests of svnmucc
4#
5#  Subversion is a tool for revision control.
6#  See http://subversion.apache.org for more information.
7#
8# ====================================================================
9#    Licensed to the Apache Software Foundation (ASF) under one
10#    or more contributor license agreements.  See the NOTICE file
11#    distributed with this work for additional information
12#    regarding copyright ownership.  The ASF licenses this file
13#    to you under the Apache License, Version 2.0 (the
14#    "License"); you may not use this file except in compliance
15#    with the License.  You may obtain a copy of the License at
16#
17#      http://www.apache.org/licenses/LICENSE-2.0
18#
19#    Unless required by applicable law or agreed to in writing,
20#    software distributed under the License is distributed on an
21#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
22#    KIND, either express or implied.  See the License for the
23#    specific language governing permissions and limitations
24#    under the License.
25######################################################################
26
27import svntest
28import re
29
30XFail = svntest.testcase.XFail_deco
31Issues = svntest.testcase.Issues_deco
32Issue = svntest.testcase.Issue_deco
33
34######################################################################
35
36@Issues(3895,3953)
37def reject_bogus_mergeinfo(sbox):
38  "reject bogus mergeinfo"
39
40  sbox.build(create_wc=False)
41
42  expected_error = ".*(E200020.*Invalid revision|E175002.*PROPPATCH)"
43
44  # At present this tests the server, but if we ever make svnmucc
45  # validate the mergeinfo up front then it will only test the client
46  svntest.actions.run_and_verify_svnmucc([], expected_error,
47                                         'propset', 'svn:mergeinfo', '/B:0',
48                                         '-m', 'log msg',
49                                         sbox.repo_url + '/A')
50
51_svnmucc_re = re.compile(b'^(r[0-9]+) committed by jrandom at (.*)$')
52_log_re = re.compile('^   ([ADRM] /[^\(]+($| \(from .*:[0-9]+\)$))')
53_err_re = re.compile('^svnmucc: (.*)$')
54
55def test_svnmucc(repo_url, expected_path_changes, *varargs):
56  """Run svnmucc with the list of SVNMUCC_ARGS arguments.  Verify that
57  its run results in a new commit with 'svn log -rHEAD' changed paths
58  that match the list of EXPECTED_PATH_CHANGES."""
59
60  # First, run svnmucc.
61  exit_code, outlines, errlines = svntest.main.run_svnmucc('-U', repo_url,
62                                                           *varargs)
63  if errlines:
64    raise svntest.main.SVNCommitFailure(str(errlines))
65  if len(outlines) != 1 or not _svnmucc_re.match(outlines[0]):
66    raise svntest.main.SVNLineUnequal(str(outlines))
67
68  # Now, run 'svn log -vq -rHEAD'
69  changed_paths = []
70  exit_code, outlines, errlines = \
71    svntest.main.run_svn(None, 'log', '-vqrHEAD', repo_url)
72  if errlines:
73    raise svntest.Failure("Unable to verify commit with 'svn log': %s"
74                          % (str(errlines)))
75  for line in outlines:
76    match = _log_re.match(line)
77    if match:
78      changed_paths.append(match.group(1).rstrip('\n\r'))
79
80  expected_path_changes.sort()
81  changed_paths.sort()
82  if changed_paths != expected_path_changes:
83    raise svntest.Failure("Logged path changes differ from expectations\n"
84                          "   expected: %s\n"
85                          "     actual: %s" % (str(expected_path_changes),
86                                               str(changed_paths)))
87
88def xtest_svnmucc(repo_url, expected_errors, *varargs):
89  """Run svnmucc with the list of SVNMUCC_ARGS arguments.  Verify that
90  its run results match the list of EXPECTED_ERRORS."""
91
92  # First, run svnmucc.
93  exit_code, outlines, errlines = svntest.main.run_svnmucc('-U', repo_url,
94                                                           *varargs)
95  errors = []
96  for line in errlines:
97    match = _err_re.match(line)
98    if match:
99      errors.append(line.rstrip('\n\r'))
100  if errors != expected_errors:
101    raise svntest.main.SVNUnmatchedError(str(errors))
102
103
104def basic_svnmucc(sbox):
105  "basic svnmucc tests"
106
107  sbox.build()
108  empty_file = sbox.ospath('empty')
109  file = sbox.ospath('file')
110  svntest.main.file_append(empty_file, '')
111  svntest.main.file_append(file, 'file')
112
113  # revision 2
114  test_svnmucc(sbox.repo_url,
115               ['A /foo'
116                ], # ---------
117               '-m', 'log msg',
118               'mkdir', 'foo')
119
120  # revision 3
121  test_svnmucc(sbox.repo_url,
122               ['A /z.c',
123                ], # ---------
124               '-m', 'log msg',
125               'put', empty_file, 'z.c')
126
127  # revision 4
128  test_svnmucc(sbox.repo_url,
129               ['A /foo/z.c (from /z.c:3)',
130                'A /foo/bar (from /foo:3)',
131                ], # ---------
132               '-m', 'log msg',
133               'cp', '3', 'z.c', 'foo/z.c',
134               'cp', '3', 'foo', 'foo/bar')
135
136  # revision 5
137  test_svnmucc(sbox.repo_url,
138               ['A /zig (from /foo:4)',
139                'D /zig/bar',
140                'D /foo',
141                'A /zig/zag (from /foo:4)',
142                ], # ---------
143               '-m', 'log msg',
144               'cp', '4', 'foo', 'zig',
145               'rm',             'zig/bar',
146               'mv',      'foo', 'zig/zag')
147
148  # revision 6
149  test_svnmucc(sbox.repo_url,
150               ['D /z.c',
151                'A /zig/zag/bar/y.c (from /z.c:5)',
152                'A /zig/zag/bar/x.c (from /z.c:3)',
153                ], # ---------
154               '-m', 'log msg',
155               'mv',      'z.c', 'zig/zag/bar/y.c',
156               'cp', '3', 'z.c', 'zig/zag/bar/x.c')
157
158  # revision 7
159  test_svnmucc(sbox.repo_url,
160               ['D /zig/zag/bar/y.c',
161                'A /zig/zag/bar/y y.c (from /zig/zag/bar/y.c:6)',
162                'A /zig/zag/bar/y%20y.c (from /zig/zag/bar/y.c:6)',
163                ], # ---------
164               '-m', 'log msg',
165               'mv',         'zig/zag/bar/y.c', 'zig/zag/bar/y%20y.c',
166               'cp', 'HEAD', 'zig/zag/bar/y.c', 'zig/zag/bar/y%2520y.c')
167
168  # revision 8
169  test_svnmucc(sbox.repo_url,
170               ['D /zig/zag/bar/y y.c',
171                'A /zig/zag/bar/z z1.c (from /zig/zag/bar/y y.c:7)',
172                'A /zig/zag/bar/z%20z.c (from /zig/zag/bar/y%20y.c:7)',
173                'A /zig/zag/bar/z z2.c (from /zig/zag/bar/y y.c:7)',
174                ], #---------
175               '-m', 'log msg',
176               'mv',         'zig/zag/bar/y%20y.c',   'zig/zag/bar/z z1.c',
177               'cp', 'HEAD', 'zig/zag/bar/y%2520y.c', 'zig/zag/bar/z%2520z.c',
178               'cp', 'HEAD', 'zig/zag/bar/y y.c',     'zig/zag/bar/z z2.c')
179
180
181  # revision 9
182  test_svnmucc(sbox.repo_url,
183               ['D /zig/zag',
184                'A /zig/foo (from /zig/zag:8)',
185                'D /zig/foo/bar/z%20z.c',
186                'D /zig/foo/bar/z z2.c',
187                'R /zig/foo/bar/z z1.c (from /zig/zag/bar/x.c:6)',
188                ], #---------
189               '-m', 'log msg',
190               'mv',      'zig/zag',         'zig/foo',
191               'rm',                         'zig/foo/bar/z z1.c',
192               'rm',                         'zig/foo/bar/z%20z2.c',
193               'rm',                         'zig/foo/bar/z%2520z.c',
194               'cp', '6', 'zig/zag/bar/x.c', 'zig/foo/bar/z%20z1.c')
195
196  # revision 10
197  test_svnmucc(sbox.repo_url,
198               ['R /zig/foo/bar (from /zig/z.c:9)',
199                ], #---------
200               '-m', 'log msg',
201               'rm',                 'zig/foo/bar',
202               'cp', '9', 'zig/z.c', 'zig/foo/bar')
203
204  # revision 11
205  test_svnmucc(sbox.repo_url,
206               ['R /zig/foo/bar (from /zig/foo/bar:9)',
207                'D /zig/foo/bar/z z1.c',
208                ], #---------
209               '-m', 'log msg',
210               'rm',                     'zig/foo/bar',
211               'cp', '9', 'zig/foo/bar', 'zig/foo/bar',
212               'rm',                     'zig/foo/bar/z%20z1.c')
213
214  # revision 12
215  test_svnmucc(sbox.repo_url,
216               ['R /zig/foo (from /zig/foo/bar:11)',
217                ], #---------
218               '-m', 'log msg',
219               'rm',                        'zig/foo',
220               'cp', 'head', 'zig/foo/bar', 'zig/foo')
221
222  # revision 13
223  test_svnmucc(sbox.repo_url,
224               ['D /zig',
225                'A /foo (from /foo:4)',
226                'A /foo/foo (from /foo:4)',
227                'A /foo/foo/foo (from /foo:4)',
228                'D /foo/foo/bar',
229                'R /foo/foo/foo/bar (from /foo:4)',
230                ], #---------
231               '-m', 'log msg',
232               'rm',             'zig',
233               'cp', '4', 'foo', 'foo',
234               'cp', '4', 'foo', 'foo/foo',
235               'cp', '4', 'foo', 'foo/foo/foo',
236               'rm',             'foo/foo/bar',
237               'rm',             'foo/foo/foo/bar',
238               'cp', '4', 'foo', 'foo/foo/foo/bar')
239
240  # revision 14
241  test_svnmucc(sbox.repo_url,
242               ['A /boozle (from /foo:4)',
243                'A /boozle/buz',
244                'A /boozle/buz/nuz',
245                ], #---------
246               '-m', 'log msg',
247               'cp',    '4', 'foo', 'boozle',
248               'mkdir',             'boozle/buz',
249               'mkdir',             'boozle/buz/nuz')
250
251  # revision 15
252  test_svnmucc(sbox.repo_url,
253               ['A /boozle/buz/svnmucc-test.py',
254                'A /boozle/guz (from /boozle/buz:14)',
255                'A /boozle/guz/svnmucc-test.py',
256                ], #---------
257               '-m', 'log msg',
258               'put',      empty_file,   'boozle/buz/svnmucc-test.py',
259               'cp', '14', 'boozle/buz', 'boozle/guz',
260               'put',      empty_file,   'boozle/guz/svnmucc-test.py')
261
262  # revision 16
263  test_svnmucc(sbox.repo_url,
264               ['M /boozle/buz/svnmucc-test.py',
265                'R /boozle/guz/svnmucc-test.py',
266                ], #---------
267               '-m', 'log msg',
268               'put', empty_file, 'boozle/buz/svnmucc-test.py',
269               'rm',              'boozle/guz/svnmucc-test.py',
270               'put', empty_file, 'boozle/guz/svnmucc-test.py')
271
272  # revision 17
273  test_svnmucc(sbox.repo_url,
274               ['R /foo/bar (from /foo/foo:16)'
275                ], #---------
276               '-m', 'log msg',
277               'rm',                            'foo/bar',
278               'cp', '16', 'foo/foo',           'foo/bar',
279               'propset',  'testprop',  'true', 'foo/bar')
280
281  # revision 18
282  test_svnmucc(sbox.repo_url,
283               ['M /foo/bar'
284                ], #---------
285               '-m', 'log msg',
286               'propdel', 'testprop', 'foo/bar')
287
288  # revision 19
289  test_svnmucc(sbox.repo_url,
290               ['M /foo/z.c',
291                'M /foo/foo',
292                ], #---------
293               '-m', 'log msg',
294               'propset', 'testprop', 'true', 'foo/z.c',
295               'propset', 'testprop', 'true', 'foo/foo')
296
297  # revision 20
298  test_svnmucc(sbox.repo_url,
299               ['M /foo/z.c',
300                'M /foo/foo',
301                ], #---------
302               '-m', 'log msg',
303               'propsetf', 'testprop', empty_file, 'foo/z.c',
304               'propsetf', 'testprop', empty_file, 'foo/foo')
305
306  # revision 21
307  test_svnmucc(sbox.repo_url,
308               ['M /foo/z.c',
309                ], #---------
310               '-m', 'log msg',
311               'propset', 'testprop', 'false', 'foo/z.c',
312               'put', file, 'foo/z.c')
313
314  # Expected missing revision error
315  xtest_svnmucc(sbox.repo_url,
316                ["svnmucc: E200004: 'a' is not a revision"
317                 ], #---------
318                '-m', 'log msg',
319                'cp', 'a', 'b')
320
321  # Expected cannot be younger error
322  xtest_svnmucc(sbox.repo_url,
323                ['svnmucc: E160006: No such revision 42',
324                 ], #---------
325                '-m', 'log msg',
326                'cp', '42', 'a', 'b')
327
328  # Expected already exists error
329  xtest_svnmucc(sbox.repo_url,
330                ["svnmucc: E160020: Path 'foo' already exists",
331                 ], #---------
332                '-m', 'log msg',
333                'cp', '17', 'a', 'foo')
334
335  # Expected copy_src already exists error
336  xtest_svnmucc(sbox.repo_url,
337                ["svnmucc: E160020: Path 'a/bar' already exists",
338                 ], #---------
339                '-m', 'log msg',
340                'cp', '17', 'foo', 'a',
341                'cp', '17', 'foo/foo', 'a/bar')
342
343  # Expected not found error
344  xtest_svnmucc(sbox.repo_url,
345                ["svnmucc: E160013: Path 'a' not found in revision 17",
346                 ], #---------
347                '-m', 'log msg',
348                'cp', '17', 'a', 'b')
349
350
351def propset_root_internal(sbox, target):
352  ## propset on ^/
353  svntest.actions.run_and_verify_svnmucc(None, [],
354                                         '-m', 'log msg',
355                                         'propset', 'foo', 'bar',
356                                         target)
357  svntest.actions.run_and_verify_svn('bar', [],
358                                     'propget', '--no-newline', 'foo',
359                                     target)
360
361  ## propdel on ^/
362  svntest.actions.run_and_verify_svnmucc(None, [],
363                                         '-m', 'log msg',
364                                         'propdel', 'foo',
365                                         target)
366  svntest.actions.run_and_verify_svn([],
367                                     '.*W200017: Property.*not found',
368                                     'propget', '--no-newline', 'foo',
369                                     target)
370
371@Issues(3663)
372def propset_root(sbox):
373  "propset/propdel on repos root"
374
375  sbox.build(create_wc=False)
376  propset_root_internal(sbox, sbox.repo_url)
377  propset_root_internal(sbox, sbox.repo_url + '/iota')
378
379
380def too_many_log_messages(sbox):
381  "test log message mutual exclusivity checks"
382
383  sbox.build() # would use read-only=True, but need a place to stuff msg_file
384  msg_file = sbox.ospath('svnmucc_msg')
385  svntest.main.file_append(msg_file, 'some log message')
386  err_msg = ["svnmucc: E205000: --message (-m), --file (-F), and "
387             "--with-revprop=svn:log are mutually exclusive"]
388
389  xtest_svnmucc(sbox.repo_url, err_msg,
390                '--non-interactive',
391                '-m', 'log msg',
392                '-F', msg_file,
393                'mkdir', 'A/subdir')
394  xtest_svnmucc(sbox.repo_url, err_msg,
395                '--non-interactive',
396                '-m', 'log msg',
397                '--with-revprop', 'svn:log=proppy log message',
398                'mkdir', 'A/subdir')
399  xtest_svnmucc(sbox.repo_url, err_msg,
400                '--non-interactive',
401                '-F', msg_file,
402                '--with-revprop', 'svn:log=proppy log message',
403                'mkdir', 'A/subdir')
404  xtest_svnmucc(sbox.repo_url, err_msg,
405                '--non-interactive',
406                '-m', 'log msg',
407                '-F', msg_file,
408                '--with-revprop', 'svn:log=proppy log message',
409                'mkdir', 'A/subdir')
410
411@Issues(3418)
412def no_log_msg_non_interactive(sbox):
413  "test non-interactive without a log message"
414
415  sbox.build(create_wc=False)
416  xtest_svnmucc(sbox.repo_url,
417                ["svnmucc: E205001: Cannot invoke editor to get log message "
418                 "when non-interactive"
419                 ], #---------
420                '--non-interactive',
421                'mkdir', 'A/subdir')
422
423
424def nested_replaces(sbox):
425  "nested replaces"
426
427  sbox.build(create_wc=False)
428  repo_url = sbox.repo_url
429  svntest.actions.run_and_verify_svnmucc(None, [],
430                           '-U', repo_url, '-m', 'r2: create tree',
431                           'rm', 'A',
432                           'rm', 'iota',
433                           'mkdir', 'A', 'mkdir', 'A/B', 'mkdir', 'A/B/C',
434                           'mkdir', 'M', 'mkdir', 'M/N', 'mkdir', 'M/N/O',
435                           'mkdir', 'X', 'mkdir', 'X/Y', 'mkdir', 'X/Y/Z')
436  svntest.actions.run_and_verify_svnmucc(None, [],
437                           '-U', repo_url, '-m', 'r3: nested replaces',
438                           *("""
439rm A rm M rm X
440cp HEAD X/Y/Z A cp HEAD A/B/C M cp HEAD M/N/O X
441cp HEAD A/B A/B cp HEAD M/N M/N cp HEAD X/Y X/Y
442rm A/B/C rm M/N/O rm X/Y/Z
443cp HEAD X A/B/C cp HEAD A M/N/O cp HEAD M X/Y/Z
444rm A/B/C/Y
445                           """.split()))
446
447  # ### TODO: need a smarter run_and_verify_log() that verifies copyfrom
448  excaped = svntest.main.ensure_list(map(re.escape, [
449    '   R /A (from /X/Y/Z:2)',
450    '   A /A/B (from /A/B:2)',
451    '   R /A/B/C (from /X:2)',
452    '   R /M (from /A/B/C:2)',
453    '   A /M/N (from /M/N:2)',
454    '   R /M/N/O (from /A:2)',
455    '   R /X (from /M/N/O:2)',
456    '   A /X/Y (from /X/Y:2)',
457    '   R /X/Y/Z (from /M:2)',
458    '   D /A/B/C/Y',
459  ]))
460  expected_output = svntest.verify.UnorderedRegexListOutput(excaped
461                    + ['^--*', '^r3.*', '^--*', '^Changed paths:',])
462  svntest.actions.run_and_verify_svn(expected_output, [],
463                                     'log', '-qvr3', repo_url)
464
465
466def prohibited_deletes_and_moves(sbox):
467  "test prohibited delete and move operations"
468
469  # These action sequences were allowed in 1.8.13, but are prohibited in 1.9.x
470  # and later.  Most of them probably indicate an inadvertent user mistake.
471  # See dev@, 2015-05-11, "Re: Issue 4579 / svnmucc fails to process certain
472  # deletes", <http://svn.haxx.se/dev/archive-2015-05/0038.shtml>
473
474  sbox.build(read_only = True)
475  svntest.main.file_write(sbox.ospath('file'), "New contents")
476
477  xtest_svnmucc(sbox.repo_url,
478                ["svnmucc: E200009: Can't delete node at 'iota'",
479                 ], #---------
480                '-m', 'r2: modify and delete /iota',
481                'put', sbox.ospath('file'), 'iota',
482                'rm', 'iota')
483
484  xtest_svnmucc(sbox.repo_url,
485                ["svnmucc: E200009: Can't delete node at 'iota'",
486                 ], #---------
487                '-m', 'r2: propset and delete /iota',
488                'propset', 'prop', 'val', 'iota',
489                'rm', 'iota')
490
491  xtest_svnmucc(sbox.repo_url,
492                ["svnmucc: E160013: Can't delete node at 'iota' as it does "
493                 "not exist",
494                 ], #---------
495                '-m', 'r2: delete and delete /iota',
496                'rm', 'iota',
497                'rm', 'iota')
498
499  # Subversion 1.8.13 used to move /iota without applying the text change.
500  xtest_svnmucc(sbox.repo_url,
501                ["svnmucc: E200009: Can't delete node at 'iota'",
502                 ], #---------
503                '-m', 'r2: modify and move /iota',
504                'put', sbox.ospath('file'), 'iota',
505                'mv', 'iota', 'iota2')
506
507  # Subversion 1.8.13 used to move /A without applying the inner remove.
508  xtest_svnmucc(sbox.repo_url,
509                ["svnmucc: E200009: Can't delete node at 'A'",
510                 ], #---------
511                '-m', 'r2: delete /A/B and move /A',
512                'rm', 'A/B',
513                'mv', 'A', 'A1')
514
515def svnmucc_type_errors(sbox):
516  "test type errors"
517
518  sbox.build(read_only=True)
519
520  sbox.simple_append('file', 'New contents')
521
522  xtest_svnmucc(sbox.repo_url,
523                ["svnmucc: E160016: Can't operate on 'B' "
524                "because 'A' is not a directory"],
525                '-m', '',
526                'put', sbox.ospath('file'), 'A',
527                'mkdir', 'A/B',
528                'propset', 'iota', 'iota', 'iota')
529
530  xtest_svnmucc(sbox.repo_url,
531                ["svnmucc: E200009: Can't delete node at 'A'"],
532                '-m', '',
533                'mkdir', 'A/Z',
534                'put', sbox.ospath('file'), 'A')
535
536  xtest_svnmucc(sbox.repo_url,
537                ["svnmucc: E160020: Path 'Z' already exists, or was created "
538                 "by an earlier operation"],
539                '-m', '',
540                'mkdir', 'A/Z',
541                'put', sbox.ospath('file'), 'A/Z')
542
543def svnmucc_propset_and_put(sbox):
544  "propset and put"
545
546  sbox.build()
547
548  sbox.simple_append('file', 'New contents')
549
550  # First in the sane order: put, then propset
551  xtest_svnmucc(sbox.repo_url,
552                [],
553                '-m', '',
554                'put', sbox.ospath('file'), 't1',
555                'propset', 't1', 't1', 't1')
556
557  # And now in an impossible order: propset, then put
558  xtest_svnmucc(sbox.repo_url,
559                ["svnmucc: E200009: Can't set properties at not existing 't2'"],
560                '-m', '',
561                'propset', 't2', 't2', 't2',
562                'put', sbox.ospath('file'), 't2')
563
564  # And if the target already exists (dir)
565  xtest_svnmucc(sbox.repo_url,
566                ["svnmucc: E200009: Can't delete node at 'A'"],
567                '-m', '',
568                'propset', 'A', 'A', 'A',
569                'put', sbox.ospath('file'), 'A')
570
571  # And if the target already exists (file) # fixed in r1702467
572  xtest_svnmucc(sbox.repo_url,
573                [],
574                '-m', '',
575                'propset', 'iota', 'iota', 'iota',
576                'put', sbox.ospath('file'), 'iota')
577
578  # Put same file twice (non existing)
579  xtest_svnmucc(sbox.repo_url,
580                ["svnmucc: E160020: Path 't3' already exists, or was created "
581                 "by an earlier operation"],
582                '-m', '',
583                'put', sbox.ospath('file'), 't3',
584                'put', sbox.ospath('file'), 't3')
585
586  # Put same file twice (existing)
587  xtest_svnmucc(sbox.repo_url,
588                ["svnmucc: E200009: Can't update file at 't1'"],
589                '-m', '',
590                'put', sbox.ospath('file'), 't1',
591                'put', sbox.ospath('file'), 't1')
592
593
594######################################################################
595
596test_list = [ None,
597              reject_bogus_mergeinfo,
598              basic_svnmucc,
599              propset_root,
600              too_many_log_messages,
601              no_log_msg_non_interactive,
602              nested_replaces,
603              prohibited_deletes_and_moves,
604              svnmucc_type_errors,
605              svnmucc_propset_and_put,
606            ]
607
608if __name__ == '__main__':
609  svntest.main.run_tests(test_list)
610