3
# log_tests.py: testing "svn log"
5
# Subversion is a tool for revision control.
6
# See http://subversion.tigris.org for more information.
8
# ====================================================================
9
# Copyright (c) 2000-2004 CollabNet. All rights reserved.
11
# This software is licensed as described in the file COPYING, which
12
# you should have received as part of this distribution. The terms
13
# are also available at http://subversion.tigris.org/license-1.html.
14
# If newer versions of this license are posted there, you may use a
15
# newer version instead, at your option.
17
######################################################################
20
import string, sys, re, os, shutil
24
from svntest import SVNAnyOutput
27
######################################################################
31
# Get a repository, commit about 6 or 7 revisions to it, each
32
# involving different kinds of operations. Make sure to have some
33
# add, del, mv, cp, as well as file modifications, and make sure that
34
# some files are modified more than once.
36
# Give each commit a recognizable log message. Test all combinations
37
# of -r options, including none. Then test with -v, which will
38
# (presumably) show changed paths as well.
40
######################################################################
44
######################################################################
48
# These variables are set by guarantee_repos_and_wc().
49
max_revision = 0 # Highest revision in the repos
51
# What separates log msgs from one another in raw log output.
52
msg_separator = '------------------------------------' \
53
+ '------------------------------------\n'
57
Skip = svntest.testcase.Skip
58
XFail = svntest.testcase.XFail
59
Item = svntest.wc.StateItem
62
######################################################################
66
def guarantee_repos_and_wc(sbox):
67
"Make a repos and wc, commit max_revision revs."
73
# Now we have a repos and wc at revision 1.
75
was_cwd = os.getcwd ()
78
# Set up the paths we'll be using most often.
79
iota_path = os.path.join ('iota')
80
mu_path = os.path.join ('A', 'mu')
81
B_path = os.path.join ('A', 'B')
82
omega_path = os.path.join ('A', 'D', 'H', 'omega')
83
pi_path = os.path.join ('A', 'D', 'G', 'pi')
84
rho_path = os.path.join ('A', 'D', 'G', 'rho')
85
alpha_path = os.path.join ('A', 'B', 'E', 'alpha')
86
beta_path = os.path.join ('A', 'B', 'E', 'beta')
87
psi_path = os.path.join ('A', 'D', 'H', 'psi')
88
epsilon_path = os.path.join ('A', 'C', 'epsilon')
90
# Do a varied bunch of commits. No copies yet, we'll wait till Ben
93
# Revision 2: edit iota
94
svntest.main.file_append (iota_path, "2")
95
svntest.main.run_svn (None, 'ci', '-m', "Log message for revision 2")
96
svntest.main.run_svn (None, 'up')
98
# Revision 3: edit A/D/H/omega, A/D/G/pi, A/D/G/rho, and A/B/E/alpha
99
svntest.main.file_append (omega_path, "3")
100
svntest.main.file_append (pi_path, "3")
101
svntest.main.file_append (rho_path, "3")
102
svntest.main.file_append (alpha_path, "3")
103
svntest.main.run_svn (None, 'ci', '-m', "Log message for revision 3")
104
svntest.main.run_svn (None, 'up')
106
# Revision 4: edit iota again, add A/C/epsilon
107
svntest.main.file_append (iota_path, "4")
108
svntest.main.file_append (epsilon_path, "4")
109
svntest.main.run_svn (None, 'add', epsilon_path)
110
svntest.main.run_svn (None, 'ci', '-m', "Log message for revision 4")
111
svntest.main.run_svn (None, 'up')
113
# Revision 5: edit A/C/epsilon, delete A/D/G/rho
114
svntest.main.file_append (epsilon_path, "5")
115
svntest.main.run_svn (None, 'rm', rho_path)
116
svntest.main.run_svn (None, 'ci', '-m', "Log message for revision 5")
117
svntest.main.run_svn (None, 'up')
119
# Revision 6: prop change on A/B, edit A/D/H/psi
120
svntest.main.run_svn (None, 'ps', 'blue', 'azul', B_path)
121
svntest.main.file_append (psi_path, "6")
122
svntest.main.run_svn (None, 'ci', '-m', "Log message for revision 6")
123
svntest.main.run_svn (None, 'up')
125
# Revision 7: edit A/mu, prop change on A/mu
126
svntest.main.file_append (mu_path, "7")
127
svntest.main.run_svn (None, 'ps', 'red', 'burgundy', mu_path)
128
svntest.main.run_svn (None, 'ci', '-m', "Log message for revision 7")
129
svntest.main.run_svn (None, 'up')
131
# Revision 8: edit iota yet again, re-add A/D/G/rho
132
svntest.main.file_append (iota_path, "8")
133
svntest.main.file_append (rho_path, "8")
134
svntest.main.run_svn (None, 'add', rho_path)
135
svntest.main.run_svn (None, 'ci', '-m', "Log message for revision 8")
136
svntest.main.run_svn (None, 'up')
138
# Revision 9: edit A/B/E/beta, delete A/B/E/alpha
139
svntest.main.file_append (beta_path, "9")
140
svntest.main.run_svn (None, 'rm', alpha_path)
141
svntest.main.run_svn (None, 'ci', '-m', "Log message for revision 9")
142
svntest.main.run_svn (None, 'up')
149
# Let's run 'svn status' and make sure the working copy looks
150
# exactly the way we think it should. Start with a generic
151
# greek-tree-list, where every local and repos revision is at 9.
152
expected_status = svntest.actions.get_virginal_state(wc_path, 9)
153
expected_status.remove('A/B/E/alpha')
154
expected_status.add({
155
'A/C/epsilon' : Item(status=' ', wc_rev=9),
158
# props exist on A/B and A/mu
159
expected_status.tweak('A/B', 'A/mu', status=' ')
161
# Run 'svn st -uv' and compare the actual results with our tree.
162
svntest.actions.run_and_verify_status(wc_path, expected_status)
167
# For errors seen while parsing log data.
168
class SVNLogParseError(Exception):
169
def __init__ (self, args=None):
173
def parse_log_output(log_lines):
174
"""Return a log chain derived from LOG_LINES.
175
A log chain is a list of hashes; each hash represents one log
176
message, in the order it appears in LOG_LINES (the first log
177
message in the data is also the first element of the list, and so
180
Each hash contains the following keys/values:
182
'revision' ===> number
185
'msg' ===> string (the log message itself)
187
If LOG_LINES contains changed-path information, then the hash
190
'paths' ===> list of strings
194
# Here's some log output to look at while writing this function:
196
# ------------------------------------------------------------------------
197
# r5 | kfogel | Tue 6 Nov 2001 17:18:19 | 1 line
199
# Log message for revision 5.
200
# ------------------------------------------------------------------------
201
# r4 | kfogel | Tue 6 Nov 2001 17:18:18 | 1 line
203
# Log message for revision 4.
204
# ------------------------------------------------------------------------
205
# r3 | kfogel | Tue 6 Nov 2001 17:18:17 | 1 line
207
# Log message for revision 3.
208
# ------------------------------------------------------------------------
209
# r2 | kfogel | Tue 6 Nov 2001 17:18:16 | 1 line
211
# Log message for revision 2.
212
# ------------------------------------------------------------------------
213
# r1 | foo | Tue 6 Nov 2001 15:27:57 | 1 line
215
# Log message for revision 1.
216
# ------------------------------------------------------------------------
218
# Regular expression to match the header line of a log message, with
219
# these groups: (revision number), (author), (date), (num lines).
220
header_re = re.compile ('^r([0-9]+) \| ' \
221
+ '([^|]*) \| ([^|]*) \| ([0-9]+) lines?')
223
# The log chain to return.
229
this_line = log_lines.pop (0)
233
match = header_re.search (this_line)
234
if match and match.groups ():
236
this_item['revision'] = match.group(1)
237
this_item['author'] = match.group(2)
238
this_item['date'] = match.group(3)
239
lines = string.atoi ((match.group (4)))
241
# Eat the expected blank line.
244
### todo: we don't parse changed-paths yet, since Subversion
245
### doesn't output them. When it does, they'll appear here,
246
### right after the header line, and then there'll be a blank
247
### line between them and the msg.
249
# Accumulate the log message
251
for line in log_lines[0:lines]:
253
del log_lines[0:lines]
254
elif this_line == msg_separator:
256
this_item['msg'] = msg
257
chain.append (this_item)
258
else: # if didn't see separator now, then something's wrong
259
raise SVNLogParseError, "trailing garbage after log message"
264
def check_log_chain (chain, revlist):
265
"""Verify that log chain CHAIN contains the right log messages for
266
revisions START to END (see documentation for parse_log_output() for
267
more about log chains.)
269
Do nothing if the log chain's messages run from revision START to END
270
with no gaps, and that each log message is one line of the form
272
'Log message for revision N'
274
where N is the revision number of that commit. Also verify that
275
author and date are present and look sane, but don't check them too
278
Raise if anything looks wrong.
281
for expect_rev in revlist:
282
log_item = chain.pop (0)
283
saw_rev = string.atoi (log_item['revision'])
284
date = log_item['date']
285
author = log_item['author']
286
msg = log_item['msg']
287
# The most important check is that the revision is right:
288
if expect_rev != saw_rev: raise svntest.Failure
289
# Check that date looks at least vaguely right:
290
date_re = re.compile ('[0-9]+')
291
if (not date_re.search (date)): raise svntest.Failure
292
# Authors are a little harder, since they might not exist over ra-dav.
293
# Well, it's not much of a check, but we'll do what we can.
294
author_re = re.compile ('[a-zA-Z]+')
295
if (not (author_re.search (author)
297
or author == '(no author)')): raise svntest.Failure
298
# Check that the log message looks right:
299
msg_re = re.compile ('Log message for revision ' + `saw_rev`)
300
if (not msg_re.search (msg)): raise svntest.Failure
302
### todo: need some multi-line log messages mixed in with the
303
### one-liners. Easy enough, just make the prime revisions use REV
304
### lines, and the rest use 1 line, or something, so it's
305
### predictable based on REV.
308
######################################################################
312
#----------------------------------------------------------------------
314
"'svn log', no args, top of wc"
316
guarantee_repos_and_wc(sbox)
318
was_cwd = os.getcwd()
319
os.chdir(sbox.wc_dir)
322
output, err = svntest.actions.run_and_verify_svn ("", None, [], 'log')
324
log_chain = parse_log_output (output)
325
if check_log_chain (log_chain, range(max_revision, 1 - 1, -1)):
326
raise svntest.Failure
332
#----------------------------------------------------------------------
333
def versioned_log_message(sbox):
334
"'svn commit -F foo' when foo is a versioned file"
338
was_cwd = os.getcwd ()
339
os.chdir (sbox.wc_dir)
342
iota_path = os.path.join ('iota')
343
mu_path = os.path.join ('A', 'mu')
344
log_path = os.path.join ('A', 'D', 'H', 'omega')
346
svntest.main.file_append (iota_path, "2")
348
# try to check in a change using a versioned file as your log entry.
349
svntest.actions.run_and_verify_svn("", None, SVNAnyOutput,
350
'ci', '-F', log_path)
352
# force it. should not produce any errors.
353
svntest.actions.run_and_verify_svn ("", None, [],
354
'ci', '-F', log_path, '--force-log')
356
svntest.main.file_append (mu_path, "2")
358
# try the same thing, but specifying the file to commit explicitly.
359
svntest.actions.run_and_verify_svn("", None, SVNAnyOutput,
360
'ci', '-F', log_path, mu_path)
362
# force it... should succeed.
363
svntest.actions.run_and_verify_svn ("", None, [],
366
'--force-log', mu_path)
372
#----------------------------------------------------------------------
373
def log_with_empty_repos(sbox):
374
"'svn log' on an empty repository"
376
# Create virgin repos
377
svntest.main.safe_rmtree(sbox.repo_dir, 1)
378
svntest.main.create_repos(sbox.repo_dir)
379
svntest.main.set_repos_paths(sbox.repo_dir)
381
svntest.actions.run_and_verify_svn ("", None, [],
383
'--username', svntest.main.wc_author,
384
'--password', svntest.main.wc_passwd,
385
svntest.main.current_repo_url)
387
#----------------------------------------------------------------------
388
def log_where_nothing_changed(sbox):
389
"'svn log -rN some_dir_unchanged_in_N'"
392
# Fix bug whereby running 'svn log -rN SOMEPATH' would result in an
393
# xml protocol error if there were no changes in revision N
394
# underneath SOMEPATH. This problem was introduced in revision
395
# 3811, which didn't cover the case where svn_repos_get_logs might
396
# invoke log_receiver zero times. Since the receiver never ran, the
397
# lrb->needs_header flag never got cleared. Control would proceed
398
# without error to the end of dav_svn__log_report(), which would
399
# send a closing tag even though no opening tag had ever been sent.
401
rho_path = os.path.join (sbox.wc_dir, 'A', 'D', 'G', 'rho')
402
svntest.main.file_append (rho_path, "some new material in rho")
403
svntest.actions.run_and_verify_svn(None, None, [], 'ci', '-m',
406
# Now run 'svn log -r2' on a directory unaffected by revision 2.
407
H_path = os.path.join (sbox.wc_dir, 'A', 'D', 'H')
408
svntest.actions.run_and_verify_svn(None, None, [],
409
'log', '-r', '2', H_path)
412
#----------------------------------------------------------------------
413
def log_to_revision_zero(sbox):
414
"'svn log -v -r 1:0 wc_root'"
417
# This used to segfault the server.
419
svntest.actions.run_and_verify_svn(None, None, [],
421
'-r', '1:0', sbox.wc_dir)
423
#----------------------------------------------------------------------
424
def log_with_path_args(sbox):
425
"'svn log', with args, top of wc"
427
guarantee_repos_and_wc(sbox)
429
was_cwd = os.getcwd()
430
os.chdir(sbox.wc_dir)
433
output, err = svntest.actions.run_and_verify_svn(
435
'log', svntest.main.current_repo_url, 'A/D/G', 'A/D/H')
437
log_chain = parse_log_output (output)
438
if check_log_chain (log_chain, [8, 6, 5, 3, 1]):
439
raise svntest.Failure
444
#----------------------------------------------------------------------
445
def url_missing_in_head(sbox):
446
"'svn log -r N URL' when URL is not in HEAD "
448
guarantee_repos_and_wc(sbox)
450
my_url = svntest.main.current_repo_url + "/A/B/E/alpha"
452
svntest.actions.run_and_verify_svn(None, None, [],
453
'log', '-r', '8', my_url)
455
#----------------------------------------------------------------------
456
def log_through_copyfrom_history(sbox):
457
"'svn log TGT' with copyfrom history"
461
mu_path = os.path.join (wc_dir, 'A', 'mu')
462
mu2_path = os.path.join (wc_dir, 'A', 'mu2')
463
mu_URL = svntest.main.current_repo_url + '/A/mu'
464
mu2_URL = svntest.main.current_repo_url + '/A/mu2'
466
svntest.main.file_append (mu_path, "2")
467
svntest.actions.run_and_verify_svn (None, None, [], 'ci', wc_dir,
468
'-m', "Log message for revision 2")
469
svntest.main.file_append (mu2_path, "this is mu2")
470
svntest.actions.run_and_verify_svn (None, None, [], 'add', mu2_path)
471
svntest.actions.run_and_verify_svn (None, None, [], 'ci', wc_dir,
472
'-m', "Log message for revision 3")
473
svntest.actions.run_and_verify_svn (None, None, [], 'rm', mu2_path)
474
svntest.actions.run_and_verify_svn (None, None, [], 'ci', wc_dir,
475
'-m', "Log message for revision 4")
476
svntest.main.file_append (mu_path, "5")
477
svntest.actions.run_and_verify_svn (None, None, [], 'ci', wc_dir,
478
'-m', "Log message for revision 5")
480
svntest.actions.run_and_verify_svn (None, None, [],
481
'cp', '-r', '5', mu_URL, mu2_URL,
482
'-m', "Log message for revision 6")
483
svntest.actions.run_and_verify_svn (None, None, [], 'up', wc_dir)
485
# The full log for mu2 is relatively unsurprising
486
output, err = svntest.actions.run_and_verify_svn (None, None, [],
488
log_chain = parse_log_output (output)
489
if check_log_chain (log_chain, [6, 5, 2, 1]):
490
raise svntest.Failure
492
output, err = svntest.actions.run_and_verify_svn (None, None, [],
494
log_chain = parse_log_output (output)
495
if check_log_chain (log_chain, [6, 5, 2, 1]):
496
raise svntest.Failure
498
# First "oddity", the full log for mu2 doesn't include r3, but the -r3
500
output, err = svntest.actions.run_and_verify_svn (None, None, [],
501
'log', '-r', '3', mu2_path)
502
log_chain = parse_log_output (output)
503
if check_log_chain (log_chain, [3]):
504
raise svntest.Failure
506
output, err = svntest.actions.run_and_verify_svn (None, None, [],
507
'log', '-r', '3', mu2_URL)
508
log_chain = parse_log_output (output)
509
if check_log_chain (log_chain, [3]):
510
raise svntest.Failure
512
# Second "oddity", the full log for mu2 includes r2, but the -r2 log
514
svntest.actions.run_and_verify_svn (None, [], SVNAnyOutput,
515
'log', '-r', '2', mu2_path)
516
svntest.actions.run_and_verify_svn (None, [], SVNAnyOutput,
517
'log', '-r', '2', mu2_URL)
519
#----------------------------------------------------------------------
520
def escape_control_chars(sbox):
521
"mod_dav_svn must escape invalid XML control chars"
523
dump_str = """SVN-fs-dump-format-version: 2
525
UUID: ffcae364-69ee-0310-a980-ca5f10462af2
528
Prop-content-length: 56
534
2005-01-24T10:09:21.759592Z
538
Prop-content-length: 128
544
This msg contains a Ctrl-T (\x14) and a Ctrl-I (\t).
545
The former might be escaped, but the latter never.
554
2005-01-24T10:09:22.012524Z
558
# Create virgin repos and working copy
559
svntest.main.safe_rmtree(sbox.repo_dir, 1)
560
svntest.main.create_repos(sbox.repo_dir)
561
svntest.main.set_repos_paths(sbox.repo_dir)
563
URL = svntest.main.current_repo_url
565
# load dumpfile with control character into repos to get
566
# a log with control char content
568
svntest.main.run_command_stdin(
569
"%s load --quiet %s" % (svntest.main.svnadmin_binary, sbox.repo_dir),
573
output, errput = svntest.actions.run_and_verify_svn ("", None, [], 'log', URL)
575
# Verify the output contains either the expected fuzzy escape
576
# sequence, or the literal control char.
577
match_unescaped_ctrl_re = "This msg contains a Ctrl-T \(.\) " \
578
"and a Ctrl-I \(\t\)\."
579
match_escaped_ctrl_re = "^This msg contains a Ctrl-T \(\?\\\\020\) " \
580
"and a Ctrl-I \(\t\)\."
583
if re.match (match_unescaped_ctrl_re, line) \
584
or re.match (match_escaped_ctrl_re, line):
588
raise svntest.Failure ("log message not transmitted properly:" +
589
str(output) + "\n" + "error: " + str(errput))
592
########################################################################
596
# list all tests here, starting with None:
599
versioned_log_message,
600
log_with_empty_repos,
601
log_where_nothing_changed,
602
log_to_revision_zero,
605
log_through_copyfrom_history,
606
escape_control_chars,
609
if __name__ == '__main__':
610
svntest.main.run_tests(test_list)