1#!/usr/local/bin/python
2
3# ====================================================================
4#    Licensed to the Apache Software Foundation (ASF) under one
5#    or more contributor license agreements.  See the NOTICE file
6#    distributed with this work for additional information
7#    regarding copyright ownership.  The ASF licenses this file
8#    to you under the Apache License, Version 2.0 (the
9#    "License"); you may not use this file except in compliance
10#    with the License.  You may obtain a copy of the License at
11#
12#      http://www.apache.org/licenses/LICENSE-2.0
13#
14#    Unless required by applicable law or agreed to in writing,
15#    software distributed under the License is distributed on an
16#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17#    KIND, either express or implied.  See the License for the
18#    specific language governing permissions and limitations
19#    under the License.
20# ====================================================================
21
22# Run this without arguments to run unit tests.
23# Run with a path to a davautocheck ops log to test that it can parse that.
24
25import os
26import re
27import sys
28import tempfile
29try:
30  # Python >=3.0
31  from urllib.parse import quote as urllib_parse_quote
32except ImportError:
33  # Python <3.0
34  from urllib import quote as urllib_parse_quote
35import unittest
36
37import svn.core
38
39import svn_server_log_parse
40
41class TestCase(unittest.TestCase):
42    def setUp(self):
43        # Define a class to stuff everything passed to any handle_
44        # method into self.result.
45        class cls(svn_server_log_parse.Parser):
46            def __getattr__(cls_self, attr):
47                if attr.startswith('handle_'):
48                    return lambda *a: setattr(self, 'result', a)
49                raise AttributeError
50        self.parse = cls().parse
51
52    def test_unknown(self):
53        line = 'unknown log line'
54        self.parse(line)
55        self.assertEqual(self.result, (line,))
56
57    def test_open(self):
58        self.assertRaises(svn_server_log_parse.Error, self.parse, 'open')
59        self.assertRaises(svn_server_log_parse.Error, self.parse, 'open 2 cap / SVN/1.60. fooclient')
60        self.assertRaises(svn_server_log_parse.Error, self.parse, 'open a cap=() / SVN/1.60. fooclient')
61        self.assertEqual(self.parse('open 2 cap=() / SVN fooclient'), '')
62        self.assertEqual(self.result, (2, [], '/', 'SVN', 'fooclient'))
63        # TODO: Teach it about the capabilities, rather than allowing
64        # any words at all.
65        self.assertEqual(self.parse('open 2 cap=(foo) / SVN foo%20client'), '')
66        self.assertEqual(self.result, (2, ['foo'], '/', 'SVN', 'foo client'))
67
68    def test_reparent(self):
69        self.assertRaises(svn_server_log_parse.Error, self.parse, 'reparent')
70        self.assertEqual(self.parse('reparent /'), '')
71        self.assertEqual(self.result, ('/',))
72
73    def test_get_latest_rev(self):
74        self.assertEqual(self.parse('get-latest-rev'), '')
75        self.assertEqual(self.result, ())
76        self.assertEqual(self.parse('get-latest-rev r3'), 'r3')
77        self.assertEqual(self.result, ())
78
79    def test_get_dated_rev(self):
80        self.assertRaises(svn_server_log_parse.Error, self.parse,
81                          'get-dated-rev')
82        self.assertEqual(self.parse('get-dated-rev 2008-04-15T20:41:24.000000Z'), '')
83        self.assertEqual(self.result, ('2008-04-15T20:41:24.000000Z',))
84
85    def test_commit(self):
86        self.assertRaises(svn_server_log_parse.Error, self.parse, 'commit')
87        self.assertRaises(svn_server_log_parse.Error, self.parse, 'commit 3')
88        self.assertEqual(self.parse('commit r3'), '')
89        self.assertEqual(self.result, (3,))
90        self.assertEqual(self.parse('commit r3 leftover'), ' leftover')
91        self.assertEqual(self.result, (3,))
92
93    def test_get_dir(self):
94        self.get_dir_or_file('get-dir')
95
96    def test_get_file(self):
97        self.get_dir_or_file('get-file')
98
99    def get_dir_or_file(self, c):
100        self.assertRaises(svn_server_log_parse.Error, self.parse, c)
101        self.assertRaises(svn_server_log_parse.Error, self.parse, c + ' foo')
102        self.assertRaises(svn_server_log_parse.Error, self.parse, c + ' foo 3')
103        self.assertEqual(self.parse(c + ' /a/b/c r3 ...'), ' ...')
104        self.assertEqual(self.result, ('/a/b/c', 3, False, False))
105        self.assertEqual(self.parse(c + ' / r3'), '')
106        self.assertEqual(self.result, ('/', 3, False, False))
107        # path must be absolute
108        self.assertRaises(svn_server_log_parse.Error,
109                          self.parse, c + ' a/b/c r3')
110        self.assertEqual(self.parse(c + ' /k r27 text'), '')
111        self.assertEqual(self.result, ('/k', 27, True, False))
112        self.assertEqual(self.parse(c + ' /k r27 props'), '')
113        self.assertEqual(self.result, ('/k', 27, False, True))
114        self.assertEqual(self.parse(c + ' /k r27 text props'), '')
115        self.assertEqual(self.result, ('/k', 27, True, True))
116        # out of order not accepted
117        self.assertEqual(self.parse(c + ' /k r27 props text'), ' text')
118        self.assertEqual(self.result, ('/k', 27, False, True))
119
120    def test_lock(self):
121        self.assertRaises(svn_server_log_parse.Error, self.parse, 'lock')
122        self.parse('lock (/foo)')
123        self.assertEqual(self.result, (['/foo'], False))
124        self.assertEqual(self.parse('lock (/foo) steal ...'), ' ...')
125        self.assertEqual(self.result, (['/foo'], True))
126        self.assertEqual(self.parse('lock (/foo) stear'), ' stear')
127
128    def test_change_rev_prop(self):
129        self.assertRaises(svn_server_log_parse.Error,
130                          self.parse, 'change-rev-prop r3')
131        self.assertRaises(svn_server_log_parse.Error,
132                          self.parse, 'change-rev-prop r svn:log')
133        self.assertRaises(svn_server_log_parse.Error,
134                          self.parse, 'change-rev-prop rX svn:log')
135        self.assertEqual(self.parse('change-rev-prop r3 svn:log ...'), ' ...')
136        self.assertEqual(self.result, (3, 'svn:log'))
137
138    def test_rev_proplist(self):
139        self.assertRaises(svn_server_log_parse.Error,
140                          self.parse, 'rev-proplist')
141        self.assertRaises(svn_server_log_parse.Error,
142                          self.parse, 'rev-proplist r')
143        self.assertRaises(svn_server_log_parse.Error,
144                          self.parse, 'rev-proplist rX')
145        self.assertEqual(self.parse('rev-proplist r3 ...'), ' ...')
146        self.assertEqual(self.result, (3,))
147
148    def test_rev_prop(self):
149        self.assertRaises(svn_server_log_parse.Error, self.parse, 'rev-prop')
150        self.assertRaises(svn_server_log_parse.Error, self.parse, 'rev-prop r')
151        self.assertRaises(svn_server_log_parse.Error, self.parse, 'rev-prop rX')
152        self.assertEqual(self.parse('rev-prop r3 foo ...'), ' ...')
153        self.assertEqual(self.result, (3, 'foo'))
154
155    def test_unlock(self):
156        self.assertRaises(svn_server_log_parse.Error, self.parse, 'unlock')
157        self.parse('unlock (/foo)')
158        self.assertEqual(self.result, (['/foo'], False))
159        self.assertEqual(self.parse('unlock (/foo) break ...'), ' ...')
160        self.assertEqual(self.result, (['/foo'], True))
161        self.assertEqual(self.parse('unlock (/foo) bear'), ' bear')
162
163    def test_get_lock(self):
164        self.assertRaises(svn_server_log_parse.Error, self.parse, 'get-lock')
165        self.parse('get-lock /foo')
166        self.assertEqual(self.result, ('/foo',))
167
168    def test_get_locks(self):
169        self.assertRaises(svn_server_log_parse.Error, self.parse, 'get-locks')
170        self.parse('get-locks /foo')
171        self.assertEqual(self.result, ('/foo',))
172
173    def test_get_locations(self):
174        self.assertRaises(svn_server_log_parse.Error, self.parse,
175                          'get-locations')
176        self.assertRaises(svn_server_log_parse.Error,
177                          self.parse, 'get-locations /foo 3')
178        self.assertEqual(self.parse('get-locations /foo (3 4) ...'), ' ...')
179        self.assertEqual(self.result, ('/foo', [3, 4]))
180        self.assertEqual(self.parse('get-locations /foo (3)'), '')
181        self.assertEqual(self.result, ('/foo', [3]))
182
183    def test_get_location_segments(self):
184        self.assertRaises(svn_server_log_parse.Error, self.parse,
185                          'get-location-segments')
186        self.assertRaises(svn_server_log_parse.Error,
187                          self.parse, 'get-location-segments /foo 3')
188        self.assertEqual(self.parse('get-location-segments /foo@2 r3:4'), '')
189        self.assertEqual(self.result, ('/foo', 2, 3, 4))
190
191    def test_get_file_revs(self):
192        self.assertRaises(svn_server_log_parse.Error, self.parse, 'get-file-revs')
193        self.assertRaises(svn_server_log_parse.Error,
194                          self.parse, 'get-file-revs /foo 3')
195        self.assertRaises(svn_server_log_parse.Error,
196                          self.parse, 'get-file-revs /foo 3:a')
197        self.assertRaises(svn_server_log_parse.Error,
198                          self.parse, 'get-file-revs /foo r3:a')
199        self.assertEqual(self.parse('get-file-revs /foo r3:4 ...'), ' ...')
200        self.assertEqual(self.result, ('/foo', 3, 4, False))
201        self.assertEqual(self.parse('get-file-revs /foo r3:4'
202                                    ' include-merged-revisions ...'), ' ...')
203        self.assertEqual(self.result, ('/foo', 3, 4, True))
204
205    def test_get_mergeinfo(self):
206        self.assertRaises(svn_server_log_parse.Error,
207                          self.parse, 'get-mergeinfo')
208        self.assertRaises(svn_server_log_parse.Error,
209                          self.parse, 'get-mergeinfo /foo')
210        self.assertRaises(svn_server_log_parse.Error,
211                          self.parse, 'get-mergeinfo (/foo')
212        self.assertRaises(svn_server_log_parse.Error,
213                          self.parse, 'get-mergeinfo (/foo /bar')
214        self.assertRaises(svn_server_log_parse.Error,
215                          self.parse, 'get-mergeinfo (/foo)')
216        self.assertRaises(svn_server_log_parse.BadMergeinfoInheritanceError,
217                          self.parse, 'get-mergeinfo (/foo) bork')
218        self.assertEqual(self.parse('get-mergeinfo (/foo) explicit'), '')
219        self.assertEqual(self.result, (['/foo'],
220                                       svn.core.svn_mergeinfo_explicit, False))
221        self.assertEqual(self.parse('get-mergeinfo (/foo /bar) inherited ...'),
222                         ' ...')
223        self.assertEqual(self.result, (['/foo', '/bar'],
224                                       svn.core.svn_mergeinfo_inherited, False))
225        self.assertEqual(self.result, (['/foo', '/bar'],
226                                       svn.core.svn_mergeinfo_inherited, False))
227
228    def test_log(self):
229        self.assertRaises(svn_server_log_parse.Error, self.parse, 'log')
230        self.assertRaises(svn_server_log_parse.Error,
231                          self.parse, 'log /foo')
232        self.assertRaises(svn_server_log_parse.Error,
233                          self.parse, 'log (/foo)')
234        self.assertEqual(self.parse('log (/foo) r3:4'
235                                    ' include-merged-revisions'), '')
236        self.assertEqual(self.result,
237                         (['/foo'], 3, 4, 0, False, False, True, []))
238        self.assertEqual(self.parse('log (/foo /bar) r3:4 revprops=all ...'),
239                         ' ...')
240        self.assertEqual(self.result,
241                         (['/foo', '/bar'], 3, 4, 0, False, False, False, None))
242        self.assertEqual(self.parse('log (/foo) r3:4 revprops=(a b) ...'),
243                         ' ...')
244        self.assertEqual(self.result,
245                         (['/foo'], 3, 4, 0, False, False, False, ['a', 'b']))
246        self.assertEqual(self.parse('log (/foo) r8:1 limit=3'), '')
247        self.assertEqual(self.result,
248                         (['/foo'], 8, 1, 3, False, False, False, []))
249
250    def test_check_path(self):
251        self.assertRaises(svn_server_log_parse.Error, self.parse, 'check-path')
252        self.assertEqual(self.parse('check-path /foo@9'), '')
253        self.assertEqual(self.result, ('/foo', 9))
254
255    def test_stat(self):
256        self.assertRaises(svn_server_log_parse.Error, self.parse, 'stat')
257        self.assertEqual(self.parse('stat /foo@9'), '')
258        self.assertEqual(self.result, ('/foo', 9))
259
260    def test_replay(self):
261        self.assertRaises(svn_server_log_parse.Error, self.parse, 'replay')
262        self.assertRaises(svn_server_log_parse.Error,
263                          self.parse, 'replay /foo')
264        self.assertRaises(svn_server_log_parse.Error,
265                          self.parse, 'replay (/foo) r9')
266        self.assertRaises(svn_server_log_parse.Error,
267                          self.parse, 'replay (/foo) r9:10')
268        self.assertEqual(self.parse('replay /foo r9'), '')
269        self.assertEqual(self.result, ('/foo', 9))
270
271    def test_checkout_or_export(self):
272        self.assertRaises(svn_server_log_parse.Error,
273                          self.parse, 'checkout-or-export')
274        self.assertRaises(svn_server_log_parse.Error,
275                          self.parse, 'checkout-or-export /foo')
276        self.assertEqual(self.parse('checkout-or-export /foo r9'), '')
277        self.assertEqual(self.result, ('/foo', 9, svn.core.svn_depth_unknown))
278        self.assertRaises(svn_server_log_parse.BadDepthError, self.parse,
279                          'checkout-or-export /foo r9 depth=INVALID-DEPTH')
280        self.assertRaises(svn_server_log_parse.BadDepthError, self.parse,
281                          'checkout-or-export /foo r9 depth=bork')
282        self.assertEqual(self.parse('checkout-or-export /foo r9 depth=files .'),
283                         ' .')
284        self.assertEqual(self.result, ('/foo', 9, svn.core.svn_depth_files))
285
286    def test_diff_1path(self):
287        self.assertRaises(svn_server_log_parse.Error,
288                          self.parse, 'diff')
289        self.assertEqual(self.parse('diff /foo r9:10'), '')
290        self.assertEqual(self.result, ('/foo', 9, 10,
291                                       svn.core.svn_depth_unknown, False))
292        self.assertEqual(self.parse('diff /foo r9:10'
293                                    ' ignore-ancestry ...'), ' ...')
294        self.assertEqual(self.result, ('/foo', 9, 10,
295                                       svn.core.svn_depth_unknown, True))
296        self.assertEqual(self.parse('diff /foo r9:10 depth=files'), '')
297        self.assertEqual(self.result, ('/foo', 9, 10,
298                                       svn.core.svn_depth_files, False))
299
300    def test_diff_2paths(self):
301        self.assertEqual(self.parse('diff /foo@9 /bar@10'), '')
302        self.assertEqual(self.result, ('/foo', 9, '/bar', 10,
303                                       svn.core.svn_depth_unknown, False))
304        self.assertEqual(self.parse('diff /foo@9 /bar@10'
305                                    ' ignore-ancestry ...'), ' ...')
306        self.assertEqual(self.result, ('/foo', 9, '/bar', 10,
307                                       svn.core.svn_depth_unknown, True))
308        self.assertEqual(self.parse('diff /foo@9 /bar@10'
309                                    ' depth=files ignore-ancestry'), '')
310        self.assertEqual(self.result, ('/foo', 9, '/bar', 10,
311                                       svn.core.svn_depth_files, True))
312
313    def test_status(self):
314        self.assertRaises(svn_server_log_parse.Error,
315                          self.parse, 'status')
316        self.assertRaises(svn_server_log_parse.Error,
317                          self.parse, 'status /foo')
318        self.assertEqual(self.parse('status /foo r9'), '')
319        self.assertEqual(self.result, ('/foo', 9, svn.core.svn_depth_unknown))
320        self.assertRaises(svn_server_log_parse.BadDepthError, self.parse,
321                          'status /foo r9 depth=INVALID-DEPTH')
322        self.assertRaises(svn_server_log_parse.BadDepthError, self.parse,
323                          'status /foo r9 depth=bork')
324        self.assertEqual(self.parse('status /foo r9 depth=files .'),
325                         ' .')
326        self.assertEqual(self.result, ('/foo', 9, svn.core.svn_depth_files))
327
328    def test_switch(self):
329        self.assertEqual(self.parse('switch /foo /bar@10 ...'), ' ...')
330        self.assertEqual(self.result, ('/foo', '/bar', 10,
331                                       svn.core.svn_depth_unknown))
332        self.assertEqual(self.parse('switch /foo /bar@10'
333                                    ' depth=files'), '')
334        self.assertEqual(self.result, ('/foo', '/bar', 10,
335                                       svn.core.svn_depth_files))
336
337    def test_update(self):
338        self.assertRaises(svn_server_log_parse.Error,
339                          self.parse, 'update')
340        self.assertRaises(svn_server_log_parse.Error,
341                          self.parse, 'update /foo')
342        self.assertEqual(self.parse('update /foo r9'), '')
343        self.assertEqual(self.result, ('/foo', 9, svn.core.svn_depth_unknown,
344                                       False))
345        self.assertRaises(svn_server_log_parse.BadDepthError, self.parse,
346                          'update /foo r9 depth=INVALID-DEPTH')
347        self.assertRaises(svn_server_log_parse.BadDepthError, self.parse,
348                          'update /foo r9 depth=bork')
349        self.assertEqual(self.parse('update /foo r9 depth=files .'), ' .')
350        self.assertEqual(self.result, ('/foo', 9, svn.core.svn_depth_files,
351                                       False))
352        self.assertEqual(self.parse('update /foo r9 send-copyfrom-args .'),
353                         ' .')
354        self.assertEqual(self.result, ('/foo', 9, svn.core.svn_depth_unknown,
355                                       True))
356
357if __name__ == '__main__':
358    if len(sys.argv) == 1:
359        # No arguments so run the unit tests.
360        unittest.main()
361        sys.stderr.write('unittest.main failed to exit\n')
362        sys.exit(2)
363
364    # Use the argument as the path to a log file to test against.
365
366    def uri_encode(s):
367        # urllib.parse.quote encodes :&@ characters, svn does not.
368        return urllib_parse_quote(s, safe='/:&@')
369
370    # Define a class to reconstruct the SVN-ACTION string.
371    class Test(svn_server_log_parse.Parser):
372        def handle_unknown(self, line):
373            sys.stderr.write('unknown log line at %d:\n%s\n' % (self.linenum,
374                                                                line))
375            sys.exit(2)
376
377        def handle_open(self, protocol, capabilities, path, ra_client, client):
378            capabilities = ' '.join(capabilities)
379            if ra_client is None:
380                ra_client = '-'
381            if client is None:
382                client = '-'
383            path = uri_encode(path)
384            self.action = ('open %d cap=(%s) %s %s %s'
385                           % (protocol, capabilities, path, ra_client, client))
386
387        def handle_reparent(self, path):
388            path = uri_encode(path)
389            self.action = 'reparent ' + path
390
391        def handle_get_latest_rev(self):
392            self.action = 'get-latest-rev'
393
394        def handle_get_dated_rev(self, date):
395            self.action = 'get-dated-rev ' + date
396
397        def handle_commit(self, revision):
398            self.action = 'commit r%d' % (revision,)
399
400        def handle_get_dir(self, path, revision, text, props):
401            path = uri_encode(path)
402            self.action = 'get-dir %s r%d' % (path, revision)
403            if text:
404                self.action += ' text'
405            if props:
406                self.action += ' props'
407
408        def handle_get_file(self, path, revision, text, props):
409            path = uri_encode(path)
410            self.action = 'get-file %s r%d' % (path, revision)
411            if text:
412                self.action += ' text'
413            if props:
414                self.action += ' props'
415
416        def handle_lock(self, paths, steal):
417            paths = [uri_encode(x) for x in paths]
418            self.action = 'lock (%s)' % (' '.join(paths),)
419            if steal:
420                self.action += ' steal'
421
422        def handle_change_rev_prop(self, revision, revprop):
423            revprop = uri_encode(revprop)
424            self.action = 'change-rev-prop r%d %s' % (revision, revprop)
425
426        def handle_rev_prop(self, revision, revprop):
427            revprop = uri_encode(revprop)
428            self.action = 'rev-prop r%d %s' % (revision, revprop)
429
430        def handle_rev_proplist(self, revision):
431            self.action = 'rev-proplist r%d' % (revision,)
432
433        def handle_unlock(self, paths, break_lock):
434            paths = [uri_encode(x) for x in paths]
435            self.action = 'unlock (%s)' % (' '.join(paths),)
436            if break_lock:
437                self.action += ' break'
438
439        def handle_get_lock(self, path):
440            path = uri_encode(path)
441            self.action = 'get-lock ' + path
442
443        def handle_get_locks(self, path):
444            self.action = 'get-locks ' + path
445            path = uri_encode(path)
446
447        def handle_get_locations(self, path, revisions):
448            path = uri_encode(path)
449            self.action = ('get-locations %s (%s)'
450                           % (path, ' '.join([str(x) for x in revisions])))
451
452        def handle_get_location_segments(self, path, peg, left, right):
453            path = uri_encode(path)
454            self.action = 'get-location-segments %s@%d r%d:%d' % (path, peg,
455                                                                  left, right)
456
457        def handle_get_file_revs(self, path, left, right,
458                                 include_merged_revisions):
459            path = uri_encode(path)
460            self.action = 'get-file-revs %s r%d:%d' % (path, left, right)
461            if include_merged_revisions:
462                self.action += ' include-merged-revisions'
463
464        def handle_get_mergeinfo(self, paths, inheritance, include_descendants):
465            paths = [uri_encode(x) for x in paths]
466            self.action = ('get-mergeinfo (%s) %s'
467                           % (' '.join(paths),
468                              svn.core.svn_inheritance_to_word(inheritance)))
469            if include_descendants:
470                self.action += ' include-descendants'
471
472        def handle_log(self, paths, left, right, limit, discover_changed_paths,
473                       strict, include_merged_revisions, revprops):
474            paths = [uri_encode(x) for x in paths]
475            self.action = 'log (%s) r%d:%d' % (' '.join(paths),
476                                               left, right)
477            if limit != 0:
478                self.action += ' limit=%d' % (limit,)
479            if discover_changed_paths:
480                self.action += ' discover-changed-paths'
481            if strict:
482                self.action += ' strict'
483            if include_merged_revisions:
484                self.action += ' include-merged-revisions'
485            if revprops is None:
486                self.action += ' revprops=all'
487            elif len(revprops) > 0:
488                revprops = [uri_encode(x) for x in revprops]
489                self.action += ' revprops=(%s)' % (' '.join(revprops),)
490
491        def handle_check_path(self, path, revision):
492            path = uri_encode(path)
493            self.action = 'check-path %s@%d' % (path, revision)
494
495        def handle_stat(self, path, revision):
496            path = uri_encode(path)
497            self.action = 'stat %s@%d' % (path, revision)
498
499        def handle_replay(self, path, revision):
500            path = uri_encode(path)
501            self.action = 'replay %s r%d' % (path, revision)
502
503        def maybe_depth(self, depth):
504            if depth != svn.core.svn_depth_unknown:
505                self.action += ' depth=%s' % (
506                    svn.core.svn_depth_to_word(depth),)
507
508        def handle_checkout_or_export(self, path, revision, depth):
509            path = uri_encode(path)
510            self.action = 'checkout-or-export %s r%d' % (path, revision)
511            self.maybe_depth(depth)
512
513        def handle_diff_1path(self, path, left, right,
514                                       depth, ignore_ancestry):
515            path = uri_encode(path)
516            self.action = 'diff %s r%d:%d' % (path, left, right)
517            self.maybe_depth(depth)
518            if ignore_ancestry:
519                self.action += ' ignore-ancestry'
520
521        def handle_diff_2paths(self, from_path, from_rev,
522                                        to_path, to_rev,
523                                        depth, ignore_ancestry):
524            from_path = uri_encode(from_path)
525            to_path = uri_encode(to_path)
526            self.action = ('diff %s@%d %s@%d'
527                           % (from_path, from_rev, to_path, to_rev))
528            self.maybe_depth(depth)
529            if ignore_ancestry:
530                self.action += ' ignore-ancestry'
531
532        def handle_status(self, path, revision, depth):
533            path = uri_encode(path)
534            self.action = 'status %s r%d' % (path, revision)
535            self.maybe_depth(depth)
536
537        def handle_switch(self, from_path, to_path, to_rev, depth):
538            from_path = uri_encode(from_path)
539            to_path = uri_encode(to_path)
540            self.action = ('switch %s %s@%d'
541                           % (from_path, to_path, to_rev))
542            self.maybe_depth(depth)
543
544        def handle_update(self, path, revision, depth, send_copyfrom_args):
545            path = uri_encode(path)
546            self.action = 'update %s r%d' % (path, revision)
547            self.maybe_depth(depth)
548            if send_copyfrom_args:
549                self.action += ' send-copyfrom-args'
550
551    tmp = tempfile.mktemp()
552    try:
553        fp = open(tmp, 'w')
554        parser = Test()
555        parser.linenum = 0
556        log_file = sys.argv[1]
557        log_type = None
558        for line in open(log_file):
559            if log_type is None:
560                # Figure out which log type we have.
561                if re.match(r'\d+ \d\d\d\d-', line):
562                    log_type = 'svnserve'
563                elif re.match(r'\[\d\d/', line):
564                    log_type = 'mod_dav_svn'
565                else:
566                    sys.stderr.write("unknown log format in '%s'"
567                                     % (log_file,))
568                    sys.exit(3)
569                sys.stderr.write('parsing %s log...\n' % (log_type,))
570                sys.stderr.flush()
571
572            words = line.split()
573            if log_type == 'svnserve':
574                # Skip over PID, date, client address, username, and repos.
575                if words[5].startswith('ERR'):
576                    # Skip error lines.
577                    fp.write(line)
578                    continue
579                leading = ' '.join(words[:5])
580                action = ' '.join(words[5:])
581            else:
582                # Find the SVN-ACTION string from the CustomLog format
583                # davautocheck.sh uses.  If that changes, this will need
584                # to as well.  Currently it's
585                #   %t %u %{SVN-REPOS-NAME}e %{SVN-ACTION}e
586                leading = ' '.join(words[:4])
587                action = ' '.join(words[4:])
588
589            # Parse the action and write the reconstructed action to
590            # the temporary file.  Ignore the returned trailing text,
591            # as we have none in the davautocheck ops log.
592            parser.linenum += 1
593            try:
594                parser.parse(action)
595            except svn_server_log_parse.Error:
596                sys.stderr.write('error at line %d: %s\n'
597                                 % (parser.linenum, action))
598                raise
599            fp.write(leading + ' ' + parser.action + '\n')
600        fp.close()
601        # Check differences between original and reconstructed files
602        # (should be identical).
603        result = os.spawnlp(os.P_WAIT, 'diff', 'diff', '-u', log_file, tmp)
604        if result == 0:
605            sys.stderr.write('OK\n')
606        sys.exit(result)
607    finally:
608        try:
609            os.unlink(tmp)
610        except Exception as e:
611            sys.stderr.write('os.unlink(tmp): %s\n' % (e,))
612