2 # verify.py: routines that handle comparison and display of expected
5 # Subversion is a tool for revision control.
6 # See http://subversion.tigris.org for more information.
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
17 # http://www.apache.org/licenses/LICENSE-2.0
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
25 ######################################################################
28 from difflib
import unified_diff
, ndiff
34 logger
= logging
.getLogger()
37 ######################################################################
40 class SVNUnexpectedOutput(svntest
.Failure
):
41 """Exception raised if an invocation of svn results in unexpected
42 output of any kind."""
45 class SVNUnexpectedStdout(SVNUnexpectedOutput
):
46 """Exception raised if an invocation of svn results in unexpected
50 class SVNUnexpectedStderr(SVNUnexpectedOutput
):
51 """Exception raised if an invocation of svn results in unexpected
55 class SVNExpectedStdout(SVNUnexpectedOutput
):
56 """Exception raised if an invocation of svn results in no output on
57 STDOUT when output was expected."""
60 class SVNExpectedStderr(SVNUnexpectedOutput
):
61 """Exception raised if an invocation of svn results in no output on
62 STDERR when output was expected."""
65 class SVNUnexpectedExitCode(SVNUnexpectedOutput
):
66 """Exception raised if an invocation of svn exits with a value other
67 than what was expected."""
70 class SVNIncorrectDatatype(SVNUnexpectedOutput
):
71 """Exception raised if invalid input is passed to the
72 run_and_verify_* API"""
75 class SVNDumpParseError(svntest
.Failure
):
76 """Exception raised if parsing a dump file fails"""
80 ######################################################################
81 # Comparison of expected vs. actual output
83 def createExpectedOutput(expected
, output_type
, match_all
=True):
84 """Return EXPECTED, promoted to an ExpectedOutput instance if not
85 None. Raise SVNIncorrectDatatype if the data type of EXPECTED is
87 if isinstance(expected
, list):
88 expected
= ExpectedOutput(expected
)
89 elif isinstance(expected
, str):
90 expected
= RegexOutput(expected
, match_all
)
91 elif isinstance(expected
, int):
92 expected
= RegexOutput(".*: E%d:.*" % expected
, False)
93 elif expected
is AnyOutput
:
94 expected
= AnyOutput()
95 elif expected
is not None and not isinstance(expected
, ExpectedOutput
):
96 raise SVNIncorrectDatatype("Unexpected type for '%s' data" % output_type
)
100 """Contains expected output, and performs comparisons."""
105 def __init__(self
, output
, match_all
=True):
106 """Initialize the expected output to OUTPUT which is a string, or a list
107 of strings, or None meaning an empty list. If MATCH_ALL is True, the
108 expected strings will be matched with the actual strings, one-to-one, in
109 the same order. If False, they will be matched with a subset of the
110 actual strings, one-to-one, in the same order, ignoring any other actual
111 strings among the matching ones."""
113 self
.match_all
= match_all
116 return str(self
.output
)
118 def __cmp__(self
, other
):
119 raise Exception('badness')
121 def matches(self
, other
, except_re
=None):
122 """Return whether SELF.output matches OTHER (which may be a list
123 of newline-terminated lines, or a single string). Either value
125 if self
.output
is None:
128 expected
= self
.output
134 if not isinstance(actual
, list):
136 if not isinstance(expected
, list):
137 expected
= [expected
]
140 return self
.matches_except(expected
, actual
, except_re
)
142 return self
.is_equivalent_list(expected
, actual
)
144 def matches_except(self
, expected
, actual
, except_re
):
145 "Return whether EXPECTED and ACTUAL match except for except_re."
146 if not self
.is_regex
:
149 while i_expected
< len(expected
) and i_actual
< len(actual
):
150 if re
.match(except_re
, actual
[i_actual
]):
152 elif re
.match(except_re
, expected
[i_expected
]):
154 elif expected
[i_expected
] == actual
[i_actual
]:
159 if i_expected
== len(expected
) and i_actual
== len(actual
):
163 raise Exception("is_regex and except_re are mutually exclusive")
165 def is_equivalent_list(self
, expected
, actual
):
166 "Return whether EXPECTED and ACTUAL are equivalent."
167 if not self
.is_regex
:
169 # The EXPECTED lines must match the ACTUAL lines, one-to-one, in
171 return expected
== actual
173 # The EXPECTED lines must match a subset of the ACTUAL lines,
174 # one-to-one, in the same order, with zero or more other ACTUAL
175 # lines interspersed among the matching ACTUAL lines.
177 for actual_line
in actual
:
178 if expected
[i_expected
] == actual_line
:
180 if i_expected
== len(expected
):
184 expected_re
= expected
[0]
185 # If we want to check that every line matches the regexp
186 # assume they all match and look for any that don't. If
187 # only one line matching the regexp is enough, assume none
188 # match and look for even one that does.
190 all_lines_match_re
= True
192 all_lines_match_re
= False
194 # If a regex was provided assume that we actually require
195 # some output. Fail if we don't have any.
199 for actual_line
in actual
:
201 if not re
.match(expected_re
, actual_line
):
204 # As soon an actual_line matches something, then we're good.
205 if re
.match(expected_re
, actual_line
):
208 return all_lines_match_re
210 def display_differences(self
, message
, label
, actual
):
211 """Delegate to the display_lines() routine with the appropriate
212 args. MESSAGE is ignored if None."""
213 display_lines(message
, label
, self
.output
, actual
,
214 self
.is_regex
, self
.is_unordered
)
217 class AnyOutput(ExpectedOutput
):
219 ExpectedOutput
.__init
__(self
, None, False)
221 def is_equivalent_list(self
, ignored
, actual
):
223 # No actual output. No match.
227 # If any line has some text, then there is output, so we match.
231 # We did not find a line with text. No match.
234 def display_differences(self
, message
, label
, actual
):
239 class RegexOutput(ExpectedOutput
):
243 class UnorderedOutput(ExpectedOutput
):
244 """Marks unordered output, and performs comparisons."""
248 def __cmp__(self
, other
):
249 raise Exception('badness')
251 def matches_except(self
, expected
, actual
, except_re
):
252 assert type(actual
) == type([]) # ### if this trips: fix it!
253 return self
.is_equivalent_list([l
for l
in expected
if not except_re
.match(l
)],
254 [l
for l
in actual
if not except_re
.match(l
)])
256 def is_equivalent_list(self
, expected
, actual
):
257 "Disregard the order of ACTUAL lines during comparison."
259 e_set
= set(expected
)
263 if len(e_set
) != len(a_set
):
266 for expect_re
in e_set
:
267 for actual_line
in a_set
:
268 if re
.match(expect_re
, actual_line
):
269 a_set
.remove(actual_line
)
272 # One of the regexes was not found
276 # All expected lines must be in the output.
277 return e_set
== a_set
280 # If any of the expected regexes are in the output, then we match.
281 for expect_re
in e_set
:
282 for actual_line
in a_set
:
283 if re
.match(expect_re
, actual_line
):
287 # If any of the expected lines are in the output, then we match.
288 return len(e_set
.intersection(a_set
)) > 0
291 class UnorderedRegexOutput(UnorderedOutput
, RegexOutput
):
296 ######################################################################
297 # Displaying expected and actual output
299 def display_trees(message
, label
, expected
, actual
):
300 'Print two trees, expected and actual.'
301 if message
is not None:
303 if expected
is not None:
304 logger
.warn('EXPECTED %s:', label
)
305 svntest
.tree
.dump_tree(expected
)
306 if actual
is not None:
307 logger
.warn('ACTUAL %s:', label
)
308 svntest
.tree
.dump_tree(actual
)
311 def display_lines(message
, label
, expected
, actual
, expected_is_regexp
=None,
312 expected_is_unordered
=None):
313 """Print MESSAGE, unless it is None, then print EXPECTED (labeled
314 with LABEL) followed by ACTUAL (also labeled with LABEL).
315 Both EXPECTED and ACTUAL may be strings or lists of strings."""
316 if message
is not None:
318 if expected
is not None:
319 output
= 'EXPECTED %s' % label
320 if expected_is_regexp
:
321 output
+= ' (regexp)'
322 expected
= [expected
+ '\n']
323 if expected_is_unordered
:
324 output
+= ' (unordered)'
329 if actual
is not None:
330 logger
.warn('ACTUAL %s:', label
)
334 # Additionally print unified diff
335 if not expected_is_regexp
:
336 logger
.warn('DIFF ' + ' '.join(output
.split(' ')[1:]))
338 if type(expected
) is str:
339 expected
= [expected
]
341 if type(actual
) is str:
344 for x
in unified_diff(expected
, actual
,
345 fromfile
="EXPECTED %s" % label
,
346 tofile
="ACTUAL %s" % label
):
349 def compare_and_display_lines(message
, label
, expected
, actual
,
350 raisable
=None, except_re
=None):
351 """Compare two sets of output lines, and print them if they differ,
352 preceded by MESSAGE iff not None. EXPECTED may be an instance of
353 ExpectedOutput (and if not, it is wrapped as such). RAISABLE is an
354 exception class, an instance of which is thrown if ACTUAL doesn't
357 raisable
= svntest
.main
.SVNLineUnequal
358 ### It'd be nicer to use createExpectedOutput() here, but its
359 ### semantics don't match all current consumers of this function.
360 if not isinstance(expected
, ExpectedOutput
):
361 expected
= ExpectedOutput(expected
)
363 if isinstance(actual
, str):
365 actual
= [line
for line
in actual
if not line
.startswith('DBG:')]
367 if not expected
.matches(actual
, except_re
):
368 expected
.display_differences(message
, label
, actual
)
371 def verify_outputs(message
, actual_stdout
, actual_stderr
,
372 expected_stdout
, expected_stderr
, all_stdout
=True):
373 """Compare and display expected vs. actual stderr and stdout lines:
374 if they don't match, print the difference (preceded by MESSAGE iff
375 not None) and raise an exception.
377 If EXPECTED_STDERR or EXPECTED_STDOUT is a string the string is
378 interpreted as a regular expression. For EXPECTED_STDOUT and
379 ACTUAL_STDOUT to match, every line in ACTUAL_STDOUT must match the
380 EXPECTED_STDOUT regex, unless ALL_STDOUT is false. For
381 EXPECTED_STDERR regexes only one line in ACTUAL_STDERR need match."""
382 expected_stderr
= createExpectedOutput(expected_stderr
, 'stderr', False)
383 expected_stdout
= createExpectedOutput(expected_stdout
, 'stdout', all_stdout
)
385 for (actual
, expected
, label
, raisable
) in (
386 (actual_stderr
, expected_stderr
, 'STDERR', SVNExpectedStderr
),
387 (actual_stdout
, expected_stdout
, 'STDOUT', SVNExpectedStdout
)):
391 if isinstance(expected
, RegexOutput
):
392 raisable
= svntest
.main
.SVNUnmatchedError
393 elif not isinstance(expected
, AnyOutput
):
394 raisable
= svntest
.main
.SVNLineUnequal
396 compare_and_display_lines(message
, label
, expected
, actual
, raisable
)
398 def verify_exit_code(message
, actual
, expected
,
399 raisable
=SVNUnexpectedExitCode
):
400 """Compare and display expected vs. actual exit codes:
401 if they don't match, print the difference (preceded by MESSAGE iff
402 not None) and raise an exception."""
404 if expected
!= actual
:
405 display_lines(message
, "Exit Code",
406 str(expected
) + '\n', str(actual
) + '\n')
409 # A simple dump file parser. While sufficient for the current
410 # testsuite it doesn't cope with all valid dump files.
412 def __init__(self
, lines
):
417 def parse_line(self
, regex
, required
=True):
418 m
= re
.match(regex
, self
.lines
[self
.current
])
421 raise SVNDumpParseError("expected '%s' at line %d\n%s"
422 % (regex
, self
.current
,
423 self
.lines
[self
.current
]))
429 def parse_blank(self
, required
=True):
430 if self
.lines
[self
.current
] != '\n': # Works on Windows
432 raise SVNDumpParseError("expected blank at line %d\n%s"
433 % (self
.current
, self
.lines
[self
.current
]))
439 def parse_format(self
):
440 return self
.parse_line('SVN-fs-dump-format-version: ([0-9]+)$')
442 def parse_uuid(self
):
443 return self
.parse_line('UUID: ([0-9a-z-]+)$')
445 def parse_revision(self
):
446 return self
.parse_line('Revision-number: ([0-9]+)$')
448 def parse_prop_length(self
, required
=True):
449 return self
.parse_line('Prop-content-length: ([0-9]+)$', required
)
451 def parse_content_length(self
, required
=True):
452 return self
.parse_line('Content-length: ([0-9]+)$', required
)
454 def parse_path(self
):
455 path
= self
.parse_line('Node-path: (.+)$', required
=False)
456 if not path
and self
.lines
[self
.current
] == 'Node-path: \n':
461 def parse_kind(self
):
462 return self
.parse_line('Node-kind: (.+)$', required
=False)
464 def parse_action(self
):
465 return self
.parse_line('Node-action: ([0-9a-z-]+)$')
467 def parse_copyfrom_rev(self
):
468 return self
.parse_line('Node-copyfrom-rev: ([0-9]+)$', required
=False)
470 def parse_copyfrom_path(self
):
471 path
= self
.parse_line('Node-copyfrom-path: (.+)$', required
=False)
472 if not path
and self
.lines
[self
.current
] == 'Node-copyfrom-path: \n':
477 def parse_copy_md5(self
):
478 return self
.parse_line('Text-copy-source-md5: ([0-9a-z]+)$', required
=False)
480 def parse_copy_sha1(self
):
481 return self
.parse_line('Text-copy-source-sha1: ([0-9a-z]+)$', required
=False)
483 def parse_text_md5(self
):
484 return self
.parse_line('Text-content-md5: ([0-9a-z]+)$', required
=False)
486 def parse_text_sha1(self
):
487 return self
.parse_line('Text-content-sha1: ([0-9a-z]+)$', required
=False)
489 def parse_text_length(self
):
490 return self
.parse_line('Text-content-length: ([0-9]+)$', required
=False)
492 # One day we may need to parse individual property name/values into a map
495 while not re
.match('PROPS-END$', self
.lines
[self
.current
]):
496 props
.append(self
.lines
[self
.current
])
501 def get_content(self
, length
):
503 while len(content
) < length
:
504 content
+= self
.lines
[self
.current
]
506 if len(content
) == length
+ 1:
507 content
= content
[:-1]
508 elif len(content
) != length
:
509 raise SVNDumpParseError("content length expected %d actual %d at line %d"
510 % (length
, len(content
), self
.current
))
513 def parse_one_node(self
):
515 node
['kind'] = self
.parse_kind()
516 action
= self
.parse_action()
517 node
['copyfrom_rev'] = self
.parse_copyfrom_rev()
518 node
['copyfrom_path'] = self
.parse_copyfrom_path()
519 node
['copy_md5'] = self
.parse_copy_md5()
520 node
['copy_sha1'] = self
.parse_copy_sha1()
521 node
['prop_length'] = self
.parse_prop_length(required
=False)
522 node
['text_length'] = self
.parse_text_length()
523 node
['text_md5'] = self
.parse_text_md5()
524 node
['text_sha1'] = self
.parse_text_sha1()
525 node
['content_length'] = self
.parse_content_length(required
=False)
527 if node
['prop_length']:
528 node
['props'] = self
.get_props()
529 if node
['text_length']:
530 node
['content'] = self
.get_content(int(node
['text_length']))
531 # Hard to determine how may blanks is 'correct' (a delete that is
532 # followed by an add that is a replace and a copy has one fewer
533 # than expected but that can't be predicted until seeing the add)
534 # so allow arbitrary number
536 while self
.current
< len(self
.lines
) and self
.parse_blank(required
=False):
538 node
['blanks'] = blanks
541 def parse_all_nodes(self
):
544 if self
.current
>= len(self
.lines
):
546 path
= self
.parse_path()
547 if not path
and not path
is '':
549 if not nodes
.get(path
):
551 action
, node
= self
.parse_one_node()
552 if nodes
[path
].get(action
):
553 raise SVNDumpParseError("duplicate action '%s' for node '%s' at line %d"
554 % (action
, path
, self
.current
))
555 nodes
[path
][action
] = node
558 def parse_one_revision(self
):
560 number
= self
.parse_revision()
561 revision
['prop_length'] = self
.parse_prop_length()
562 revision
['content_length'] = self
.parse_content_length()
564 revision
['props'] = self
.get_props()
566 revision
['nodes'] = self
.parse_all_nodes()
567 return number
, revision
569 def parse_all_revisions(self
):
570 while self
.current
< len(self
.lines
):
571 number
, revision
= self
.parse_one_revision()
572 if self
.parsed
.get(number
):
573 raise SVNDumpParseError("duplicate revision %d at line %d"
574 % (number
, self
.current
))
575 self
.parsed
[number
] = revision
578 self
.parsed
['format'] = self
.parse_format()
580 self
.parsed
['uuid'] = self
.parse_uuid()
582 self
.parse_all_revisions()
585 def compare_dump_files(message
, label
, expected
, actual
):
586 """Parse two dump files EXPECTED and ACTUAL, both of which are lists
587 of lines as returned by run_and_verify_dump, and check that the same
588 revisions, nodes, properties, etc. are present in both dumps.
591 parsed_expected
= DumpParser(expected
).parse()
592 parsed_actual
= DumpParser(actual
).parse()
594 if parsed_expected
!= parsed_actual
:
595 raise svntest
.Failure('\n' + '\n'.join(ndiff(
596 pprint
.pformat(parsed_expected
).splitlines(),
597 pprint
.pformat(parsed_actual
).splitlines())))