diff options
Diffstat (limited to 'TestCmd.py')
-rw-r--r-- | TestCmd.py | 551 |
1 files changed, 0 insertions, 551 deletions
diff --git a/TestCmd.py b/TestCmd.py deleted file mode 100644 index 438bbed..0000000 --- a/TestCmd.py +++ /dev/null @@ -1,551 +0,0 @@ -""" -TestCmd.py: a testing framework for commands and scripts. - -The TestCmd module provides a framework for portable automated testing -of executable commands and scripts (in any language, not just Python), -especially commands and scripts that require file system interaction. - -In addition to running tests and evaluating conditions, the TestCmd module -manages and cleans up one or more temporary workspace directories, and -provides methods for creating files and directories in those workspace -directories from in-line data, here-documents), allowing tests to be -completely self-contained. - -A TestCmd environment object is created via the usual invocation: - - test = TestCmd() - -The TestCmd module provides pass_test(), fail_test(), and no_result() -unbound methods that report test results for use with the Aegis change -management system. These methods terminate the test immediately, -reporting PASSED, FAILED, or NO RESULT respectively, and exiting with -status 0 (success), 1 or 2 respectively. This allows for a distinction -between an actual failed test and a test that could not be properly -evaluated because of an external condition (such as a full file system -or incorrect permissions). -""" - -# Copyright 2000 Steven Knight -# This module is free software, and you may redistribute it and/or modify -# it under the same terms as Python itself, so long as this copyright message -# and disclaimer are retained in their original form. -# -# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, -# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF -# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -# DAMAGE. -# -# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, -# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, -# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -from string import join, split - -__author__ = "Steven Knight <knight@baldmt.com>" -__revision__ = "TestCmd.py 0.D001 2001/01/14 00:43:41 software" -__version__ = "0.01" - -from types import * - -import FCNTL -import os -import os.path -import popen2 -import re -import shutil -import stat -import sys -import tempfile -import traceback - -tempfile.template = 'testcmd.' - -_Cleanup = [] - -def _clean(): - global _Cleanup - list = _Cleanup[:] - _Cleanup = [] - list.reverse() - for test in list: - test.cleanup() - -sys.exitfunc = _clean - -def _caller(tblist, skip): - string = "" - arr = [] - for file, line, name, text in tblist: - if file[-10:] == "TestCmd.py": - break - arr = [(file, line, name, text)] + arr - atfrom = "at" - for file, line, name, text in arr[skip:]: - if name == "?": - name = "" - else: - name = " (" + name + ")" - string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name)) - atfrom = "\tfrom" - return string - -def fail_test(self = None, condition = 1, function = None, skip = 0): - """Cause the test to fail. - - By default, the fail_test() method reports that the test FAILED - and exits with a status of 1. If a condition argument is supplied, - the test fails only if the condition is true. - """ - if not condition: - return - if not function is None: - function() - of = "" - desc = "" - sep = " " - if not self is None: - if self.program: - of = " of " + self.program - sep = "\n\t" - if self.description: - desc = " [" + self.description + "]" - sep = "\n\t" - - at = _caller(traceback.extract_stack(), skip) - sys.stderr.write("FAILED test" + of + desc + sep + at) - - sys.exit(1) - -def no_result(self = None, condition = 1, function = None, skip = 0): - """Causes a test to exit with no valid result. - - By default, the no_result() method reports NO RESULT for the test - and exits with a status of 2. If a condition argument is supplied, - the test fails only if the condition is true. - """ - if not condition: - return - if not function is None: - function() - of = "" - desc = "" - sep = " " - if not self is None: - if self.program: - of = " of " + self.program - sep = "\n\t" - if self.description: - desc = " [" + self.description + "]" - sep = "\n\t" - - at = _caller(traceback.extract_stack(), skip) - sys.stderr.write("NO RESULT for test" + of + desc + sep + at) - - sys.exit(2) - -def pass_test(self = None, condition = 1, function = None): - """Causes a test to pass. - - By default, the pass_test() method reports PASSED for the test - and exits with a status of 0. If a condition argument is supplied, - the test passes only if the condition is true. - """ - if not condition: - return - if not function is None: - function() - sys.stderr.write("PASSED\n") - sys.exit(0) - -def match_exact(lines = None, matches = None): - """ - """ - if not type(lines) is ListType: - lines = split(lines, "\n") - if not type(matches) is ListType: - matches = split(matches, "\n") - if len(lines) != len(matches): - return - for i in range(len(lines)): - if lines[i] != matches[i]: - return - return 1 - -def match_re(lines = None, res = None): - """ - """ - if not type(lines) is ListType: - lines = split(lines, "\n") - if not type(res) is ListType: - res = split(res, "\n") - if len(lines) != len(res): - return - for i in range(len(lines)): - if not re.compile("^" + res[i] + "$").search(lines[i]): - return - return 1 - -class TestCmd: - """Class TestCmd - """ - - def __init__(self, description = None, - program = None, - interpreter = None, - workdir = None, - subdir = None, - verbose = 0, - match = None): - self._cwd = os.getcwd() - self.description_set(description) - self.program_set(program) - self.interpreter_set(interpreter) - self.verbose_set(verbose) - if not match is None: - self.match_func = match - else: - self.match_func = match_re - self._dirlist = [] - self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0} - if os.environ.has_key('PRESERVE') and not os.environ['PRESERVE'] is '': - self._preserve['pass_test'] = os.environ['PRESERVE'] - self._preserve['fail_test'] = os.environ['PRESERVE'] - self._preserve['no_result'] = os.environ['PRESERVE'] - else: - try: - self._preserve['pass_test'] = os.environ['PRESERVE_PASS'] - except KeyError: - pass - try: - self._preserve['fail_test'] = os.environ['PRESERVE_FAIL'] - except KeyError: - pass - try: - self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT'] - except KeyError: - pass - self._stdout = [] - self._stderr = [] - self.status = None - self.condition = 'no_result' - self.workdir_set(workdir) - self.subdir(subdir) - - def __del__(self): - self.cleanup() - - def __repr__(self): - return "%x" % id(self) - - def cleanup(self, condition = None): - """Removes any temporary working directories for the specified - TestCmd environment. If the environment variable PRESERVE was - set when the TestCmd environment was created, temporary working - directories are not removed. If any of the environment variables - PRESERVE_PASS, PRESERVE_FAIL, or PRESERVE_NO_RESULT were set - when the TestCmd environment was created, then temporary working - directories are not removed if the test passed, failed, or had - no result, respectively. Temporary working directories are also - preserved for conditions specified via the preserve method. - - Typically, this method is not called directly, but is used when - the script exits to clean up temporary working directories as - appropriate for the exit status. - """ - if not self._dirlist: - return - if condition is None: - condition = self.condition - #print "cleanup(" + condition + "): ", self._preserve - if self._preserve[condition]: - return - os.chdir(self._cwd) - self.workdir = None - list = self._dirlist[:] - self._dirlist = [] - list.reverse() - for dir in list: - self.writable(dir, 1) - shutil.rmtree(dir, ignore_errors = 1) - try: - global _Cleanup - _Cleanup.remove(self) - except (AttributeError, ValueError): - pass - - def description_set(self, description): - """Set the description of the functionality being tested. - """ - self.description = description - -# def diff(self): -# """Diff two arrays. -# """ - - def fail_test(self, condition = 1, function = None, skip = 0): - """Cause the test to fail. - """ - if not condition: - return - self.condition = 'fail_test' - fail_test(self = self, - condition = condition, - function = function, - skip = skip) - - def interpreter_set(self, interpreter): - """Set the program to be used to interpret the program - under test as a script. - """ - self.interpreter = interpreter - - def match(self, lines, matches): - """Compare actual and expected file contents. - """ - return self.match_func(lines, matches) - - def match_exact(self, lines, matches): - """Compare actual and expected file contents. - """ - return match_exact(lines, matches) - - def match_re(self, lines, res): - """Compare actual and expected file contents. - """ - return match_re(lines, res) - - def no_result(self, condition = 1, function = None, skip = 0): - """Report that the test could not be run. - """ - if not condition: - return - self.condition = 'no_result' - no_result(self = self, - condition = condition, - function = function, - skip = skip) - - def pass_test(self, condition = 1, function = None): - """Cause the test to pass. - """ - if not condition: - return - self.condition = 'pass_test' - pass_test(self = self, condition = condition, function = function) - - def preserve(self, *conditions): - """Arrange for the temporary working directories for the - specified TestCmd environment to be preserved for one or more - conditions. If no conditions are specified, arranges for - the temporary working directories to be preserved for all - conditions. - """ - if conditions is (): - conditions = ('pass_test', 'fail_test', 'no_result') - for cond in conditions: - self._preserve[cond] = 1 - - def program_set(self, program): - """Set the executable program or script to be tested. - """ - if program and not os.path.isabs(program): - program = os.path.join(self._cwd, program) - self.program = program - - def read(self, file): - """Reads and returns the contents of the specified file name. - The file name may be a list, in which case the elements are - concatenated with the os.path.join() method. The file is - assumed to be under the temporary working directory unless it - is an absolute path name. - """ - if type(file) is ListType: - file = apply(os.path.join, tuple(file)) - if not os.path.isabs(file): - file = os.path.join(self.workdir, file) - f = os.fdopen(os.open(file, FCNTL.O_RDONLY)) - contents = f.read() - f.close() - return contents - - def run(self, program = None, - interpreter = None, - arguments = None, - chdir = None, - stdin = None): - """Runs a test of the program or script for the test - environment. Standard output and error output are saved for - future retrieval via the stdout() and stderr() methods. - """ - if chdir: - oldcwd = os.getcwd() - if not os.path.isabs(chdir): - chdir = os.path.join(self.workpath(chdir)) - if self.verbose: - sys.stderr.write("chdir(" + chdir + ")\n") - os.chdir(chdir) - cmd = None - if program: - if not os.path.isabs(program): - program = os.path.join(self._cwd, program) - cmd = program - if interpreter: - cmd = interpreter + " " + cmd - else: - cmd = self.program - if self.interpreter: - cmd = self.interpreter + " " + cmd - if arguments: - cmd = cmd + " " + arguments - if self.verbose: - sys.stderr.write(cmd + "\n") - p = popen2.Popen3(cmd, 1) - if stdin: - if type(stdin) is ListType: - for line in stdin: - p.tochild.write(line) - else: - p.tochild.write(stdin) - p.tochild.close() - self._stdout.append(p.fromchild.read()) - self._stderr.append(p.childerr.read()) - self.status = p.wait() - if chdir: - os.chdir(oldcwd) - - def stderr(self, run = None): - """Returns the error output from the specified run number. - If there is no specified run number, then returns the error - output of the last run. If the run number is less than zero, - then returns the error output from that many runs back from the - current run. - """ - if not run: - run = len(self._stderr) - elif run < 0: - run = len(self._stderr) + run - run = run - 1 - return self._stderr[run] - - def stdout(self, run = None): - """Returns the standard output from the specified run number. - If there is no specified run number, then returns the standard - output of the last run. If the run number is less than zero, - then returns the standard output from that many runs back from - the current run. - """ - if not run: - run = len(self._stdout) - elif run < 0: - run = len(self._stdout) + run - run = run - 1 - return self._stdout[run] - - def subdir(self, *subdirs): - """Create new subdirectories under the temporary working - directory, one for each argument. An argument may be a list, - in which case the list elements are concatenated using the - os.path.join() method. Subdirectories multiple levels deep - must be created using a separate argument for each level: - - test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory']) - - Returns the number of subdirectories actually created. - """ - count = 0 - for sub in subdirs: - if sub is None: - continue - if type(sub) is ListType: - sub = apply(os.path.join, tuple(sub)) - new = os.path.join(self.workdir, sub) - try: - os.mkdir(new) - except: - pass - else: - count = count + 1 - return count - - def verbose_set(self, verbose): - """Set the verbose level. - """ - self.verbose = verbose - - def workdir_set(self, path): - """Creates a temporary working directory with the specified - path name. If the path is a null string (''), a unique - directory name is created. - """ - if (path != None): - if path == '': - path = tempfile.mktemp() - if path != None: - os.mkdir(path) - self._dirlist.append(path) - global _Cleanup - try: - _Cleanup.index(self) - except ValueError: - _Cleanup.append(self) - # We'd like to set self.workdir like this: - # self.workdir = path - # But symlinks in the path will report things - # differently from os.getcwd(), so chdir there - # and back to fetch the canonical path. - cwd = os.getcwd() - os.chdir(path) - self.workdir = os.getcwd() - os.chdir(cwd) - else: - self.workdir = None - - def workpath(self, *args): - """Returns the absolute path name to a subdirectory or file - within the current temporary working directory. Concatenates - the temporary working directory name with the specified - arguments using the os.path.join() method. - """ - return apply(os.path.join, (self.workdir,) + tuple(args)) - - def writable(self, top, write): - """Make the specified directory tree writable (write == 1) - or not (write == None). - """ - - def _walk_chmod(arg, dirname, names): - st = os.stat(dirname) - os.chmod(dirname, arg(st[stat.ST_MODE])) - for name in names: - n = os.path.join(dirname, name) - st = os.stat(n) - os.chmod(n, arg(st[stat.ST_MODE])) - - def _mode_writable(mode): - return stat.S_IMODE(mode|0200) - - def _mode_non_writable(mode): - return stat.S_IMODE(mode&~0200) - - if write: - f = _mode_writable - else: - f = _mode_non_writable - os.path.walk(top, _walk_chmod, f) - - def write(self, file, content): - """Writes the specified content text (second argument) to the - specified file name (first argument). The file name may be - a list, in which case the elements are concatenated with the - os.path.join() method. The file is created under the temporary - working directory. Any subdirectories in the path must already - exist. """ - if type(file) is ListType: - file = apply(os.path.join, tuple(file)) - if not os.path.isabs(file): - file = os.path.join(self.workdir, file) - fd = os.open(file, FCNTL.O_CREAT|FCNTL.O_WRONLY) - os.write(fd, content) - os.close(fd) |