Merge the tc-issue-3334 branch to trunk:
[svnrdump.git] / svntest / verify.py
blob31ccc5f7a188a93292225d7ccd860595c69e4687
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 comparisions."""
82 def __init__(self, output, match_all=True):
83 """Set SELF.output (which may be None). If MATCH_ALL is True,
84 require that all lines from OUTPUT match when performing
85 comparsisons. If False, allow any lines to match."""
86 self.output = output
87 self.match_all = match_all
88 self.is_reg_exp = False
90 def __str__(self):
91 return str(self.output)
93 def __cmp__(self, other):
94 """Return whether SELF.output matches OTHER (which may be a list
95 of newline-terminated lines, or a single string). Either value
96 may be None."""
97 if self.output is None:
98 expected = []
99 else:
100 expected = self.output
101 if other is None:
102 actual = []
103 else:
104 actual = other
106 if isinstance(actual, list):
107 if isinstance(expected, type('')):
108 expected = [expected]
109 is_match = self.is_equivalent_list(expected, actual)
110 elif isinstance(actual, type('')):
111 is_match = self.is_equivalent_line(expected, actual)
112 else: # unhandled type
113 is_match = False
115 if is_match:
116 return 0
117 else:
118 return 1
120 def is_equivalent_list(self, expected, actual):
121 "Return whether EXPECTED and ACTUAL are equivalent."
122 if not self.is_reg_exp:
123 if len(expected) != len(actual):
124 return False
125 for i in range(0, len(actual)):
126 if not self.is_equivalent_line(expected[i], actual[i]):
127 return False
128 return True
129 else:
130 expected_re = expected[0]
131 # If we want to check that every line matches the regexp
132 # assume they all match and look for any that don't. If
133 # only one line matching the regexp is enough, assume none
134 # match and look for even one that does.
135 if self.match_all:
136 all_lines_match_re = True
137 else:
138 all_lines_match_re = False
140 # If a regex was provided assume that we actually require
141 # some output. Fail if we don't.
142 if len(actual) == 0:
143 return False
145 for i in range(0, len(actual)):
146 if self.match_all:
147 if not self.is_equivalent_line(expected_re, actual[i]):
148 all_lines_match_re = False
149 break
150 else:
151 if self.is_equivalent_line(expected_re, actual[i]):
152 return True
153 return all_lines_match_re
155 def is_equivalent_line(self, expected, actual):
156 "Return whether EXPECTED and ACTUAL are equal."
157 return expected == actual
159 def display_differences(self, message, label, actual):
160 """Delegate to the display_lines() routine with the appropriate
161 args. MESSAGE is ignored if None."""
162 display_lines(message, label, self.output, actual, False, False)
164 class AnyOutput(ExpectedOutput):
165 def __init__(self):
166 ExpectedOutput.__init__(self, None, False)
168 def is_equivalent_list(self, ignored, actual):
169 if len(actual) == 0:
170 # Empty text or empty list -- either way, no output!
171 return False
172 elif isinstance(actual, list):
173 for line in actual:
174 if self.is_equivalent_line(None, line):
175 return True
176 return False
177 else:
178 return True
180 def is_equivalent_line(self, ignored, actual):
181 return len(actual) > 0
183 def display_differences(self, message, label, actual):
184 if message:
185 print(message)
187 class RegexOutput(ExpectedOutput):
188 def __init__(self, output, match_all=True, is_reg_exp=True):
189 self.output = output
190 self.match_all = match_all
191 self.is_reg_exp = is_reg_exp
193 def is_equivalent_line(self, expected, actual):
194 "Return whether the regex EXPECTED matches the ACTUAL text."
195 return re.match(expected, actual) is not None
197 def display_differences(self, message, label, actual):
198 display_lines(message, label, self.output, actual, True, False)
200 class UnorderedOutput(ExpectedOutput):
201 """Marks unordered output, and performs comparisions."""
203 def __cmp__(self, other):
204 "Handle ValueError."
205 try:
206 return ExpectedOutput.__cmp__(self, other)
207 except ValueError:
208 return 1
210 def is_equivalent_list(self, expected, actual):
211 "Disregard the order of ACTUAL lines during comparison."
212 if self.match_all:
213 if len(expected) != len(actual):
214 return False
215 expected = list(expected)
216 for actual_line in actual:
217 try:
218 i = self.is_equivalent_line(expected, actual_line)
219 expected.pop(i)
220 except ValueError:
221 return False
222 return True
223 else:
224 for actual_line in actual:
225 try:
226 self.is_equivalent_line(expected, actual_line)
227 return True
228 except ValueError:
229 pass
230 return False
232 def is_equivalent_line(self, expected, actual):
233 """Return the index into the EXPECTED lines of the line ACTUAL.
234 Raise ValueError if not found."""
235 return expected.index(actual)
237 def display_differences(self, message, label, actual):
238 display_lines(message, label, self.output, actual, False, True)
240 class UnorderedRegexOutput(UnorderedOutput, RegexOutput):
241 def is_equivalent_line(self, expected, actual):
242 for i in range(0, len(expected)):
243 if RegexOutput.is_equivalent_line(self, expected[i], actual):
244 return i
245 else:
246 raise ValueError("'%s' not found" % actual)
248 def display_differences(self, message, label, actual):
249 display_lines(message, label, self.output, actual, True, True)
252 ######################################################################
253 # Displaying expected and actual output
255 def display_trees(message, label, expected, actual):
256 'Print two trees, expected and actual.'
257 if message is not None:
258 print(message)
259 if expected is not None:
260 print('EXPECTED %s:' % label)
261 tree.dump_tree(expected)
262 if actual is not None:
263 print('ACTUAL %s:' % label)
264 tree.dump_tree(actual)
267 def display_lines(message, label, expected, actual, expected_is_regexp=None,
268 expected_is_unordered=None):
269 """Print MESSAGE, unless it is None, then print EXPECTED (labeled
270 with LABEL) followed by ACTUAL (also labeled with LABEL).
271 Both EXPECTED and ACTUAL may be strings or lists of strings."""
272 if message is not None:
273 print(message)
274 if expected is not None:
275 output = 'EXPECTED %s' % label
276 if expected_is_regexp:
277 output += ' (regexp)'
278 if expected_is_unordered:
279 output += ' (unordered)'
280 output += ':'
281 print(output)
282 for x in expected:
283 sys.stdout.write(x)
284 if expected_is_regexp:
285 sys.stdout.write('\n')
286 if actual is not None:
287 print('ACTUAL %s:' % label)
288 for x in actual:
289 sys.stdout.write(x)
291 def compare_and_display_lines(message, label, expected, actual,
292 raisable=main.SVNLineUnequal):
293 """Compare two sets of output lines, and print them if they differ,
294 preceded by MESSAGE iff not None. EXPECTED may be an instance of
295 ExpectedOutput (and if not, it is wrapped as such). RAISABLE is an
296 exception class, an instance of which is thrown if ACTUAL doesn't
297 match EXPECTED."""
298 ### It'd be nicer to use createExpectedOutput() here, but its
299 ### semantics don't match all current consumers of this function.
300 if not isinstance(expected, ExpectedOutput):
301 expected = ExpectedOutput(expected)
303 if expected != actual:
304 expected.display_differences(message, label, actual)
305 raise raisable
307 def verify_outputs(message, actual_stdout, actual_stderr,
308 expected_stdout, expected_stderr, all_stdout=True):
309 """Compare and display expected vs. actual stderr and stdout lines:
310 if they don't match, print the difference (preceded by MESSAGE iff
311 not None) and raise an exception.
313 If EXPECTED_STDERR or EXPECTED_STDOUT is a string the string is
314 interpreted as a regular expression. For EXPECTED_STDOUT and
315 ACTUAL_STDOUT to match, every line in ACTUAL_STDOUT must match the
316 EXPECTED_STDOUT regex, unless ALL_STDOUT is false. For
317 EXPECTED_STDERR regexes only one line in ACTUAL_STDERR need match."""
318 expected_stderr = createExpectedOutput(expected_stderr, False)
319 expected_stdout = createExpectedOutput(expected_stdout, all_stdout)
321 for (actual, expected, label, raisable) in (
322 (actual_stderr, expected_stderr, 'STDERR', SVNExpectedStderr),
323 (actual_stdout, expected_stdout, 'STDOUT', SVNExpectedStdout)):
324 if expected is None:
325 continue
327 expected = createExpectedOutput(expected)
328 if isinstance(expected, RegexOutput):
329 raisable = main.SVNUnmatchedError
330 elif not isinstance(expected, AnyOutput):
331 raisable = main.SVNLineUnequal
333 compare_and_display_lines(message, label, expected, actual, raisable)
335 def verify_exit_code(message, actual, expected,
336 raisable=SVNUnexpectedExitCode):
337 """Compare and display expected vs. actual exit codes:
338 if they don't match, print the difference (preceded by MESSAGE iff
339 not None) and raise an exception."""
341 # main.spawn_process() (by virtue of main.wait_on_pipe()) will return an
342 # exit code of None on platforms not supporting Popen3
343 if actual is not None and expected != actual:
344 display_lines(message, "Exit Code",
345 str(expected) + '\n', str(actual) + '\n')
346 raise raisable