Add a test skeleton for obliterate, and a set of test helper functions that
[svnrdump.git] / svntest / verify.py
blob1a8c0f773b74db24a7feb43fd07741396ce0120b
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 == 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."""
90 def __init__(self, output, match_all=True):
91 """Initialize the expected output to OUTPUT which is a string, or a list
92 of strings, or None meaning an empty list. If MATCH_ALL is True, the
93 expected strings will be matched with the actual strings, one-to-one, in
94 the same order. If False, they will be matched with a subset of the
95 actual strings, one-to-one, in the same order, ignoring any other actual
96 strings among the matching ones."""
97 self.output = output
98 self.match_all = match_all
99 self.is_reg_exp = False
101 def __str__(self):
102 return str(self.output)
104 def __cmp__(self, other):
105 """Return whether SELF.output matches OTHER (which may be a list
106 of newline-terminated lines, or a single string). Either value
107 may be None."""
108 if self.output is None:
109 expected = []
110 else:
111 expected = self.output
112 if other is None:
113 actual = []
114 else:
115 actual = other
117 if isinstance(actual, list):
118 if isinstance(expected, str):
119 expected = [expected]
120 is_match = self.is_equivalent_list(expected, actual)
121 elif isinstance(actual, str):
122 is_match = self.is_equivalent_line(expected, actual)
123 else: # unhandled type
124 is_match = False
126 if is_match:
127 return 0
128 else:
129 return 1
131 def is_equivalent_list(self, expected, actual):
132 "Return whether EXPECTED and ACTUAL are equivalent."
133 if not self.is_reg_exp:
134 if self.match_all:
135 # The EXPECTED lines must match the ACTUAL lines, one-to-one, in
136 # the same order.
137 if len(expected) != len(actual):
138 return False
139 for i in range(0, len(actual)):
140 if not self.is_equivalent_line(expected[i], actual[i]):
141 return False
142 return True
143 else:
144 # The EXPECTED lines must match a subset of the ACTUAL lines,
145 # one-to-one, in the same order, with zero or more other ACTUAL
146 # lines interspersed among the matching ACTUAL lines.
147 i_expected = 0
148 for actual_line in actual:
149 if self.is_equivalent_line(expected[i_expected], actual_line):
150 i_expected += 1
151 if i_expected == len(expected):
152 return True
153 return False
154 else:
155 expected_re = expected[0]
156 # If we want to check that every line matches the regexp
157 # assume they all match and look for any that don't. If
158 # only one line matching the regexp is enough, assume none
159 # match and look for even one that does.
160 if self.match_all:
161 all_lines_match_re = True
162 else:
163 all_lines_match_re = False
165 # If a regex was provided assume that we actually require
166 # some output. Fail if we don't have any.
167 if len(actual) == 0:
168 return False
170 for i in range(0, len(actual)):
171 if self.match_all:
172 if not self.is_equivalent_line(expected_re, actual[i]):
173 all_lines_match_re = False
174 break
175 else:
176 if self.is_equivalent_line(expected_re, actual[i]):
177 return True
178 return all_lines_match_re
180 def is_equivalent_line(self, expected, actual):
181 "Return whether EXPECTED and ACTUAL are equal."
182 return expected == actual
184 def display_differences(self, message, label, actual):
185 """Delegate to the display_lines() routine with the appropriate
186 args. MESSAGE is ignored if None."""
187 display_lines(message, label, self.output, actual, False, False)
189 class AnyOutput(ExpectedOutput):
190 def __init__(self):
191 ExpectedOutput.__init__(self, None, False)
193 def is_equivalent_list(self, ignored, actual):
194 if len(actual) == 0:
195 # Empty text or empty list -- either way, no output!
196 return False
197 elif isinstance(actual, list):
198 for line in actual:
199 if self.is_equivalent_line(None, line):
200 return True
201 return False
202 else:
203 return True
205 def is_equivalent_line(self, ignored, actual):
206 return len(actual) > 0
208 def display_differences(self, message, label, actual):
209 if message:
210 print(message)
212 class RegexOutput(ExpectedOutput):
213 def __init__(self, output, match_all=True, is_reg_exp=True):
214 self.output = output
215 self.match_all = match_all
216 self.is_reg_exp = is_reg_exp
218 def is_equivalent_line(self, expected, actual):
219 "Return whether the regex EXPECTED matches the ACTUAL text."
220 return re.match(expected, actual) is not None
222 def display_differences(self, message, label, actual):
223 display_lines(message, label, self.output, actual, True, False)
225 class UnorderedOutput(ExpectedOutput):
226 """Marks unordered output, and performs comparisons."""
228 def __cmp__(self, other):
229 "Handle ValueError."
230 try:
231 return ExpectedOutput.__cmp__(self, other)
232 except ValueError:
233 return 1
235 def is_equivalent_list(self, expected, actual):
236 "Disregard the order of ACTUAL lines during comparison."
237 if self.match_all:
238 if len(expected) != len(actual):
239 return False
240 expected = list(expected)
241 for actual_line in actual:
242 try:
243 i = self.is_equivalent_line(expected, actual_line)
244 expected.pop(i)
245 except ValueError:
246 return False
247 return True
248 else:
249 for actual_line in actual:
250 try:
251 self.is_equivalent_line(expected, actual_line)
252 return True
253 except ValueError:
254 pass
255 return False
257 def is_equivalent_line(self, expected, actual):
258 """Return the index into the EXPECTED lines of the line ACTUAL.
259 Raise ValueError if not found."""
260 return expected.index(actual)
262 def display_differences(self, message, label, actual):
263 display_lines(message, label, self.output, actual, False, True)
265 class UnorderedRegexOutput(UnorderedOutput, RegexOutput):
266 def is_equivalent_line(self, expected, actual):
267 for i in range(0, len(expected)):
268 if RegexOutput.is_equivalent_line(self, expected[i], actual):
269 return i
270 else:
271 raise ValueError("'%s' not found" % actual)
273 def display_differences(self, message, label, actual):
274 display_lines(message, label, self.output, actual, True, True)
277 ######################################################################
278 # Displaying expected and actual output
280 def display_trees(message, label, expected, actual):
281 'Print two trees, expected and actual.'
282 if message is not None:
283 print(message)
284 if expected is not None:
285 print('EXPECTED %s:' % label)
286 svntest.tree.dump_tree(expected)
287 if actual is not None:
288 print('ACTUAL %s:' % label)
289 svntest.tree.dump_tree(actual)
292 def display_lines(message, label, expected, actual, expected_is_regexp=None,
293 expected_is_unordered=None):
294 """Print MESSAGE, unless it is None, then print EXPECTED (labeled
295 with LABEL) followed by ACTUAL (also labeled with LABEL).
296 Both EXPECTED and ACTUAL may be strings or lists of strings."""
297 if message is not None:
298 print(message)
299 if expected is not None:
300 output = 'EXPECTED %s' % label
301 if expected_is_regexp:
302 output += ' (regexp)'
303 if expected_is_unordered:
304 output += ' (unordered)'
305 output += ':'
306 print(output)
307 for x in expected:
308 sys.stdout.write(x)
309 if expected_is_regexp:
310 sys.stdout.write('\n')
311 if actual is not None:
312 print('ACTUAL %s:' % label)
313 for x in actual:
314 sys.stdout.write(x)
316 def compare_and_display_lines(message, label, expected, actual,
317 raisable=None):
318 """Compare two sets of output lines, and print them if they differ,
319 preceded by MESSAGE iff not None. EXPECTED may be an instance of
320 ExpectedOutput (and if not, it is wrapped as such). RAISABLE is an
321 exception class, an instance of which is thrown if ACTUAL doesn't
322 match EXPECTED."""
323 if raisable is None:
324 raisable = svntest.main.SVNLineUnequal
325 ### It'd be nicer to use createExpectedOutput() here, but its
326 ### semantics don't match all current consumers of this function.
327 if not isinstance(expected, ExpectedOutput):
328 expected = ExpectedOutput(expected)
330 if isinstance(actual, str):
331 actual = [actual]
332 actual = [line for line in actual if not line.startswith('DBG:')]
334 if expected != actual:
335 expected.display_differences(message, label, actual)
336 raise raisable
338 def verify_outputs(message, actual_stdout, actual_stderr,
339 expected_stdout, expected_stderr, all_stdout=True):
340 """Compare and display expected vs. actual stderr and stdout lines:
341 if they don't match, print the difference (preceded by MESSAGE iff
342 not None) and raise an exception.
344 If EXPECTED_STDERR or EXPECTED_STDOUT is a string the string is
345 interpreted as a regular expression. For EXPECTED_STDOUT and
346 ACTUAL_STDOUT to match, every line in ACTUAL_STDOUT must match the
347 EXPECTED_STDOUT regex, unless ALL_STDOUT is false. For
348 EXPECTED_STDERR regexes only one line in ACTUAL_STDERR need match."""
349 expected_stderr = createExpectedOutput(expected_stderr, 'stderr', False)
350 expected_stdout = createExpectedOutput(expected_stdout, 'stdout', all_stdout)
352 for (actual, expected, label, raisable) in (
353 (actual_stderr, expected_stderr, 'STDERR', SVNExpectedStderr),
354 (actual_stdout, expected_stdout, 'STDOUT', SVNExpectedStdout)):
355 if expected is None:
356 continue
358 if isinstance(expected, RegexOutput):
359 raisable = svntest.main.SVNUnmatchedError
360 elif not isinstance(expected, AnyOutput):
361 raisable = svntest.main.SVNLineUnequal
363 compare_and_display_lines(message, label, expected, actual, raisable)
365 def verify_exit_code(message, actual, expected,
366 raisable=SVNUnexpectedExitCode):
367 """Compare and display expected vs. actual exit codes:
368 if they don't match, print the difference (preceded by MESSAGE iff
369 not None) and raise an exception."""
371 if expected != actual:
372 display_lines(message, "Exit Code",
373 str(expected) + '\n', str(actual) + '\n')
374 raise raisable