Re-update to rcsparse r2495 to get all the goodness of the new update script.
[cvs2svn.git] / svntest / verify.py
blobdeea7cbd16ac05ec77b7563a40aeec119a5d995e
2 # verify.py: routines that handle comparison and display of expected
3 # vs. actual output
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
24 # under the License.
25 ######################################################################
27 import re, sys
29 import svntest
32 ######################################################################
33 # Exception types
35 class SVNUnexpectedOutput(svntest.Failure):
36 """Exception raised if an invocation of svn results in unexpected
37 output of any kind."""
38 pass
40 class SVNUnexpectedStdout(SVNUnexpectedOutput):
41 """Exception raised if an invocation of svn results in unexpected
42 output on STDOUT."""
43 pass
45 class SVNUnexpectedStderr(SVNUnexpectedOutput):
46 """Exception raised if an invocation of svn results in unexpected
47 output on STDERR."""
48 pass
50 class SVNExpectedStdout(SVNUnexpectedOutput):
51 """Exception raised if an invocation of svn results in no output on
52 STDOUT when output was expected."""
53 pass
55 class SVNExpectedStderr(SVNUnexpectedOutput):
56 """Exception raised if an invocation of svn results in no output on
57 STDERR when output was expected."""
58 pass
60 class SVNUnexpectedExitCode(SVNUnexpectedOutput):
61 """Exception raised if an invocation of svn exits with a value other
62 than what was expected."""
63 pass
65 class SVNIncorrectDatatype(SVNUnexpectedOutput):
66 """Exception raised if invalid input is passed to the
67 run_and_verify_* API"""
68 pass
71 ######################################################################
72 # Comparison of expected vs. actual output
74 def createExpectedOutput(expected, output_type, match_all=True):
75 """Return EXPECTED, promoted to an ExpectedOutput instance if not
76 None. Raise SVNIncorrectDatatype if the data type of EXPECTED is
77 not handled."""
78 if isinstance(expected, list):
79 expected = ExpectedOutput(expected)
80 elif isinstance(expected, str):
81 expected = RegexOutput(expected, match_all)
82 elif expected is AnyOutput:
83 expected = AnyOutput()
84 elif expected is not None and not isinstance(expected, ExpectedOutput):
85 raise SVNIncorrectDatatype("Unexpected type for '%s' data" % output_type)
86 return expected
88 class ExpectedOutput:
89 """Contains expected output, and performs comparisons."""
91 is_regex = False
92 is_unordered = False
94 def __init__(self, output, match_all=True):
95 """Initialize the expected output to OUTPUT which is a string, or a list
96 of strings, or None meaning an empty list. If MATCH_ALL is True, the
97 expected strings will be matched with the actual strings, one-to-one, in
98 the same order. If False, they will be matched with a subset of the
99 actual strings, one-to-one, in the same order, ignoring any other actual
100 strings among the matching ones."""
101 self.output = output
102 self.match_all = match_all
104 def __str__(self):
105 return str(self.output)
107 def __cmp__(self, other):
108 raise 'badness'
110 def matches(self, other):
111 """Return whether SELF.output matches OTHER (which may be a list
112 of newline-terminated lines, or a single string). Either value
113 may be None."""
114 if self.output is None:
115 expected = []
116 else:
117 expected = self.output
118 if other is None:
119 actual = []
120 else:
121 actual = other
123 if not isinstance(actual, list):
124 actual = [actual]
125 if not isinstance(expected, list):
126 expected = [expected]
128 return self.is_equivalent_list(expected, actual)
130 def is_equivalent_list(self, expected, actual):
131 "Return whether EXPECTED and ACTUAL are equivalent."
132 if not self.is_regex:
133 if self.match_all:
134 # The EXPECTED lines must match the ACTUAL lines, one-to-one, in
135 # the same order.
136 return expected == actual
138 # The EXPECTED lines must match a subset of the ACTUAL lines,
139 # one-to-one, in the same order, with zero or more other ACTUAL
140 # lines interspersed among the matching ACTUAL lines.
141 i_expected = 0
142 for actual_line in actual:
143 if expected[i_expected] == actual_line:
144 i_expected += 1
145 if i_expected == len(expected):
146 return True
147 return False
149 expected_re = expected[0]
150 # If we want to check that every line matches the regexp
151 # assume they all match and look for any that don't. If
152 # only one line matching the regexp is enough, assume none
153 # match and look for even one that does.
154 if self.match_all:
155 all_lines_match_re = True
156 else:
157 all_lines_match_re = False
159 # If a regex was provided assume that we actually require
160 # some output. Fail if we don't have any.
161 if len(actual) == 0:
162 return False
164 for actual_line in actual:
165 if self.match_all:
166 if not re.match(expected_re, actual_line):
167 return False
168 else:
169 # As soon an actual_line matches something, then we're good.
170 if re.match(expected_re, actual_line):
171 return True
173 return all_lines_match_re
175 def display_differences(self, message, label, actual):
176 """Delegate to the display_lines() routine with the appropriate
177 args. MESSAGE is ignored if None."""
178 display_lines(message, label, self.output, actual,
179 self.is_regex, self.is_unordered)
182 class AnyOutput(ExpectedOutput):
183 def __init__(self):
184 ExpectedOutput.__init__(self, None, False)
186 def is_equivalent_list(self, ignored, actual):
187 if len(actual) == 0:
188 # No actual output. No match.
189 return False
191 for line in actual:
192 # If any line has some text, then there is output, so we match.
193 if line:
194 return True
196 # We did not find a line with text. No match.
197 return False
199 def display_differences(self, message, label, actual):
200 if message:
201 print(message)
204 class RegexOutput(ExpectedOutput):
205 is_regex = True
208 class UnorderedOutput(ExpectedOutput):
209 """Marks unordered output, and performs comparisons."""
211 is_unordered = True
213 def __cmp__(self, other):
214 raise 'badness'
216 def is_equivalent_list(self, expected, actual):
217 "Disregard the order of ACTUAL lines during comparison."
219 e_set = set(expected)
220 a_set = set(actual)
222 if self.match_all:
223 if len(e_set) != len(a_set):
224 return False
225 if self.is_regex:
226 for expect_re in e_set:
227 for actual_line in a_set:
228 if re.match(expect_re, actual_line):
229 a_set.remove(actual_line)
230 break
231 else:
232 # One of the regexes was not found
233 return False
234 return True
236 # All expected lines must be in the output.
237 return e_set == a_set
239 if self.is_regex:
240 # If any of the expected regexes are in the output, then we match.
241 for expect_re in e_set:
242 for actual_line in a_set:
243 if re.match(expect_re, actual_line):
244 return True
245 return False
247 # If any of the expected lines are in the output, then we match.
248 return len(e_set.intersection(a_set)) > 0
251 class UnorderedRegexOutput(UnorderedOutput, RegexOutput):
252 is_regex = True
253 is_unordered = True
256 ######################################################################
257 # Displaying expected and actual output
259 def display_trees(message, label, expected, actual):
260 'Print two trees, expected and actual.'
261 if message is not None:
262 print(message)
263 if expected is not None:
264 print('EXPECTED %s:' % label)
265 svntest.tree.dump_tree(expected)
266 if actual is not None:
267 print('ACTUAL %s:' % label)
268 svntest.tree.dump_tree(actual)
271 def display_lines(message, label, expected, actual, expected_is_regexp=None,
272 expected_is_unordered=None):
273 """Print MESSAGE, unless it is None, then print EXPECTED (labeled
274 with LABEL) followed by ACTUAL (also labeled with LABEL).
275 Both EXPECTED and ACTUAL may be strings or lists of strings."""
276 if message is not None:
277 print(message)
278 if expected is not None:
279 output = 'EXPECTED %s' % label
280 if expected_is_regexp:
281 output += ' (regexp)'
282 if expected_is_unordered:
283 output += ' (unordered)'
284 output += ':'
285 print(output)
286 for x in expected:
287 sys.stdout.write(x)
288 if expected_is_regexp:
289 sys.stdout.write('\n')
290 if actual is not None:
291 print('ACTUAL %s:' % label)
292 for x in actual:
293 sys.stdout.write(x)
295 def compare_and_display_lines(message, label, expected, actual,
296 raisable=None):
297 """Compare two sets of output lines, and print them if they differ,
298 preceded by MESSAGE iff not None. EXPECTED may be an instance of
299 ExpectedOutput (and if not, it is wrapped as such). RAISABLE is an
300 exception class, an instance of which is thrown if ACTUAL doesn't
301 match EXPECTED."""
302 if raisable is None:
303 raisable = svntest.main.SVNLineUnequal
304 ### It'd be nicer to use createExpectedOutput() here, but its
305 ### semantics don't match all current consumers of this function.
306 if not isinstance(expected, ExpectedOutput):
307 expected = ExpectedOutput(expected)
309 if isinstance(actual, str):
310 actual = [actual]
311 actual = [line for line in actual if not line.startswith('DBG:')]
313 if not expected.matches(actual):
314 expected.display_differences(message, label, actual)
315 raise raisable
317 def verify_outputs(message, actual_stdout, actual_stderr,
318 expected_stdout, expected_stderr, all_stdout=True):
319 """Compare and display expected vs. actual stderr and stdout lines:
320 if they don't match, print the difference (preceded by MESSAGE iff
321 not None) and raise an exception.
323 If EXPECTED_STDERR or EXPECTED_STDOUT is a string the string is
324 interpreted as a regular expression. For EXPECTED_STDOUT and
325 ACTUAL_STDOUT to match, every line in ACTUAL_STDOUT must match the
326 EXPECTED_STDOUT regex, unless ALL_STDOUT is false. For
327 EXPECTED_STDERR regexes only one line in ACTUAL_STDERR need match."""
328 expected_stderr = createExpectedOutput(expected_stderr, 'stderr', False)
329 expected_stdout = createExpectedOutput(expected_stdout, 'stdout', all_stdout)
331 for (actual, expected, label, raisable) in (
332 (actual_stderr, expected_stderr, 'STDERR', SVNExpectedStderr),
333 (actual_stdout, expected_stdout, 'STDOUT', SVNExpectedStdout)):
334 if expected is None:
335 continue
337 if isinstance(expected, RegexOutput):
338 raisable = svntest.main.SVNUnmatchedError
339 elif not isinstance(expected, AnyOutput):
340 raisable = svntest.main.SVNLineUnequal
342 compare_and_display_lines(message, label, expected, actual, raisable)
344 def verify_exit_code(message, actual, expected,
345 raisable=SVNUnexpectedExitCode):
346 """Compare and display expected vs. actual exit codes:
347 if they don't match, print the difference (preceded by MESSAGE iff
348 not None) and raise an exception."""
350 if expected != actual:
351 display_lines(message, "Exit Code",
352 str(expected) + '\n', str(actual) + '\n')
353 raise raisable