Import Samba Testing Framework code from private CVS module.
[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.  It is
26 similar in design to the very nice 'svntest' system used by
27 Subversion, but has no Subversion-specific features.
28
29 It is somewhat similar to PyUnit, except:
30
31  - it allows capture of detailed log messages from a test, to be
32    optionally displayed if the test fails.
33
34  - it allows execution of a specified subset of tests
35
36  - it avoids Java idioms that are not so useful in Python
37
38 WRITING TESTS:
39
40   Each test case is a callable object, typically a function.  Its
41   documentation string describes the test, and the first line of the
42   docstring should be a brief name.
43
44   The test should return 0 for pass, or non-zero for failure.
45   Alternatively they may raise an exception.
46
47   Tests may import this "comfychair" module to get some useful
48   utilities, but that is not strictly required.
49   
50 """
51
52 # TODO: Put everything into a temporary directory?
53
54 # TODO: Have a means for tests to customize the display of their
55 # failure messages.  In particular, if a shell command failed, then
56 # give its stderr.
57
58 import sys, re
59
60 class TestCase:
61     """A base class for tests.  This class defines required functions which
62     can optionally be overridden by subclasses.  It also provides some
63     utility functions for"""
64
65     def __init__(self):
66         self.test_log = ""
67         self.background_pids = []
68     
69     def setUp(self):
70         """Set up test fixture."""
71         pass
72
73     def tearDown(self):
74         """Tear down test fixture."""
75         pass
76
77     def runTest(self):
78         """Run the test."""
79         pass
80
81     def fail(self, reason = ""):
82         """Say the test failed."""
83         raise AssertionError(reason)
84
85     def assert_(self, expr, reason = ""):
86         if not expr:
87             raise AssertionError(reason)
88
89     def assert_re_match(self, pattern, s):
90         """Assert that a string matches a particular pattern
91
92         Inputs:
93           pattern      string: regular expression
94           s            string: to be matched
95
96         Raises:
97           AssertionError if not matched
98           """
99         if not re.match(pattern, s):
100             raise AssertionError("string %s does not match regexp %s" % (`s`, `pattern`))
101
102     def assert_regexp(self, pattern, s):
103         """Assert that a string *contains* a particular pattern
104
105         Inputs:
106           pattern      string: regular expression
107           s            string: to be searched
108
109         Raises:
110           AssertionError if not matched
111           """
112         if not re.search(pattern, s):
113             raise AssertionError("string %s does not contain regexp %s" % (`s`, `pattern`))
114
115
116     def assert_no_file(self, filename):
117         import os.path
118         assert not os.path.exists(filename), ("file exists but should not: %s" % filename)
119
120
121     def runCmdNoWait(self, cmd):
122         import os
123         name = cmd[0]
124         self.test_log = self.test_log + "Run in background:\n" + `cmd` + "\n"
125         pid = os.spawnvp(os.P_NOWAIT, name, cmd)
126         self.test_log = self.test_log + "pid: %d\n" % pid
127         return pid
128
129
130     def runCmd(self, cmd, expectedResult = 0):
131         """Run a command, fail if the command returns an unexpected exit
132         code.  Return the output produced."""
133         rc, output = self.runCmdUnchecked(cmd)
134         if rc != expectedResult:
135             raise AssertionError("command returned %d; expected %s: \"%s\"" %
136                                  (rc, expectedResult, cmd))
137
138         return output
139
140     def runCmdUnchecked(self, cmd, skip_on_noexec = 0):
141         """Invoke a command; return (exitcode, stdout)"""
142         import os, popen2
143         pobj = popen2.Popen4(cmd)
144         output = pobj.fromchild.read()
145         waitstatus = pobj.wait()
146         assert not os.WIFSIGNALED(waitstatus), \
147                ("%s terminated with signal %d", cmd, os.WTERMSIG(waitstatus))
148         rc = os.WEXITSTATUS(waitstatus)
149         self.test_log = self.test_log + ("""Run command: %s
150 Wait status: %#x
151 Output:
152 %s""" % (cmd, waitstatus, output))
153         if skip_on_noexec and rc == 127:
154             # Either we could not execute the command or the command
155             # returned exit code 127.  According to system(3) we can't
156             # tell the difference.
157             raise NotRunError, "could not execute %s" % cmd
158         return rc, output
159
160     def explainFailure(self, exc_info = None):
161         import traceback
162         # Move along, nothing to see here
163         if not exc_info and self.test_log == "":
164             return
165         print "-----------------------------------------------------------------"
166         if exc_info:
167             traceback.print_exc(file=sys.stdout)
168         print self.test_log
169         print "-----------------------------------------------------------------"
170
171     def require(self, predicate, message):
172         """Check a predicate for running this test.
173
174 If the predicate value is not true, the test is skipped with a message explaining
175 why."""
176         if not predicate:
177             raise NotRunError, message
178
179     def require_root(self):
180         """Skip this test unless run by root."""
181         import os
182         self.require(os.getuid() == 0,
183                      "must be root to run this test")
184
185     def log(self, msg):
186         """Log a message to the test log.  This message is displayed if
187         the test fails, or when the runtests function is invoked with
188         the verbose option."""
189         self.test_log = self.test_log + msg + "\n"
190
191 class NotRunError(Exception):
192     def __init__(self, value = None):
193         self.value = value
194
195 def test_name(test):
196     """Return a human-readable name for a test.
197
198     Inputs:
199       test         some kind of callable test object
200
201     Returns:
202       name         string: a short printable name
203       """
204     try:
205         return test.__name__
206     except:
207         return `test`
208
209 def runtests(test_list, verbose = 0):
210     """Run a series of tests.
211
212     Eventually, this routine will also examine sys.argv[] to handle
213     extra options.
214
215     Inputs:
216       test_list    sequence of callable test objects
217
218     Returns:
219       unix return code: 0 for success, 1 for failures, 2 for test failure
220     """
221     import traceback
222     ret = 0
223     for test in test_list:
224         print "%-60s" % test_name(test),
225         # flush now so that long running tests are easier to follow
226         sys.stdout.flush()
227
228         try:
229             try: # run test and show result
230                 obj = test()
231                 if hasattr(obj, "setUp"):
232                     obj.setUp()
233                 obj.runTest()
234                 print "OK"
235             except KeyboardInterrupt:
236                 print "INTERRUPT"
237                 obj.explainFailure(sys.exc_info())
238                 ret = 2
239                 break
240             except NotRunError, msg:
241                 print "NOTRUN, %s" % msg.value
242             except:
243                 print "FAIL"
244                 obj.explainFailure(sys.exc_info())
245                 ret = 1
246         finally:
247             try:
248                 if hasattr(obj, "tearDown"):
249                     obj.tearDown()
250             except KeyboardInterrupt:
251                 print "interrupted during tearDown"
252                 obj.explainFailure(sys.exc_info())
253                 ret = 2
254                 break
255             except:
256                 print "error during tearDown"
257                 obj.explainFailure(sys.exc_info())
258                 ret = 1
259         # Display log file if we're verbose
260         if ret == 0 and verbose:
261             obj.explainFailure()
262             
263     return ret
264
265 if __name__ == '__main__':
266     print __doc__