Merge from Subversion:
[samba.git] / source3 / stf / comfychair.py
1 #! /usr/bin/env python
2
3 # Copyright (C) 2002, 2003 by Martin Pool <mbp@samba.org>
4 # Copyright (C) 2003 by Tim Potter <tpot@samba.org>
5
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License as
8 # published by the Free Software Foundation; either version 2 of the
9 # License, or (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
19 # USA
20
21 """comfychair: a Python-based instrument of software torture.
22
23 Copyright (C) 2002, 2003 by Martin Pool <mbp@samba.org>
24 Copyright (C) 2003 by Tim Potter <tpot@samba.org>
25
26 This is a test framework designed for testing programs written in
27 Python, or (through a fork/exec interface) any other language.
28
29 For more information, see the file README.comfychair.
30
31 To run a test suite based on ComfyChair, just run it as a program.
32 """
33
34 # TODO: Put everything into a temporary directory?
35
36 # TODO: Have a means for tests to customize the display of their
37 # failure messages.  In particular, if a shell command failed, then
38 # give its stderr.
39
40 import sys, re
41
42 class TestCase:
43     """A base class for tests.  This class defines required functions which
44     can optionally be overridden by subclasses.  It also provides some
45     utility functions for"""
46
47     def __init__(self):
48         self.test_log = ""
49         self.background_pids = []
50     
51     def setup(self):
52         """Set up test fixture."""
53         pass
54
55     def teardown(self):
56         """Tear down test fixture."""
57         pass
58
59     def runtest(self):
60         """Run the test."""
61         pass
62
63     def fail(self, reason = ""):
64         """Say the test failed."""
65         raise AssertionError(reason)
66
67
68     #############################################################
69     # Requisition methods
70
71     def require(self, predicate, message):
72         """Check a predicate for running this test.
73
74 If the predicate value is not true, the test is skipped with a message explaining
75 why."""
76         if not predicate:
77             raise NotRunError, message
78
79     def require_root(self):
80         """Skip this test unless run by root."""
81         import os
82         self.require(os.getuid() == 0,
83                      "must be root to run this test")
84
85     #############################################################
86     # Assertion methods
87
88     def assert_(self, expr, reason = ""):
89         if not expr:
90             raise AssertionError(reason)
91
92     def assert_equal(self, a, b):
93         if not a == b:
94             raise AssertionError("assertEquals failed: %s" % `(a, b)`)
95             
96     def assert_notequal(self, a, b):
97         if a == b:
98             raise AssertionError("assertNotEqual failed: %s" % `(a, b)`)
99
100     def assert_re_match(self, pattern, s):
101         """Assert that a string matches a particular pattern
102
103         Inputs:
104           pattern      string: regular expression
105           s            string: to be matched
106
107         Raises:
108           AssertionError if not matched
109           """
110         if not re.match(pattern, s):
111             raise AssertionError("string does not match regexp\n"
112                                  "    string: %s\n"
113                                  "    re: %s" % (`s`, `pattern`))
114
115     def assert_re_search(self, pattern, s):
116         """Assert that a string *contains* a particular pattern
117
118         Inputs:
119           pattern      string: regular expression
120           s            string: to be searched
121
122         Raises:
123           AssertionError if not matched
124           """
125         if not re.search(pattern, s):
126             raise AssertionError("string does not contain regexp\n"
127                                  "    string: %s\n"
128                                  "    re: %s" % (`s`, `pattern`))
129
130
131     def assert_no_file(self, filename):
132         import os.path
133         assert not os.path.exists(filename), ("file exists but should not: %s" % filename)
134
135
136     #############################################################
137     # Methods for running programs
138
139     def runcmd_background(self, cmd):
140         import os
141         name = cmd[0]
142         self.test_log = self.test_log + "Run in background:\n" + `cmd` + "\n"
143         pid = os.spawnvp(os.P_NOWAIT, name, cmd)
144         self.test_log = self.test_log + "pid: %d\n" % pid
145         return pid
146
147
148     def runcmd(self, cmd, expectedResult = 0):
149         """Run a command, fail if the command returns an unexpected exit
150         code.  Return the output produced."""
151         rc, output = self.runcmd_unchecked(cmd)
152         if rc != expectedResult:
153             raise AssertionError("command returned %d; expected %s: \"%s\"" %
154                                  (rc, expectedResult, cmd))
155
156         return output
157
158     def runcmd_unchecked(self, cmd, skip_on_noexec = 0):
159         """Invoke a command; return (exitcode, stdout)"""
160         import os, popen2
161         pobj = popen2.Popen4(cmd)
162         output = pobj.fromchild.read()
163         waitstatus = pobj.wait()
164         assert not os.WIFSIGNALED(waitstatus), \
165                ("%s terminated with signal %d", cmd, os.WTERMSIG(waitstatus))
166         rc = os.WEXITSTATUS(waitstatus)
167         self.test_log = self.test_log + ("""Run command: %s
168 Wait status: %#x (exit code %d, signal %d)
169 Output:
170 %s""" % (cmd, waitstatus, os.WEXITSTATUS(waitstatus), os.WTERMSIG(waitstatus),
171          output))
172         if skip_on_noexec and rc == 127:
173             # Either we could not execute the command or the command
174             # returned exit code 127.  According to system(3) we can't
175             # tell the difference.
176             raise NotRunError, "could not execute %s" % `cmd`
177         return rc, output
178
179     def explain_failure(self, exc_info = None):
180         import traceback
181         # Move along, nothing to see here
182         if not exc_info and self.test_log == "":
183             return
184         print "-----------------------------------------------------------------"
185         if exc_info:
186             traceback.print_exc(file=sys.stdout)
187         print self.test_log
188         print "-----------------------------------------------------------------"
189
190
191     def log(self, msg):
192         """Log a message to the test log.  This message is displayed if
193         the test fails, or when the runtests function is invoked with
194         the verbose option."""
195         self.test_log = self.test_log + msg + "\n"
196
197
198 class NotRunError(Exception):
199     """Raised if a test must be skipped because of missing resources"""
200     def __init__(self, value = None):
201         self.value = value
202
203
204 def runtests(test_list, verbose = 0):
205     """Run a series of tests.
206
207     Eventually, this routine will also examine sys.argv[] to handle
208     extra options.
209
210     Inputs:
211       test_list    sequence of callable test objects
212
213     Returns:
214       unix return code: 0 for success, 1 for failures, 2 for test failure
215     """
216     import traceback
217     ret = 0
218     for test_class in test_list:
219         print "%-30s" % _test_name(test_class),
220         # flush now so that long running tests are easier to follow
221         sys.stdout.flush()
222
223         try:
224             try: # run test and show result
225                 obj = test_class()
226                 if hasattr(obj, "setup"):
227                     obj.setup()
228                 obj.runtest()
229                 print "OK"
230             except KeyboardInterrupt:
231                 print "INTERRUPT"
232                 obj.explain_failure(sys.exc_info())
233                 ret = 2
234                 break
235             except NotRunError, msg:
236                 print "NOTRUN, %s" % msg.value
237             except:
238                 print "FAIL"
239                 obj.explain_failure(sys.exc_info())
240                 ret = 1
241         finally:
242             try:
243                 if hasattr(obj, "teardown"):
244                     obj.teardown()
245             except KeyboardInterrupt:
246                 print "interrupted during teardown"
247                 obj.explain_failure(sys.exc_info())
248                 ret = 2
249                 break
250             except:
251                 print "error during teardown"
252                 obj.explain_failure(sys.exc_info())
253                 ret = 1
254         # Display log file if we're verbose
255         if ret == 0 and verbose:
256             obj.explain_failure()
257             
258     return ret
259
260
261 def _test_name(test_class):
262     """Return a human-readable name for a test class.
263     """
264     try:
265         return test_class.__name__
266     except:
267         return `test_class`
268
269
270 def print_help():
271     """Help for people running tests"""
272     import sys
273     print """%s: software test suite based on ComfyChair
274
275 usage:
276     To run all tests, just run this program.  To run particular tests,
277     list them on the command line.
278
279 options:
280     --help           show usage message
281     --list           list available tests
282     --verbose        show more information while running tests
283 """ % sys.argv[0]
284
285
286 def print_list(test_list):
287     """Show list of available tests"""
288     for test_class in test_list:
289         print "    %s" % _test_name(test_class)
290
291
292 def main(tests):
293     """Main entry point for test suites based on ComfyChair.
294
295 Test suites should contain this boilerplate:
296
297     if __name__ == '__main__':
298         comfychair.main(tests)
299
300 This function handles standard options such as --help and --list, and
301 by default runs all tests in the suggested order.
302
303 Calls sys.exit() on completion.
304 """
305     from sys import argv
306     import getopt, sys
307
308     verbose = 0
309
310     opts, args = getopt.getopt(argv[1:], '', ['help', 'list', 'verbose'])
311     if ('--help', '') in opts:
312         print_help()
313         return
314     elif ('--list', '') in opts:
315         print_list(tests)
316         return 
317
318     if ('--verbose', '') in opts:
319         verbose = 1
320
321     if args:
322         by_name = {}
323         for t in tests:
324             by_name[_test_name(t)] = t
325         which_tests = [by_name[name] for name in args]
326     else:
327         which_tests = tests
328
329     sys.exit(runtests(which_tests, verbose))
330
331
332 if __name__ == '__main__':
333     print __doc__