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