http://scons.tigris.org/issues/show_bug.cgi?id=2329
[scons.git] / QMTest / TestCommon.py
1 """
2 TestCommon.py:  a testing framework for commands and scripts
3                 with commonly useful error handling
4
5 The TestCommon module provides a simple, high-level interface for writing
6 tests of executable commands and scripts, especially commands and scripts
7 that interact with the file system.  All methods throw exceptions and
8 exit on failure, with useful error messages.  This makes a number of
9 explicit checks unnecessary, making the test scripts themselves simpler
10 to write and easier to read.
11
12 The TestCommon class is a subclass of the TestCmd class.  In essence,
13 TestCommon is a wrapper that handles common TestCmd error conditions in
14 useful ways.  You can use TestCommon directly, or subclass it for your
15 program and add additional (or override) methods to tailor it to your
16 program's specific needs.  Alternatively, the TestCommon class serves
17 as a useful example of how to define your own TestCmd subclass.
18
19 As a subclass of TestCmd, TestCommon provides access to all of the
20 variables and methods from the TestCmd module.  Consequently, you can
21 use any variable or method documented in the TestCmd module without
22 having to explicitly import TestCmd.
23
24 A TestCommon environment object is created via the usual invocation:
25
26     import TestCommon
27     test = TestCommon.TestCommon()
28
29 You can use all of the TestCmd keyword arguments when instantiating a
30 TestCommon object; see the TestCmd documentation for details.
31
32 Here is an overview of the methods and keyword arguments that are
33 provided by the TestCommon class:
34
35     test.must_be_writable('file1', ['file2', ...])
36
37     test.must_contain('file', 'required text\n')
38
39     test.must_contain_all_lines(output, lines, ['title', find])
40
41     test.must_contain_any_line(output, lines, ['title', find])
42
43     test.must_exist('file1', ['file2', ...])
44
45     test.must_match('file', "expected contents\n")
46
47     test.must_not_be_writable('file1', ['file2', ...])
48
49     test.must_not_contain('file', 'banned text\n')
50
51     test.must_not_contain_any_line(output, lines, ['title', find])
52
53     test.must_not_exist('file1', ['file2', ...])
54
55     test.run(options = "options to be prepended to arguments",
56              stdout = "expected standard output from the program",
57              stderr = "expected error output from the program",
58              status = expected_status,
59              match = match_function)
60
61 The TestCommon module also provides the following variables
62
63     TestCommon.python_executable
64     TestCommon.exe_suffix
65     TestCommon.obj_suffix
66     TestCommon.shobj_prefix
67     TestCommon.shobj_suffix
68     TestCommon.lib_prefix
69     TestCommon.lib_suffix
70     TestCommon.dll_prefix
71     TestCommon.dll_suffix
72
73 """
74
75 # Copyright 2000-2010 Steven Knight
76 # This module is free software, and you may redistribute it and/or modify
77 # it under the same terms as Python itself, so long as this copyright message
78 # and disclaimer are retained in their original form.
79 #
80 # IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
81 # SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
82 # THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
83 # DAMAGE.
84 #
85 # THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
86 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
87 # PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
88 # AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
89 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
90 from __future__ import generators  ### KEEP FOR COMPATIBILITY FIXERS
91
92 __author__ = "Steven Knight <knight at baldmt dot com>"
93 __revision__ = "TestCommon.py 0.37.D001 2010/01/11 16:55:50 knight"
94 __version__ = "0.37"
95
96 import copy
97 import os
98 import os.path
99 import stat
100 import sys
101 import UserList
102
103 from TestCmd import *
104 from TestCmd import __all__
105
106 __all__.extend([ 'TestCommon',
107                  'exe_suffix',
108                  'obj_suffix',
109                  'shobj_prefix',
110                  'shobj_suffix',
111                  'lib_prefix',
112                  'lib_suffix',
113                  'dll_prefix',
114                  'dll_suffix',
115                ])
116
117 # Variables that describe the prefixes and suffixes on this system.
118 if sys.platform == 'win32':
119     exe_suffix   = '.exe'
120     obj_suffix   = '.obj'
121     shobj_suffix = '.obj'
122     shobj_prefix = ''
123     lib_prefix   = ''
124     lib_suffix   = '.lib'
125     dll_prefix   = ''
126     dll_suffix   = '.dll'
127 elif sys.platform == 'cygwin':
128     exe_suffix   = '.exe'
129     obj_suffix   = '.o'
130     shobj_suffix = '.os'
131     shobj_prefix = ''
132     lib_prefix   = 'lib'
133     lib_suffix   = '.a'
134     dll_prefix   = ''
135     dll_suffix   = '.dll'
136 elif sys.platform.find('irix') != -1:
137     exe_suffix   = ''
138     obj_suffix   = '.o'
139     shobj_suffix = '.o'
140     shobj_prefix = ''
141     lib_prefix   = 'lib'
142     lib_suffix   = '.a'
143     dll_prefix   = 'lib'
144     dll_suffix   = '.so'
145 elif sys.platform.find('darwin') != -1:
146     exe_suffix   = ''
147     obj_suffix   = '.o'
148     shobj_suffix = '.os'
149     shobj_prefix = ''
150     lib_prefix   = 'lib'
151     lib_suffix   = '.a'
152     dll_prefix   = 'lib'
153     dll_suffix   = '.dylib'
154 elif sys.platform.find('sunos') != -1:
155     exe_suffix   = ''
156     obj_suffix   = '.o'
157     shobj_suffix = '.os'
158     shobj_prefix = 'so_'
159     lib_prefix   = 'lib'
160     lib_suffix   = '.a'
161     dll_prefix   = 'lib'
162     dll_suffix   = '.dylib'
163 else:
164     exe_suffix   = ''
165     obj_suffix   = '.o'
166     shobj_suffix = '.os'
167     shobj_prefix = ''
168     lib_prefix   = 'lib'
169     lib_suffix   = '.a'
170     dll_prefix   = 'lib'
171     dll_suffix   = '.so'
172
173 def is_List(e):
174     return isinstance(e, list) \
175         or isinstance(e, UserList.UserList)
176
177 def is_writable(f):
178     mode = os.stat(f)[stat.ST_MODE]
179     return mode & stat.S_IWUSR
180
181 def separate_files(flist):
182     existing = []
183     missing = []
184     for f in flist:
185         if os.path.exists(f):
186             existing.append(f)
187         else:
188             missing.append(f)
189     return existing, missing
190
191 if os.name == 'posix':
192     def _failed(self, status = 0):
193         if self.status is None or status is None:
194             return None
195         return _status(self) != status
196     def _status(self):
197         return self.status
198 elif os.name == 'nt':
199     def _failed(self, status = 0):
200         return not (self.status is None or status is None) and \
201                self.status != status
202     def _status(self):
203         return self.status
204
205 class TestCommon(TestCmd):
206
207     # Additional methods from the Perl Test::Cmd::Common module
208     # that we may wish to add in the future:
209     #
210     #  $test->subdir('subdir', ...);
211     #
212     #  $test->copy('src_file', 'dst_file');
213
214     def __init__(self, **kw):
215         """Initialize a new TestCommon instance.  This involves just
216         calling the base class initialization, and then changing directory
217         to the workdir.
218         """
219         TestCmd.__init__(self, **kw)
220         os.chdir(self.workdir)
221
222     def must_be_writable(self, *files):
223         """Ensures that the specified file(s) exist and are writable.
224         An individual file can be specified as a list of directory names,
225         in which case the pathname will be constructed by concatenating
226         them.  Exits FAILED if any of the files does not exist or is
227         not writable.
228         """
229         files = [is_List(x) and os.path.join(*x) or x for x in files]
230         existing, missing = separate_files(files)
231         unwritable = [x for x in existing if not is_writable(x)]
232         if missing:
233             print "Missing files: `%s'" % "', `".join(missing)
234         if unwritable:
235             print "Unwritable files: `%s'" % "', `".join(unwritable)
236         self.fail_test(missing + unwritable)
237
238     def must_contain(self, file, required, mode = 'rb'):
239         """Ensures that the specified file contains the required text.
240         """
241         file_contents = self.read(file, mode)
242         contains = (file_contents.find(required) != -1)
243         if not contains:
244             print "File `%s' does not contain required string." % file
245             print self.banner('Required string ')
246             print required
247             print self.banner('%s contents ' % file)
248             print file_contents
249             self.fail_test(not contains)
250
251     def must_contain_all_lines(self, output, lines, title=None, find=None):
252         """Ensures that the specified output string (first argument)
253         contains all of the specified lines (second argument).
254
255         An optional third argument can be used to describe the type
256         of output being searched, and only shows up in failure output.
257
258         An optional fourth argument can be used to supply a different
259         function, of the form "find(line, output), to use when searching
260         for lines in the output.
261         """
262         if find is None:
263             find = lambda o, l: o.find(l) != -1
264         missing = []
265         for line in lines:
266             if not find(output, line):
267                 missing.append(line)
268
269         if missing:
270             if title is None:
271                 title = 'output'
272             sys.stdout.write("Missing expected lines from %s:\n" % title)
273             for line in missing:
274                 sys.stdout.write('    ' + repr(line) + '\n')
275             sys.stdout.write(self.banner(title + ' '))
276             sys.stdout.write(output)
277             self.fail_test()
278
279     def must_contain_any_line(self, output, lines, title=None, find=None):
280         """Ensures that the specified output string (first argument)
281         contains at least one of the specified lines (second argument).
282
283         An optional third argument can be used to describe the type
284         of output being searched, and only shows up in failure output.
285
286         An optional fourth argument can be used to supply a different
287         function, of the form "find(line, output), to use when searching
288         for lines in the output.
289         """
290         if find is None:
291             find = lambda o, l: o.find(l) != -1
292         for line in lines:
293             if find(output, line):
294                 return
295
296         if title is None:
297             title = 'output'
298         sys.stdout.write("Missing any expected line from %s:\n" % title)
299         for line in lines:
300             sys.stdout.write('    ' + repr(line) + '\n')
301         sys.stdout.write(self.banner(title + ' '))
302         sys.stdout.write(output)
303         self.fail_test()
304
305     def must_contain_lines(self, lines, output, title=None):
306         # Deprecated; retain for backwards compatibility.
307         return self.must_contain_all_lines(output, lines, title)
308
309     def must_exist(self, *files):
310         """Ensures that the specified file(s) must exist.  An individual
311         file be specified as a list of directory names, in which case the
312         pathname will be constructed by concatenating them.  Exits FAILED
313         if any of the files does not exist.
314         """
315         files = [is_List(x) and os.path.join(*x) or x for x in files]
316         missing = [x for x in files if not os.path.exists(x)]
317         if missing:
318             print "Missing files: `%s'" % "', `".join(missing)
319             self.fail_test(missing)
320
321     def must_match(self, file, expect, mode = 'rb'):
322         """Matches the contents of the specified file (first argument)
323         against the expected contents (second argument).  The expected
324         contents are a list of lines or a string which will be split
325         on newlines.
326         """
327         file_contents = self.read(file, mode)
328         try:
329             self.fail_test(not self.match(file_contents, expect))
330         except KeyboardInterrupt:
331             raise
332         except:
333             print "Unexpected contents of `%s'" % file
334             self.diff(expect, file_contents, 'contents ')
335             raise
336
337     def must_not_contain(self, file, banned, mode = 'rb'):
338         """Ensures that the specified file doesn't contain the banned text.
339         """
340         file_contents = self.read(file, mode)
341         contains = (file_contents.find(banned) != -1)
342         if contains:
343             print "File `%s' contains banned string." % file
344             print self.banner('Banned string ')
345             print banned
346             print self.banner('%s contents ' % file)
347             print file_contents
348             self.fail_test(contains)
349
350     def must_not_contain_any_line(self, output, lines, title=None, find=None):
351         """Ensures that the specified output string (first argument)
352         does not contain any of the specified lines (second argument).
353
354         An optional third argument can be used to describe the type
355         of output being searched, and only shows up in failure output.
356
357         An optional fourth argument can be used to supply a different
358         function, of the form "find(line, output), to use when searching
359         for lines in the output.
360         """
361         if find is None:
362             find = lambda o, l: o.find(l) != -1
363         unexpected = []
364         for line in lines:
365             if find(output, line):
366                 unexpected.append(line)
367
368         if unexpected:
369             if title is None:
370                 title = 'output'
371             sys.stdout.write("Unexpected lines in %s:\n" % title)
372             for line in unexpected:
373                 sys.stdout.write('    ' + repr(line) + '\n')
374             sys.stdout.write(self.banner(title + ' '))
375             sys.stdout.write(output)
376             self.fail_test()
377
378     def must_not_contain_lines(self, lines, output, title=None):
379         return self.must_not_contain_any_line(output, lines, title)
380
381     def must_not_exist(self, *files):
382         """Ensures that the specified file(s) must not exist.
383         An individual file be specified as a list of directory names, in
384         which case the pathname will be constructed by concatenating them.
385         Exits FAILED if any of the files exists.
386         """
387         files = [is_List(x) and os.path.join(*x) or x for x in files]
388         existing = list(filter(os.path.exists, files))
389         if existing:
390             print "Unexpected files exist: `%s'" % "', `".join(existing)
391             self.fail_test(existing)
392
393
394     def must_not_be_writable(self, *files):
395         """Ensures that the specified file(s) exist and are not writable.
396         An individual file can be specified as a list of directory names,
397         in which case the pathname will be constructed by concatenating
398         them.  Exits FAILED if any of the files does not exist or is
399         writable.
400         """
401         files = [is_List(x) and os.path.join(*x) or x for x in files]
402         existing, missing = separate_files(files)
403         writable = list(filter(is_writable, existing))
404         if missing:
405             print "Missing files: `%s'" % "', `".join(missing)
406         if writable:
407             print "Writable files: `%s'" % "', `".join(writable)
408         self.fail_test(missing + writable)
409
410     def _complete(self, actual_stdout, expected_stdout,
411                         actual_stderr, expected_stderr, status, match):
412         """
413         Post-processes running a subcommand, checking for failure
414         status and displaying output appropriately.
415         """
416         if _failed(self, status):
417             expect = ''
418             if status != 0:
419                 expect = " (expected %s)" % str(status)
420             print "%s returned %s%s" % (self.program, str(_status(self)), expect)
421             print self.banner('STDOUT ')
422             print actual_stdout
423             print self.banner('STDERR ')
424             print actual_stderr
425             self.fail_test()
426         if not expected_stdout is None and not match(actual_stdout, expected_stdout):
427             self.diff(expected_stdout, actual_stdout, 'STDOUT ')
428             if actual_stderr:
429                 print self.banner('STDERR ')
430                 print actual_stderr
431             self.fail_test()
432         if not expected_stderr is None and not match(actual_stderr, expected_stderr):
433             print self.banner('STDOUT ')
434             print actual_stdout
435             self.diff(expected_stderr, actual_stderr, 'STDERR ')
436             self.fail_test()
437
438     def start(self, program = None,
439                     interpreter = None,
440                     arguments = None,
441                     universal_newlines = None,
442                     **kw):
443         """
444         Starts a program or script for the test environment.
445
446         This handles the "options" keyword argument and exceptions.
447         """
448         try:
449             options = kw['options']
450             del kw['options']
451         except KeyError:
452             pass
453         else:
454             if options:
455                 if arguments is None:
456                     arguments = options
457                 else:
458                     arguments = options + " " + arguments
459         try:
460             return TestCmd.start(self, program, interpreter, arguments, universal_newlines,
461                          **kw)
462         except KeyboardInterrupt:
463             raise
464         except Exception, e:
465             print self.banner('STDOUT ')
466             try:
467                 print self.stdout()
468             except IndexError:
469                 pass
470             print self.banner('STDERR ')
471             try:
472                 print self.stderr()
473             except IndexError:
474                 pass
475             cmd_args = self.command_args(program, interpreter, arguments)
476             sys.stderr.write('Exception trying to execute: %s\n' % cmd_args)
477             raise e
478
479     def finish(self, popen, stdout = None, stderr = '', status = 0, **kw):
480         """
481         Finishes and waits for the process being run under control of
482         the specified popen argument.  Additional arguments are similar
483         to those of the run() method:
484
485                 stdout  The expected standard output from
486                         the command.  A value of None means
487                         don't test standard output.
488
489                 stderr  The expected error output from
490                         the command.  A value of None means
491                         don't test error output.
492
493                 status  The expected exit status from the
494                         command.  A value of None means don't
495                         test exit status.
496         """
497         TestCmd.finish(self, popen, **kw)
498         match = kw.get('match', self.match)
499         self._complete(self.stdout(), stdout,
500                        self.stderr(), stderr, status, match)
501
502     def run(self, options = None, arguments = None,
503                   stdout = None, stderr = '', status = 0, **kw):
504         """Runs the program under test, checking that the test succeeded.
505
506         The arguments are the same as the base TestCmd.run() method,
507         with the addition of:
508
509                 options Extra options that get appended to the beginning
510                         of the arguments.
511
512                 stdout  The expected standard output from
513                         the command.  A value of None means
514                         don't test standard output.
515
516                 stderr  The expected error output from
517                         the command.  A value of None means
518                         don't test error output.
519
520                 status  The expected exit status from the
521                         command.  A value of None means don't
522                         test exit status.
523
524         By default, this expects a successful exit (status = 0), does
525         not test standard output (stdout = None), and expects that error
526         output is empty (stderr = "").
527         """
528         if options:
529             if arguments is None:
530                 arguments = options
531             else:
532                 arguments = options + " " + arguments
533         kw['arguments'] = arguments
534         try:
535             match = kw['match']
536             del kw['match']
537         except KeyError:
538             match = self.match
539         TestCmd.run(self, **kw)
540         self._complete(self.stdout(), stdout,
541                        self.stderr(), stderr, status, match)
542
543     def skip_test(self, message="Skipping test.\n"):
544         """Skips a test.
545
546         Proper test-skipping behavior is dependent on the external
547         TESTCOMMON_PASS_SKIPS environment variable.  If set, we treat
548         the skip as a PASS (exit 0), and otherwise treat it as NO RESULT.
549         In either case, we print the specified message as an indication
550         that the substance of the test was skipped.
551
552         (This was originally added to support development under Aegis.
553         Technically, skipping a test is a NO RESULT, but Aegis would
554         treat that as a test failure and prevent the change from going to
555         the next step.  Since we ddn't want to force anyone using Aegis
556         to have to install absolutely every tool used by the tests, we
557         would actually report to Aegis that a skipped test has PASSED
558         so that the workflow isn't held up.)
559         """
560         if message:
561             sys.stdout.write(message)
562             sys.stdout.flush()
563         pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS')
564         if pass_skips in [None, 0, '0']:
565             # skip=1 means skip this function when showing where this
566             # result came from.  They only care about the line where the
567             # script called test.skip_test(), not the line number where
568             # we call test.no_result().
569             self.no_result(skip=1)
570         else:
571             # We're under the development directory for this change,
572             # so this is an Aegis invocation; pass the test (exit 0).
573             self.pass_test()
574
575 # Local Variables:
576 # tab-width:4
577 # indent-tabs-mode:nil
578 # End:
579 # vim: set expandtab tabstop=4 shiftwidth=4: