diff options
Diffstat (limited to 'tests/coveragetest.py')
-rw-r--r-- | tests/coveragetest.py | 465 |
1 files changed, 465 insertions, 0 deletions
diff --git a/tests/coveragetest.py b/tests/coveragetest.py new file mode 100644 index 00000000..6f6217aa --- /dev/null +++ b/tests/coveragetest.py @@ -0,0 +1,465 @@ +"""Base test case class for coverage testing.""" + +import glob, imp, os, random, shlex, shutil, sys, tempfile, textwrap + +import coverage +from coverage.backward import sorted, StringIO # pylint: disable=W0622 +from coverage.backward import to_bytes +from coverage.control import _TEST_NAME_FILE +from test.backtest import run_command +from test.backunittest import TestCase + +class Tee(object): + """A file-like that writes to all the file-likes it has.""" + + def __init__(self, *files): + """Make a Tee that writes to all the files in `files.`""" + self._files = files + if hasattr(files[0], "encoding"): + self.encoding = files[0].encoding + + def write(self, data): + """Write `data` to all the files.""" + for f in self._files: + f.write(data) + + if 0: + # Use this if you need to use a debugger, though it makes some tests + # fail, I'm not sure why... + def __getattr__(self, name): + return getattr(self._files[0], name) + + +# Status returns for the command line. +OK, ERR = 0, 1 + +class CoverageTest(TestCase): + """A base class for Coverage test cases.""" + + run_in_temp_dir = True + + def setUp(self): + super(CoverageTest, self).setUp() + + if _TEST_NAME_FILE: + f = open(_TEST_NAME_FILE, "w") + f.write("%s_%s" % (self.__class__.__name__, self._testMethodName)) + f.close() + + # Tell newer unittest implementations to print long helpful messages. + self.longMessage = True + + # tearDown will restore the original sys.path + self.old_syspath = sys.path[:] + + if self.run_in_temp_dir: + # Create a temporary directory. + self.noise = str(random.random())[2:] + self.temp_root = os.path.join(tempfile.gettempdir(), 'test_cover') + self.temp_dir = os.path.join(self.temp_root, self.noise) + os.makedirs(self.temp_dir) + self.old_dir = os.getcwd() + os.chdir(self.temp_dir) + + # Modules should be importable from this temp directory. + sys.path.insert(0, '') + + # Keep a counter to make every call to check_coverage unique. + self.n = 0 + + # Record environment variables that we changed with set_environ. + self.environ_undos = {} + + # Capture stdout and stderr so we can examine them in tests. + # nose keeps stdout from littering the screen, so we can safely Tee it, + # but it doesn't capture stderr, so we don't want to Tee stderr to the + # real stderr, since it will interfere with our nice field of dots. + self.old_stdout = sys.stdout + self.captured_stdout = StringIO() + sys.stdout = Tee(sys.stdout, self.captured_stdout) + self.old_stderr = sys.stderr + self.captured_stderr = StringIO() + sys.stderr = self.captured_stderr + + # Record sys.modules here so we can restore it in tearDown. + self.old_modules = dict(sys.modules) + + def tearDown(self): + super(CoverageTest, self).tearDown() + + # Restore the original sys.path. + sys.path = self.old_syspath + + if self.run_in_temp_dir: + # Get rid of the temporary directory. + os.chdir(self.old_dir) + shutil.rmtree(self.temp_root) + + # Restore the environment. + self.undo_environ() + + # Restore stdout and stderr + sys.stdout = self.old_stdout + sys.stderr = self.old_stderr + + self.clean_modules() + + def clean_modules(self): + """Remove any new modules imported during the test run. + + This lets us import the same source files for more than one test. + + """ + for m in [m for m in sys.modules if m not in self.old_modules]: + del sys.modules[m] + + def set_environ(self, name, value): + """Set an environment variable `name` to be `value`. + + The environment variable is set, and record is kept that it was set, + so that `tearDown` can restore its original value. + + """ + if name not in self.environ_undos: + self.environ_undos[name] = os.environ.get(name) + os.environ[name] = value + + def original_environ(self, name, if_missing=None): + """The environment variable `name` from when the test started.""" + if name in self.environ_undos: + ret = self.environ_undos[name] + else: + ret = os.environ.get(name) + if ret is None: + ret = if_missing + return ret + + def undo_environ(self): + """Undo all the changes made by `set_environ`.""" + for name, value in self.environ_undos.items(): + if value is None: + del os.environ[name] + else: + os.environ[name] = value + + def stdout(self): + """Return the data written to stdout during the test.""" + return self.captured_stdout.getvalue() + + def stderr(self): + """Return the data written to stderr during the test.""" + return self.captured_stderr.getvalue() + + def make_file(self, filename, text="", newline=None): + """Create a temp file. + + `filename` is the path to the file, including directories if desired, + and `text` is the content. If `newline` is provided, it is a string + that will be used as the line endings in the created file. + + Returns the path to the file. + + """ + # Tests that call `make_file` should be run in a temp environment. + assert self.run_in_temp_dir + text = textwrap.dedent(text) + if newline: + text = text.replace("\n", newline) + + # Make sure the directories are available. + dirs, _ = os.path.split(filename) + if dirs and not os.path.exists(dirs): + os.makedirs(dirs) + + # Create the file. + f = open(filename, 'wb') + try: + f.write(to_bytes(text)) + finally: + f.close() + + return filename + + def clean_local_file_imports(self): + """Clean up the results of calls to `import_local_file`. + + Use this if you need to `import_local_file` the same file twice in + one test. + + """ + # So that we can re-import files, clean them out first. + self.clean_modules() + # Also have to clean out the .pyc file, since the timestamp + # resolution is only one second, a changed file might not be + # picked up. + for pyc in glob.glob('*.pyc'): + os.remove(pyc) + if os.path.exists("__pycache__"): + shutil.rmtree("__pycache__") + + def import_local_file(self, modname): + """Import a local file as a module. + + Opens a file in the current directory named `modname`.py, imports it + as `modname`, and returns the module object. + + """ + modfile = modname + '.py' + f = open(modfile, 'r') + + for suff in imp.get_suffixes(): + if suff[0] == '.py': + break + try: + # pylint: disable=W0631 + # (Using possibly undefined loop variable 'suff') + mod = imp.load_module(modname, f, modfile, suff) + finally: + f.close() + return mod + + def start_import_stop(self, cov, modname): + """Start coverage, import a file, then stop coverage. + + `cov` is started and stopped, with an `import_local_file` of + `modname` in the middle. + + The imported module is returned. + + """ + cov.start() + try: # pragma: nested + # Import the python file, executing it. + mod = self.import_local_file(modname) + finally: # pragma: nested + # Stop Coverage. + cov.stop() + return mod + + def get_module_name(self): + """Return the module name to use for this test run.""" + # We append self.n because otherwise two calls in one test will use the + # same filename and whether the test works or not depends on the + # timestamps in the .pyc file, so it becomes random whether the second + # call will use the compiled version of the first call's code or not! + modname = 'coverage_test_' + self.noise + str(self.n) + self.n += 1 + return modname + + # Map chars to numbers for arcz_to_arcs + _arcz_map = {'.': -1} + _arcz_map.update(dict([(c, ord(c)-ord('0')) for c in '123456789'])) + _arcz_map.update(dict( + [(c, 10+ord(c)-ord('A')) for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'] + )) + + def arcz_to_arcs(self, arcz): + """Convert a compact textual representation of arcs to a list of pairs. + + The text has space-separated pairs of letters. Period is -1, 1-9 are + 1-9, A-Z are 10 through 36. The resulting list is sorted regardless of + the order of the input pairs. + + ".1 12 2." --> [(-1,1), (1,2), (2,-1)] + + Minus signs can be included in the pairs: + + "-11, 12, 2-5" --> [(-1,1), (1,2), (2,-5)] + + """ + arcs = [] + for pair in arcz.split(): + asgn = bsgn = 1 + if len(pair) == 2: + a,b = pair + else: + assert len(pair) == 3 + if pair[0] == '-': + _,a,b = pair + asgn = -1 + else: + assert pair[1] == '-' + a,_,b = pair + bsgn = -1 + arcs.append((asgn*self._arcz_map[a], bsgn*self._arcz_map[b])) + return sorted(arcs) + + def assertEqualArcs(self, a1, a2, msg=None): + """Assert that the arc lists `a1` and `a2` are equal.""" + # Make them into multi-line strings so we can see what's going wrong. + s1 = "\n".join([repr(a) for a in a1]) + "\n" + s2 = "\n".join([repr(a) for a in a2]) + "\n" + self.assertMultiLineEqual(s1, s2, msg) + + def check_coverage(self, text, lines=None, missing="", report="", + excludes=None, partials="", + arcz=None, arcz_missing="", arcz_unpredicted=""): + """Check the coverage measurement of `text`. + + The source `text` is run and measured. `lines` are the line numbers + that are executable, or a list of possible line numbers, any of which + could match. `missing` are the lines not executed, `excludes` are + regexes to match against for excluding lines, and `report` is the text + of the measurement report. + + For arc measurement, `arcz` is a string that can be decoded into arcs + in the code (see `arcz_to_arcs` for the encoding scheme), + `arcz_missing` are the arcs that are not executed, and + `arcs_unpredicted` are the arcs executed in the code, but not deducible + from the code. + + """ + # We write the code into a file so that we can import it. + # Coverage wants to deal with things as modules with file names. + modname = self.get_module_name() + + self.make_file(modname+".py", text) + + arcs = arcs_missing = arcs_unpredicted = None + if arcz is not None: + arcs = self.arcz_to_arcs(arcz) + arcs_missing = self.arcz_to_arcs(arcz_missing or "") + arcs_unpredicted = self.arcz_to_arcs(arcz_unpredicted or "") + + # Start up Coverage. + cov = coverage.coverage(branch=(arcs_missing is not None)) + cov.erase() + for exc in excludes or []: + cov.exclude(exc) + for par in partials or []: + cov.exclude(par, which='partial') + + mod = self.start_import_stop(cov, modname) + + # Clean up our side effects + del sys.modules[modname] + + # Get the analysis results, and check that they are right. + analysis = cov._analyze(mod) + if lines is not None: + if type(lines[0]) == type(1): + # lines is just a list of numbers, it must match the statements + # found in the code. + self.assertEqual(analysis.statements, lines) + else: + # lines is a list of possible line number lists, one of them + # must match. + for line_list in lines: + if analysis.statements == line_list: + break + else: + self.fail("None of the lines choices matched %r" % + analysis.statements + ) + + if type(missing) == type(""): + self.assertEqual(analysis.missing_formatted(), missing) + else: + for missing_list in missing: + if analysis.missing_formatted() == missing_list: + break + else: + self.fail("None of the missing choices matched %r" % + analysis.missing_formatted() + ) + + if arcs is not None: + self.assertEqualArcs( + analysis.arc_possibilities(), arcs, "Possible arcs differ" + ) + + if arcs_missing is not None: + self.assertEqualArcs( + analysis.arcs_missing(), arcs_missing, + "Missing arcs differ" + ) + + if arcs_unpredicted is not None: + self.assertEqualArcs( + analysis.arcs_unpredicted(), arcs_unpredicted, + "Unpredicted arcs differ" + ) + + if report: + frep = StringIO() + cov.report(mod, file=frep) + rep = " ".join(frep.getvalue().split("\n")[2].split()[1:]) + self.assertEqual(report, rep) + + def nice_file(self, *fparts): + """Canonicalize the filename composed of the parts in `fparts`.""" + fname = os.path.join(*fparts) + return os.path.normcase(os.path.abspath(os.path.realpath(fname))) + + def assert_same_files(self, flist1, flist2): + """Assert that `flist1` and `flist2` are the same set of file names.""" + flist1_nice = [self.nice_file(f) for f in flist1] + flist2_nice = [self.nice_file(f) for f in flist2] + self.assertSameElements(flist1_nice, flist2_nice) + + def assert_exists(self, fname): + """Assert that `fname` is a file that exists.""" + msg = "File %r should exist" % fname + self.assert_(os.path.exists(fname), msg) + + def assert_doesnt_exist(self, fname): + """Assert that `fname` is a file that doesn't exist.""" + msg = "File %r shouldn't exist" % fname + self.assert_(not os.path.exists(fname), msg) + + def command_line(self, args, ret=OK, _covpkg=None): + """Run `args` through the command line. + + Use this when you want to run the full coverage machinery, but in the + current process. Exceptions may be thrown from deep in the code. + Asserts that `ret` is returned by `CoverageScript.command_line`. + + Compare with `run_command`. + + Returns None. + + """ + script = coverage.CoverageScript(_covpkg=_covpkg) + ret_actual = script.command_line(shlex.split(args)) + self.assertEqual(ret_actual, ret) + + def run_command(self, cmd): + """Run the command-line `cmd` in a subprocess, and print its output. + + Use this when you need to test the process behavior of coverage. + + Compare with `command_line`. + + Returns the process' stdout text. + + """ + _, output = self.run_command_status(cmd) + return output + + def run_command_status(self, cmd, status=0): + """Run the command-line `cmd` in a subprocess, and print its output. + + Use this when you need to test the process behavior of coverage. + + Compare with `command_line`. + + Returns a pair: the process' exit status and stdout text. + + The `status` argument is returned as the status on older Pythons where + we can't get the actual exit status of the process. + + """ + # Add our test modules directory to PYTHONPATH. I'm sure there's too + # much path munging here, but... + here = os.path.dirname(self.nice_file(coverage.__file__, "..")) + testmods = self.nice_file(here, 'test/modules') + zipfile = self.nice_file(here, 'test/zipmods.zip') + pypath = os.getenv('PYTHONPATH', '') + if pypath: + pypath += os.pathsep + pypath += testmods + os.pathsep + zipfile + self.set_environ('PYTHONPATH', pypath) + + status, output = run_command(cmd, status=status) + print(output) + return status, output |