Implement the test suite's ExpectedOutput(..., match_all=False) code path
[svnrdump.git] / svntest / verify.py
blob9e570f71288cfb1a99443824f452dfa499f56504
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 # Copyright (c) 2000-2008 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 ######################################################################
19 import re, sys
21 import main, tree, wc # general svntest routines in this module.
22 from svntest import Failure
24 ######################################################################
25 # Exception types
27 class SVNUnexpectedOutput(Failure):
28 """Exception raised if an invocation of svn results in unexpected
29 output of any kind."""
30 pass
32 class SVNUnexpectedStdout(SVNUnexpectedOutput):
33 """Exception raised if an invocation of svn results in unexpected
34 output on STDOUT."""
35 pass
37 class SVNUnexpectedStderr(SVNUnexpectedOutput):
38 """Exception raised if an invocation of svn results in unexpected
39 output on STDERR."""
40 pass
42 class SVNExpectedStdout(SVNUnexpectedOutput):
43 """Exception raised if an invocation of svn results in no output on
44 STDOUT when output was expected."""
45 pass
47 class SVNExpectedStderr(SVNUnexpectedOutput):
48 """Exception raised if an invocation of svn results in no output on
49 STDERR when output was expected."""
50 pass
52 class SVNUnexpectedExitCode(SVNUnexpectedOutput):
53 """Exception raised if an invocation of svn exits with a value other
54 than what was expected."""
55 pass
57 class SVNIncorrectDatatype(SVNUnexpectedOutput):
58 """Exception raised if invalid input is passed to the
59 run_and_verify_* API"""
60 pass
63 ######################################################################
64 # Comparison of expected vs. actual output
66 def createExpectedOutput(expected, match_all=True):
67 """Return EXPECTED, promoted to an ExpectedOutput instance if not
68 None. Raise SVNIncorrectDatatype if the data type of EXPECTED is
69 not handled."""
70 if isinstance(expected, type([])):
71 expected = ExpectedOutput(expected)
72 elif isinstance(expected, type('')):
73 expected = RegexOutput(expected, match_all)
74 elif expected == AnyOutput:
75 expected = AnyOutput()
76 elif expected is not None and not isinstance(expected, ExpectedOutput):
77 raise SVNIncorrectDatatype("Unexpected type for '%s' data" % output_type)
78 return expected
80 class ExpectedOutput:
81 """Contains expected output, and performs comparisons."""
82 def __init__(self, output, match_all=True):
83 """Initialize the expected output to OUTPUT which is a string, or a list
84 of strings, or None meaning an empty list. If MATCH_ALL is True, the
85 expected strings will be matched with the actual strings, one-to-one, in
86 the same order. If False, they will be matched with a subset of the
87 actual strings, one-to-one, in the same order, ignoring any other actual
88 strings among the matching ones."""
89 self.output = output
90 self.match_all = match_all
91 self.is_reg_exp = False
93 def __str__(self):
94 return str(self.output)
96 def __cmp__(self, other):
97 """Return whether SELF.output matches OTHER (which may be a list
98 of newline-terminated lines, or a single string). Either value
99 may be None."""
100 if self.output is None:
101 expected = []
102 else:
103 expected = self.output
104 if other is None:
105 actual = []
106 else:
107 actual = other
109 if isinstance(actual, list):
110 if isinstance(expected, type('')):
111 expected = [expected]
112 is_match = self.is_equivalent_list(expected, actual)
113 elif isinstance(actual, type('')):
114 is_match = self.is_equivalent_line(expected, actual)
115 else: # unhandled type
116 is_match = False
118 if is_match:
119 return 0
120 else:
121 return 1
123 def is_equivalent_list(self, expected, actual):
124 "Return whether EXPECTED and ACTUAL are equivalent."
125 if not self.is_reg_exp:
126 if self.match_all:
127 # The EXPECTED lines must match the ACTUAL lines, one-to-one, in
128 # the same order.
129 if len(expected) != len(actual):
130 return False
131 for i in range(0, len(actual)):
132 if not self.is_equivalent_line(expected[i], actual[i]):
133 return False
134 return True
135 else:
136 # The EXPECTED lines must match a subset of the ACTUAL lines,
137 # one-to-one, in the same order, with zero or more other ACTUAL
138 # lines interspersed among the matching ACTUAL lines.
139 i_expected = 0
140 for actual_line in actual:
141 if self.is_equivalent_line(expected[i_expected], actual_line):
142 i_expected += 1
143 if i_expected == len(expected):
144 return True
145 return False
146 else:
147 expected_re = expected[0]
148 # If we want to check that every line matches the regexp
149 # assume they all match and look for any that don't. If
150 # only one line matching the regexp is enough, assume none
151 # match and look for even one that does.
152 if self.match_all:
153 all_lines_match_re = True
154 else:
155 all_lines_match_re = False
157 # If a regex was provided assume that we actually require
158 # some output. Fail if we don't have any.
159 if len(actual) == 0:
160 return False
162 for i in range(0, len(actual)):
163 if self.match_all:
164 if not self.is_equivalent_line(expected_re, actual[i]):
165 all_lines_match_re = False
166 break
167 else:
168 if self.is_equivalent_line(expected_re, actual[i]):
169 return True
170 return all_lines_match_re
172 def is_equivalent_line(self, expected, actual):
173 "Return whether EXPECTED and ACTUAL are equal."
174 return expected == actual
176 def display_differences(self, message, label, actual):
177 """Delegate to the display_lines() routine with the appropriate
178 args. MESSAGE is ignored if None."""
179 display_lines(message, label, self.output, actual, False, False)
181 class AnyOutput(ExpectedOutput):
182 def __init__(self):
183 ExpectedOutput.__init__(self, None, False)
185 def is_equivalent_list(self, ignored, actual):
186 if len(actual) == 0:
187 # Empty text or empty list -- either way, no output!
188 return False
189 elif isinstance(actual, list):
190 for line in actual:
191 if self.is_equivalent_line(None, line):
192 return True
193 return False
194 else:
195 return True
197 def is_equivalent_line(self, ignored, actual):
198 return len(actual) > 0
200 def display_differences(self, message, label, actual):
201 if message:
202 print(message)
204 class RegexOutput(ExpectedOutput):
205 def __init__(self, output, match_all=True, is_reg_exp=True):
206 self.output = output
207 self.match_all = match_all
208 self.is_reg_exp = is_reg_exp
210 def is_equivalent_line(self, expected, actual):
211 "Return whether the regex EXPECTED matches the ACTUAL text."
212 return re.match(expected, actual) is not None
214 def display_differences(self, message, label, actual):
215 display_lines(message, label, self.output, actual, True, False)
217 class UnorderedOutput(ExpectedOutput):
218 """Marks unordered output, and performs comparisions."""
220 def __cmp__(self, other):
221 "Handle ValueError."
222 try:
223 return ExpectedOutput.__cmp__(self, other)
224 except ValueError:
225 return 1
227 def is_equivalent_list(self, expected, actual):
228 "Disregard the order of ACTUAL lines during comparison."
229 if self.match_all:
230 if len(expected) != len(actual):
231 return False
232 expected = list(expected)
233 for actual_line in actual:
234 try:
235 i = self.is_equivalent_line(expected, actual_line)
236 expected.pop(i)
237 except ValueError:
238 return False
239 return True
240 else:
241 for actual_line in actual:
242 try:
243 self.is_equivalent_line(expected, actual_line)
244 return True
245 except ValueError:
246 pass
247 return False
249 def is_equivalent_line(self, expected, actual):
250 """Return the index into the EXPECTED lines of the line ACTUAL.
251 Raise ValueError if not found."""
252 return expected.index(actual)
254 def display_differences(self, message, label, actual):
255 display_lines(message, label, self.output, actual, False, True)
257 class UnorderedRegexOutput(UnorderedOutput, RegexOutput):
258 def is_equivalent_line(self, expected, actual):
259 for i in range(0, len(expected)):
260 if RegexOutput.is_equivalent_line(self, expected[i], actual):
261 return i
262 else:
263 raise ValueError("'%s' not found" % actual)
265 def display_differences(self, message, label, actual):
266 display_lines(message, label, self.output, actual, True, True)
269 ######################################################################
270 # Displaying expected and actual output
272 def display_trees(message, label, expected, actual):
273 'Print two trees, expected and actual.'
274 if message is not None:
275 print(message)
276 if expected is not None:
277 print('EXPECTED %s:' % label)
278 tree.dump_tree(expected)
279 if actual is not None:
280 print('ACTUAL %s:' % label)
281 tree.dump_tree(actual)
284 def display_lines(message, label, expected, actual, expected_is_regexp=None,
285 expected_is_unordered=None):
286 """Print MESSAGE, unless it is None, then print EXPECTED (labeled
287 with LABEL) followed by ACTUAL (also labeled with LABEL).
288 Both EXPECTED and ACTUAL may be strings or lists of strings."""
289 if message is not None:
290 print(message)
291 if expected is not None:
292 output = 'EXPECTED %s' % label
293 if expected_is_regexp:
294 output += ' (regexp)'
295 if expected_is_unordered:
296 output += ' (unordered)'
297 output += ':'
298 print(output)
299 for x in expected:
300 sys.stdout.write(x)
301 if expected_is_regexp:
302 sys.stdout.write('\n')
303 if actual is not None:
304 print('ACTUAL %s:' % label)
305 for x in actual:
306 sys.stdout.write(x)
308 def compare_and_display_lines(message, label, expected, actual,
309 raisable=main.SVNLineUnequal):
310 """Compare two sets of output lines, and print them if they differ,
311 preceded by MESSAGE iff not None. EXPECTED may be an instance of
312 ExpectedOutput (and if not, it is wrapped as such). RAISABLE is an
313 exception class, an instance of which is thrown if ACTUAL doesn't
314 match EXPECTED."""
315 ### It'd be nicer to use createExpectedOutput() here, but its
316 ### semantics don't match all current consumers of this function.
317 if not isinstance(expected, ExpectedOutput):
318 expected = ExpectedOutput(expected)
320 if expected != actual:
321 expected.display_differences(message, label, actual)
322 raise raisable
324 def verify_outputs(message, actual_stdout, actual_stderr,
325 expected_stdout, expected_stderr, all_stdout=True):
326 """Compare and display expected vs. actual stderr and stdout lines:
327 if they don't match, print the difference (preceded by MESSAGE iff
328 not None) and raise an exception.
330 If EXPECTED_STDERR or EXPECTED_STDOUT is a string the string is
331 interpreted as a regular expression. For EXPECTED_STDOUT and
332 ACTUAL_STDOUT to match, every line in ACTUAL_STDOUT must match the
333 EXPECTED_STDOUT regex, unless ALL_STDOUT is false. For
334 EXPECTED_STDERR regexes only one line in ACTUAL_STDERR need match."""
335 expected_stderr = createExpectedOutput(expected_stderr, False)
336 expected_stdout = createExpectedOutput(expected_stdout, all_stdout)
338 for (actual, expected, label, raisable) in (
339 (actual_stderr, expected_stderr, 'STDERR', SVNExpectedStderr),
340 (actual_stdout, expected_stdout, 'STDOUT', SVNExpectedStdout)):
341 if expected is None:
342 continue
344 expected = createExpectedOutput(expected)
345 if isinstance(expected, RegexOutput):
346 raisable = main.SVNUnmatchedError
347 elif not isinstance(expected, AnyOutput):
348 raisable = main.SVNLineUnequal
350 compare_and_display_lines(message, label, expected, actual, raisable)
352 def verify_exit_code(message, actual, expected,
353 raisable=SVNUnexpectedExitCode):
354 """Compare and display expected vs. actual exit codes:
355 if they don't match, print the difference (preceded by MESSAGE iff
356 not None) and raise an exception."""
358 # main.spawn_process() (by virtue of main.wait_on_pipe()) will return an
359 # exit code of None on platforms not supporting Popen3
360 if actual is not None and expected != actual:
361 display_lines(message, "Exit Code",
362 str(expected) + '\n', str(actual) + '\n')
363 raise raisable