Source code for TestSupport

# labtools, Copyright (C) 2017 Jerry Fowler and Paul Scheet.
# This program comes with ABSOLUTELY NO WARRANTY. It is licensed under
# GNU GPL Version 3. License and warranty may be viewed in the manual.
'''
A support package for using the unittest module.
These are routines that are commonly useful during testing.
They include test filename generation, file comparisions, and some logging support.
'''


import os
import sys

from argparse import Namespace
import datetime
import inspect
import logging
import re
import shutil
import io
import subprocess
import tempfile
import unittest
import warnings

from labtools import timeutil
from labtools import misc
from labtools import const
from labtools.labexceptions import LabtoolsWarning

ECHO = 'echo'
DIFF = 'diff'

TARGET = os.path.join(misc.CWD, 'build', 'target')

[docs]def run_test(args, terms): ''' Return True if we want to run it, False otherwise. ''' aterminargs = any([t in args for t in terms]) if 'ignore' in args: return not aterminargs if 'only' in args or len(args): return aterminargs #print('Default true') return True
def prep_test(): result = Namespace() result.errors = [] result.failures = [] return result
[docs]def test_block(args, module, keys, userexception=LabtoolsWarning, stdout=sys.stdout): ''' Abstract the standard testing unit so I can build a help menu for unit_test.py ''' if run_test(args, keys): suite = unittest.TestLoader().loadTestsFromModule(module) print('Testing (%s)' % (keys[0]), file=stdout) try: result = unittest.TextTestRunner().run(suite) except userexception as sw: print(sw.message, file=stdout) return 1 return len(result.errors + result.failures) return 0
[docs]class TestSupport(): '''Assemble the support routines in an object for access. My normal invocation is in the testcase.setUp() method, as >>> def setUp(self): >>> self.support = TestSupport(self) ''' def __init__(self, tester): """ Initialize internal variables. """ self.tester = tester self.target = TARGET pass
[docs] def localfile(self, *args): ''' Create a new filename relative to the invoked program path. *args* can be a list, which is concatenated onto the end of the path using os.path.join() ''' return misc.localpath(*args)
[docs] def targetfile(self, *args): ''' Return a new path in the build test target directory. *args* can be a list, which is concatenated onto the end of the path using os.path.join() ''' return os.path.join(self.target, *args)
[docs] def testname(self, suffix=const.EMPTY, stack=1): ''' Return the name of the method that invoked this method, so that it can be used in constructing filenames, etc. Add the *suffix* if given. The *stack* parameter allows other routines to build testnames based on the invoking method, as for example new_test_filename() below ''' return '%s%s' % (sys._getframe(stack).f_code.co_name, suffix)
[docs] def new_test_filename(self, suffix=const.EMPTY, unique=False): ''' Return a new filename based on the invoking method. If *unique* is true, invoke os.tempnam() to guarantee a *unique* name. ''' if unique: ignored, name = tempfile.mkstemp(suffix=(const.DOT+suffix), prefix=self.testname(stack=2), dir=self.target) return name else: return self.targetfile(self.testname(suffix if suffix else '.out', stack=2))
def test_directory(self, suffix=const.EMPTY, create=True, cd_in=False): dir = self.targetfile(self.testname(suffix, stack=2)) if os.path.exists(dir): if os.path.isdir(dir): shutil.rmtree(dir) else: os.remove(dir) if create: os.makedirs(dir) if cd_in: os.chdir(dir) return dir
[docs] def log_test(self, message=const.EMPTY, stream=None): ''' Using testname(), writes the invoking method into the log, adding message if given. If *stream* is given, also writes the message to the *stream*. Typical invocation would be as the first line of a test method. Special case for testing this method allows 'return' to return the actual string for comparison. ''' funcname = self.testname(stack=2) unitname = 'unknown' # inspect.getmembers(sys._getframe(1)) print(misc.dotted_list([str(unitname), funcname])) out = ['TEST[%s]' % funcname] funcdoc = eval('self.tester.%s.__doc__' % (funcname)) if funcdoc: out.append(misc.first_line(funcdoc)) if message: out.append(message) msg = misc.spaced_list(out) logging.debug(msg) if stream == 'return': return msg if stream: print(msg, file=stream)
[docs] def test_file_diff(self, desired, produced, desiredIs='file', prefix=None, msg=None): ''' Perform a unittest assertion that two files match. if *desiredIs* ='string', compare *desired* as a string against the *produced* file. If *prefix* is not None, then replace file paths in *desired* that contain *prefix* with the current path of the test directory. The use of *prefix* is essential for different working copies of code that generates absolute pathnames. Report appropriate errors if either *desired* or *produced* do not exist. Put *msg* in the assertion if given. ''' caller = sys._getframe(1).f_code.co_name if desiredIs == 'file': self.tester.assertTrue(misc.file_or_gz(desired, test=True), msg='You have not created the "desired-output" file %s' % (desired)) if not prefix and desiredIs == 'file': self.tester.assertTrue(os.path.exists(produced), msg='%s failed to produce output file %s' % (caller, produced)) rc = subprocess.call([DIFF, desired, produced]) if rc: msg = '%s%s %s %s' % ((msg + const.NEWLINE if msg else const.EMPTY), DIFF, desired, produced) logging.warn(msg) self.tester.assertEqual(0, rc, msg=msg) else: # desired is a string or (prefix is true and desired is a file) stringdesired = desired if desiredIs == 'file': logging.debug('''Opening desired '%s' for replacement''' % (desired)) # Then we've got a prefix, and must read and alter the desired file: with open(desired) as wanted: stringdesired = wanted.read() # now desiredIs 'string' either because it was to begin with, or because we # turned it into one. in the latter case, prefix must be not None #logging.debug('''Swap '%s' in desired '%s' for replacement''' % (prefix, stringdesired)) if prefix: stringdesired = self._replace_path_prefix(stringdesired, self.localfile(const.DOT), prefix) p = subprocess.Popen([DIFF, const.DASH, produced], stdin=subprocess.PIPE) stdout, stderr = p.communicate(input=stringdesired.encode()) if p.returncode: #logging.warn('(%s) %d: %s %s' % (caller, p.returncode, # stdout.readlines() if stdout else 'No stdout', # stderr.readlines().strip() if stderr else 'No stderr')) if not os.path.isdir(self.target): os.makedirs(self.target) try: tmp = tempfile.NamedTemporaryFile(prefix=self.testname(stack=1), dir=self.target, delete=False) tmpn = tmp.name tmp.write(stringdesired.encode()) tmp.close() except IOError as io: self.fail(io.msg) msg = '%s %s%s %s %s' % (caller, (msg + const.NEWLINE if msg else const.EMPTY), DIFF, tmpn, produced) logging.warn(msg) self.tester.assertEqual(0, p.returncode, msg=msg) self.tester.assertIsNone(stderr, msg=msg)
[docs] def assert_re(self, RE, product, flags=False): ''' helper method to use regexes, chiefly for absolute pathnames ''' match = re.match(RE, product, flags) self.tester.assertTrue(match, msg = ("regex '%s' fails to match '%s'" % (RE, product)))
@staticmethod def print_namespace(namespace, stream=sys.stdout, all=False): ''' For debugging: dump the contents of the given *namespace*. Default is to sys.stdout. If *all* = True, include the _name attributes. ''' print(const.NEWLINE.join(TestSupport.list_namespace(namespace, all) + list()), file=stream) @staticmethod
[docs] def list_namespace(namespace, all=False): ''' Return a list of *namespace*'s names and their values, in the format 'name -> value' ''' rval = list() for name in vars(namespace): if all or not name.startswith(const.UNDERBAR): rval.extend(['%s -> %s' % (name, namespace.__dict__[name])]) return sorted(rval)
@staticmethod
[docs] def removeDir(path): ''' Clean up and remove a directory if it exists. Typically used in setUp(). ''' if os.path.isdir(path): shutil.rmtree(path)
# Three methods that are actually more generic than their names, # for pulling/setting arguments in a list of arguments @staticmethod
[docs] def delete_arg(arglist, flag, unary=False): ''' Remove a parameter *flag* and, (by default) its value, or just the flag if *unary* = True ''' ind = arglist.index(flag) range_end = ind+1 if unary else ind+2 arglist[ind:range_end] = []
@staticmethod
[docs] def alter_arg(arglist, flag, newvalue): ''' Set the value of a parameter keyed by *flag* to *newvalue* ''' ind = arglist.index(flag) arglist[ind+1] = newvalue
@staticmethod
[docs] def valueof_arg(arglist, flag, unary=False): ''' Return the value of a parameter *flag* or just whether it exists if *unary* =True ''' if flag not in arglist: return False if unary else None ind = arglist.index(flag) if unary: value = ind > -1 else: value = arglist[ind+1] return value
@staticmethod def _replace_path_prefix(replacee, replacement, prefix): ''' Isolate *replacement* for unit testing ''' pattern = '/[/\S]+/%s' % (prefix) return re.sub(pattern, replacement, replacee, count=0)