summaryrefslogtreecommitdiff
path: root/test/coveragetest.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/coveragetest.py')
-rw-r--r--test/coveragetest.py304
1 files changed, 198 insertions, 106 deletions
diff --git a/test/coveragetest.py b/test/coveragetest.py
index d1631a3d..9b85b034 100644
--- a/test/coveragetest.py
+++ b/test/coveragetest.py
@@ -1,11 +1,11 @@
"""Base test case class for coverage testing."""
-import imp, os, random, re, shutil, sys, tempfile, textwrap, unittest
+import imp, os, random, shlex, shutil, sys, tempfile, textwrap
import coverage
-from coverage.backward import set, sorted, StringIO # pylint: disable-msg=W0622
+from coverage.backward import sorted, StringIO # pylint: disable-msg=W0622
from backtest import run_command
-
+from backunittest import TestCase
class Tee(object):
"""A file-like that writes to all the file-likes it has."""
@@ -13,18 +13,21 @@ class Tee(object):
def __init__(self, *files):
"""Make a Tee that writes to all the files in `files.`"""
self.files = files
-
+
def write(self, data):
"""Write `data` to all the files."""
for f in self.files:
f.write(data)
-class CoverageTest(unittest.TestCase):
+# 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):
if self.run_in_temp_dir:
# Create a temporary directory.
@@ -34,49 +37,106 @@ class CoverageTest(unittest.TestCase):
os.makedirs(self.temp_dir)
self.old_dir = os.getcwd()
os.chdir(self.temp_dir)
-
- # Preserve changes to PYTHONPATH.
- self.old_pypath = os.environ.get('PYTHONPATH', '')
-
+
+
# Modules should be importable from this temp directory.
self.old_syspath = sys.path[:]
sys.path.insert(0, '')
-
+
# Keep a counter to make every call to check_coverage unique.
self.n = 0
- # Use a Tee to capture stdout.
+ # 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):
if self.run_in_temp_dir:
- # Restore the original sys.path and PYTHONPATH
+ # Restore the original sys.path.
sys.path = self.old_syspath
- os.environ['PYTHONPATH'] = self.old_pypath
-
+
# Get rid of the temporary directory.
os.chdir(self.old_dir)
shutil.rmtree(self.temp_root)
-
- # Restore stdout.
+
+ # Restore the environment.
+ self.undo_environ()
+
+ # Restore stdout and stderr
sys.stdout = self.old_stdout
+ sys.stderr = self.old_stderr
+
+ # 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):
"""Create a temp file.
-
- `filename` is the file name, and `text` is the content.
-
+
+ `filename` is the path to the file, including directories if desired,
+ and `text` is the content.
+
"""
# Tests that call `make_file` should be run in a temp environment.
assert self.run_in_temp_dir
text = textwrap.dedent(text)
-
+
+ # 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, 'w')
f.write(text)
@@ -86,7 +146,7 @@ class CoverageTest(unittest.TestCase):
"""Import the module named modname, and return the module object."""
modfile = modname + '.py'
f = open(modfile, 'r')
-
+
for suff in imp.get_suffixes():
if suff[0] == '.py':
break
@@ -107,49 +167,73 @@ class CoverageTest(unittest.TestCase):
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 a,b in arcz.split():
- arcs.append((self._arcz_map[a], self._arcz_map[b]))
+ 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="", excludes=None,
report="", 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, `missing` are the lines not executed, `excludes`
- are regexes to match against for excluding lines, and `report` is
- the text of the measurement report.
-
+ 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
@@ -157,7 +241,7 @@ class CoverageTest(unittest.TestCase):
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()
@@ -165,11 +249,12 @@ class CoverageTest(unittest.TestCase):
cov.exclude(exc)
cov.start()
- # Import the python file, executing it.
- mod = self.import_module(modname)
-
- # Stop Coverage.
- cov.stop()
+ try: # pragma: recursive coverage
+ # Import the python file, executing it.
+ mod = self.import_module(modname)
+ finally: # pragma: recursive coverage
+ # Stop Coverage.
+ cov.stop()
# Clean up our side effects
del sys.modules[modname]
@@ -178,8 +263,12 @@ class CoverageTest(unittest.TestCase):
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
@@ -188,26 +277,33 @@ class CoverageTest(unittest.TestCase):
analysis.statements
)
- if missing is not None:
- if type(missing) == type(""):
- self.assertEqual(analysis.missing_formatted(), missing)
+ if type(missing) == type(""):
+ self.assertEqual(analysis.missing_formatted(), missing)
+ else:
+ for missing_list in missing:
+ if analysis.missing_formatted() == missing_list:
+ break
else:
- for missing_list in missing:
- if analysis.missing == missing_list:
- break
- else:
- self.fail("None of the missing choices matched %r" %
- analysis.missing_formatted()
- )
+ self.fail("None of the missing choices matched %r" %
+ analysis.missing_formatted()
+ )
if arcs is not None:
- self.assertEqual(analysis.arc_possibilities(), arcs)
+ self.assertEqualArcs(
+ analysis.arc_possibilities(), arcs, "Possible arcs differ"
+ )
if arcs_missing is not None:
- self.assertEqual(analysis.arcs_missing(), arcs_missing)
+ self.assertEqualArcs(
+ analysis.arcs_missing(), arcs_missing,
+ "Missing arcs differ"
+ )
if arcs_unpredicted is not None:
- self.assertEqual(analysis.arcs_unpredicted(), arcs_unpredicted)
+ self.assertEqualArcs(
+ analysis.arcs_unpredicted(), arcs_unpredicted,
+ "Unpredicted arcs differ"
+ )
if report:
frep = StringIO()
@@ -215,68 +311,64 @@ class CoverageTest(unittest.TestCase):
rep = " ".join(frep.getvalue().split("\n")[2].split()[1:])
self.assertEqual(report, rep)
- def assert_raises_msg(self, excClass, msg, callableObj, *args, **kwargs):
- """ Just like unittest.TestCase.assertRaises,
- but checks that the message is right too.
- """
- try:
- callableObj(*args, **kwargs)
- except excClass:
- _, exc, _ = sys.exc_info()
- excMsg = str(exc)
- if not msg:
- # No message provided: it passes.
- return #pragma: no cover
- elif excMsg == msg:
- # Message provided, and we got the right message: it passes.
- return
- else: #pragma: no cover
- # Message provided, and it didn't match: fail!
- raise self.failureException(
- "Right exception, wrong message: got '%s' expected '%s'" %
- (excMsg, msg)
- )
- # No need to catch other exceptions: They'll fail the test all by
- # themselves!
- else: #pragma: no cover
- if hasattr(excClass,'__name__'):
- excName = excClass.__name__
- else:
- excName = str(excClass)
- raise self.failureException(
- "Expected to raise %s, didn't get an exception at all" %
- excName
- )
-
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 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`, print its output.
+ """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 = self.old_pypath
+ pypath = self.original_environ('PYTHONPATH', "")
if pypath:
pypath += os.pathsep
pypath += testmods + os.pathsep + zipfile
- os.environ['PYTHONPATH'] = pypath
-
- _, output = run_command(cmd)
- print(output)
- return output
+ self.set_environ('PYTHONPATH', pypath)
- def assert_equal_sets(self, s1, s2):
- """Assert that the two arguments are equal as sets."""
- self.assertEqual(set(s1), set(s2))
-
- def assert_matches(self, s, regex):
- """Assert that `s` matches `regex`."""
- m = re.search(regex, s)
- if not m:
- raise self.failureException("%r doesn't match %r" % (s, regex))
+ status, output = run_command(cmd, status=status)
+ print(output)
+ return status, output