OLD | NEW |
(Empty) | |
| 1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
| 2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
| 3 |
| 4 """Base test case class for coverage.py testing.""" |
| 5 |
| 6 import datetime |
| 7 import glob |
| 8 import os |
| 9 import random |
| 10 import re |
| 11 import shlex |
| 12 import shutil |
| 13 import sys |
| 14 |
| 15 import coverage |
| 16 from coverage.backunittest import TestCase |
| 17 from coverage.backward import StringIO, import_local_file, string_class |
| 18 from coverage.cmdline import CoverageScript |
| 19 from coverage.debug import _TEST_NAME_FILE, DebugControl |
| 20 from coverage.test_helpers import ( |
| 21 EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, |
| 22 ) |
| 23 |
| 24 from nose.plugins.skip import SkipTest |
| 25 |
| 26 from tests.helpers import run_command |
| 27 |
| 28 |
| 29 # Status returns for the command line. |
| 30 OK, ERR = 0, 1 |
| 31 |
| 32 |
| 33 class CoverageTest( |
| 34 EnvironmentAwareMixin, |
| 35 StdStreamCapturingMixin, |
| 36 TempDirMixin, |
| 37 TestCase |
| 38 ): |
| 39 """A base class for coverage.py test cases.""" |
| 40 |
| 41 # Standard unittest setting: show me diffs even if they are very long. |
| 42 maxDiff = None |
| 43 |
| 44 # Tell newer unittest implementations to print long helpful messages. |
| 45 longMessage = True |
| 46 |
| 47 def setUp(self): |
| 48 super(CoverageTest, self).setUp() |
| 49 |
| 50 if _TEST_NAME_FILE: # pragma: debugging |
| 51 with open(_TEST_NAME_FILE, "w") as f: |
| 52 f.write("%s_%s" % ( |
| 53 self.__class__.__name__, self._testMethodName, |
| 54 )) |
| 55 |
| 56 def skip(self, reason): |
| 57 """Skip this test, and give a reason.""" |
| 58 self.class_behavior().skipped += 1 |
| 59 raise SkipTest(reason) |
| 60 |
| 61 def clean_local_file_imports(self): |
| 62 """Clean up the results of calls to `import_local_file`. |
| 63 |
| 64 Use this if you need to `import_local_file` the same file twice in |
| 65 one test. |
| 66 |
| 67 """ |
| 68 # So that we can re-import files, clean them out first. |
| 69 self.cleanup_modules() |
| 70 # Also have to clean out the .pyc file, since the timestamp |
| 71 # resolution is only one second, a changed file might not be |
| 72 # picked up. |
| 73 for pyc in glob.glob('*.pyc'): |
| 74 os.remove(pyc) |
| 75 if os.path.exists("__pycache__"): |
| 76 shutil.rmtree("__pycache__") |
| 77 |
| 78 def import_local_file(self, modname): |
| 79 """Import a local file as a module. |
| 80 |
| 81 Opens a file in the current directory named `modname`.py, imports it |
| 82 as `modname`, and returns the module object. |
| 83 |
| 84 """ |
| 85 return import_local_file(modname) |
| 86 |
| 87 def start_import_stop(self, cov, modname): |
| 88 """Start coverage, import a file, then stop coverage. |
| 89 |
| 90 `cov` is started and stopped, with an `import_local_file` of |
| 91 `modname` in the middle. |
| 92 |
| 93 The imported module is returned. |
| 94 |
| 95 """ |
| 96 cov.start() |
| 97 try: # pragma: nested |
| 98 # Import the Python file, executing it. |
| 99 mod = self.import_local_file(modname) |
| 100 finally: # pragma: nested |
| 101 # Stop coverage.py. |
| 102 cov.stop() |
| 103 return mod |
| 104 |
| 105 def get_module_name(self): |
| 106 """Return the module name to use for this test run.""" |
| 107 return 'coverage_test_' + str(random.random())[2:] |
| 108 |
| 109 # Map chars to numbers for arcz_to_arcs |
| 110 _arcz_map = {'.': -1} |
| 111 _arcz_map.update(dict((c, ord(c) - ord('0')) for c in '123456789')) |
| 112 _arcz_map.update(dict( |
| 113 (c, 10 + ord(c) - ord('A')) for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' |
| 114 )) |
| 115 |
| 116 def arcz_to_arcs(self, arcz): |
| 117 """Convert a compact textual representation of arcs to a list of pairs. |
| 118 |
| 119 The text has space-separated pairs of letters. Period is -1, 1-9 are |
| 120 1-9, A-Z are 10 through 36. The resulting list is sorted regardless of |
| 121 the order of the input pairs. |
| 122 |
| 123 ".1 12 2." --> [(-1,1), (1,2), (2,-1)] |
| 124 |
| 125 Minus signs can be included in the pairs: |
| 126 |
| 127 "-11, 12, 2-5" --> [(-1,1), (1,2), (2,-5)] |
| 128 |
| 129 """ |
| 130 arcs = [] |
| 131 for pair in arcz.split(): |
| 132 asgn = bsgn = 1 |
| 133 if len(pair) == 2: |
| 134 a, b = pair |
| 135 else: |
| 136 assert len(pair) == 3 |
| 137 if pair[0] == '-': |
| 138 _, a, b = pair |
| 139 asgn = -1 |
| 140 else: |
| 141 assert pair[1] == '-' |
| 142 a, _, b = pair |
| 143 bsgn = -1 |
| 144 arcs.append((asgn * self._arcz_map[a], bsgn * self._arcz_map[b])) |
| 145 return sorted(arcs) |
| 146 |
| 147 def assert_equal_args(self, a1, a2, msg=None): |
| 148 """Assert that the arc lists `a1` and `a2` are equal.""" |
| 149 # Make them into multi-line strings so we can see what's going wrong. |
| 150 s1 = "\n".join(repr(a) for a in a1) + "\n" |
| 151 s2 = "\n".join(repr(a) for a in a2) + "\n" |
| 152 self.assertMultiLineEqual(s1, s2, msg) |
| 153 |
| 154 def check_coverage( |
| 155 self, text, lines=None, missing="", report="", |
| 156 excludes=None, partials="", |
| 157 arcz=None, arcz_missing=None, arcz_unpredicted=None, |
| 158 arcs=None, arcs_missing=None, arcs_unpredicted=None, |
| 159 ): |
| 160 """Check the coverage measurement of `text`. |
| 161 |
| 162 The source `text` is run and measured. `lines` are the line numbers |
| 163 that are executable, or a list of possible line numbers, any of which |
| 164 could match. `missing` are the lines not executed, `excludes` are |
| 165 regexes to match against for excluding lines, and `report` is the text |
| 166 of the measurement report. |
| 167 |
| 168 For arc measurement, `arcz` is a string that can be decoded into arcs |
| 169 in the code (see `arcz_to_arcs` for the encoding scheme), |
| 170 `arcz_missing` are the arcs that are not executed, and |
| 171 `arcs_unpredicted` are the arcs executed in the code, but not deducible |
| 172 from the code. |
| 173 |
| 174 Returns the Coverage object, in case you want to poke at it some more. |
| 175 |
| 176 """ |
| 177 # We write the code into a file so that we can import it. |
| 178 # Coverage.py wants to deal with things as modules with file names. |
| 179 modname = self.get_module_name() |
| 180 |
| 181 self.make_file(modname + ".py", text) |
| 182 |
| 183 if arcs is None and arcz is not None: |
| 184 arcs = self.arcz_to_arcs(arcz) |
| 185 if arcs_missing is None and arcz_missing is not None: |
| 186 arcs_missing = self.arcz_to_arcs(arcz_missing) |
| 187 if arcs_unpredicted is None and arcz_unpredicted is not None: |
| 188 arcs_unpredicted = self.arcz_to_arcs(arcz_unpredicted) |
| 189 branch = any(x is not None for x in [arcs, arcs_missing, arcs_unpredicte
d]) |
| 190 |
| 191 # Start up coverage.py. |
| 192 cov = coverage.Coverage(branch=branch) |
| 193 cov.erase() |
| 194 for exc in excludes or []: |
| 195 cov.exclude(exc) |
| 196 for par in partials or []: |
| 197 cov.exclude(par, which='partial') |
| 198 |
| 199 mod = self.start_import_stop(cov, modname) |
| 200 |
| 201 # Clean up our side effects |
| 202 del sys.modules[modname] |
| 203 |
| 204 # Get the analysis results, and check that they are right. |
| 205 analysis = cov._analyze(mod) |
| 206 statements = sorted(analysis.statements) |
| 207 if lines is not None: |
| 208 if isinstance(lines[0], int): |
| 209 # lines is just a list of numbers, it must match the statements |
| 210 # found in the code. |
| 211 self.assertEqual(statements, lines) |
| 212 else: |
| 213 # lines is a list of possible line number lists, one of them |
| 214 # must match. |
| 215 for line_list in lines: |
| 216 if statements == line_list: |
| 217 break |
| 218 else: |
| 219 self.fail("None of the lines choices matched %r" % statement
s) |
| 220 |
| 221 missing_formatted = analysis.missing_formatted() |
| 222 if isinstance(missing, string_class): |
| 223 self.assertEqual(missing_formatted, missing) |
| 224 else: |
| 225 for missing_list in missing: |
| 226 if missing_formatted == missing_list: |
| 227 break |
| 228 else: |
| 229 self.fail("None of the missing choices matched %r" % missing
_formatted) |
| 230 |
| 231 if arcs is not None: |
| 232 self.assert_equal_args(analysis.arc_possibilities(), arcs, "Possible
arcs differ") |
| 233 |
| 234 if arcs_missing is not None: |
| 235 self.assert_equal_args( |
| 236 analysis.arcs_missing(), arcs_missing, |
| 237 "Missing arcs differ" |
| 238 ) |
| 239 |
| 240 if arcs_unpredicted is not None: |
| 241 self.assert_equal_args( |
| 242 analysis.arcs_unpredicted(), arcs_unpredicted, |
| 243 "Unpredicted arcs differ" |
| 244 ) |
| 245 |
| 246 if report: |
| 247 frep = StringIO() |
| 248 cov.report(mod, file=frep) |
| 249 rep = " ".join(frep.getvalue().split("\n")[2].split()[1:]) |
| 250 self.assertEqual(report, rep) |
| 251 |
| 252 return cov |
| 253 |
| 254 def nice_file(self, *fparts): |
| 255 """Canonicalize the file name composed of the parts in `fparts`.""" |
| 256 fname = os.path.join(*fparts) |
| 257 return os.path.normcase(os.path.abspath(os.path.realpath(fname))) |
| 258 |
| 259 def assert_same_files(self, flist1, flist2): |
| 260 """Assert that `flist1` and `flist2` are the same set of file names.""" |
| 261 flist1_nice = [self.nice_file(f) for f in flist1] |
| 262 flist2_nice = [self.nice_file(f) for f in flist2] |
| 263 self.assertCountEqual(flist1_nice, flist2_nice) |
| 264 |
| 265 def assert_exists(self, fname): |
| 266 """Assert that `fname` is a file that exists.""" |
| 267 msg = "File %r should exist" % fname |
| 268 self.assertTrue(os.path.exists(fname), msg) |
| 269 |
| 270 def assert_doesnt_exist(self, fname): |
| 271 """Assert that `fname` is a file that doesn't exist.""" |
| 272 msg = "File %r shouldn't exist" % fname |
| 273 self.assertTrue(not os.path.exists(fname), msg) |
| 274 |
| 275 def assert_starts_with(self, s, prefix, msg=None): |
| 276 """Assert that `s` starts with `prefix`.""" |
| 277 if not s.startswith(prefix): |
| 278 self.fail(msg or ("%r doesn't start with %r" % (s, prefix))) |
| 279 |
| 280 def assert_recent_datetime(self, dt, seconds=10, msg=None): |
| 281 """Assert that `dt` marks a time at most `seconds` seconds ago.""" |
| 282 age = datetime.datetime.now() - dt |
| 283 # Python2.6 doesn't have total_seconds :( |
| 284 self.assertEqual(age.days, 0, msg) |
| 285 self.assertGreaterEqual(age.seconds, 0, msg) |
| 286 self.assertLessEqual(age.seconds, seconds, msg) |
| 287 |
| 288 def command_line(self, args, ret=OK, _covpkg=None): |
| 289 """Run `args` through the command line. |
| 290 |
| 291 Use this when you want to run the full coverage machinery, but in the |
| 292 current process. Exceptions may be thrown from deep in the code. |
| 293 Asserts that `ret` is returned by `CoverageScript.command_line`. |
| 294 |
| 295 Compare with `run_command`. |
| 296 |
| 297 Returns None. |
| 298 |
| 299 """ |
| 300 script = CoverageScript(_covpkg=_covpkg) |
| 301 ret_actual = script.command_line(shlex.split(args)) |
| 302 self.assertEqual(ret_actual, ret) |
| 303 |
| 304 def run_command(self, cmd): |
| 305 """Run the command-line `cmd` in a sub-process, and print its output. |
| 306 |
| 307 Use this when you need to test the process behavior of coverage. |
| 308 |
| 309 Compare with `command_line`. |
| 310 |
| 311 Returns the process' stdout text. |
| 312 |
| 313 """ |
| 314 # Running Python sub-processes can be tricky. Use the real name of our |
| 315 # own executable. So "python foo.py" might get executed as |
| 316 # "python3.3 foo.py". This is important because Python 3.x doesn't |
| 317 # install as "python", so you might get a Python 2 executable instead |
| 318 # if you don't use the executable's basename. |
| 319 if cmd.startswith("python "): |
| 320 cmd = os.path.basename(sys.executable) + cmd[6:] |
| 321 |
| 322 _, output = self.run_command_status(cmd) |
| 323 return output |
| 324 |
| 325 def run_command_status(self, cmd): |
| 326 """Run the command-line `cmd` in a sub-process, and print its output. |
| 327 |
| 328 Use this when you need to test the process behavior of coverage. |
| 329 |
| 330 Compare with `command_line`. |
| 331 |
| 332 Returns a pair: the process' exit status and stdout text. |
| 333 |
| 334 """ |
| 335 # Add our test modules directory to PYTHONPATH. I'm sure there's too |
| 336 # much path munging here, but... |
| 337 here = os.path.dirname(self.nice_file(coverage.__file__, "..")) |
| 338 testmods = self.nice_file(here, 'tests/modules') |
| 339 zipfile = self.nice_file(here, 'tests/zipmods.zip') |
| 340 pypath = os.getenv('PYTHONPATH', '') |
| 341 if pypath: |
| 342 pypath += os.pathsep |
| 343 pypath += testmods + os.pathsep + zipfile |
| 344 self.set_environ('PYTHONPATH', pypath) |
| 345 |
| 346 status, output = run_command(cmd) |
| 347 print(output) |
| 348 return status, output |
| 349 |
| 350 def report_from_command(self, cmd): |
| 351 """Return the report from the `cmd`, with some convenience added.""" |
| 352 report = self.run_command(cmd).replace('\\', '/') |
| 353 self.assertNotIn("error", report.lower()) |
| 354 return report |
| 355 |
| 356 def report_lines(self, report): |
| 357 """Return the lines of the report, as a list.""" |
| 358 lines = report.split('\n') |
| 359 self.assertEqual(lines[-1], "") |
| 360 return lines[:-1] |
| 361 |
| 362 def line_count(self, report): |
| 363 """How many lines are in `report`?""" |
| 364 return len(self.report_lines(report)) |
| 365 |
| 366 def squeezed_lines(self, report): |
| 367 """Return a list of the lines in report, with the spaces squeezed.""" |
| 368 lines = self.report_lines(report) |
| 369 return [re.sub(r"\s+", " ", l.strip()) for l in lines] |
| 370 |
| 371 def last_line_squeezed(self, report): |
| 372 """Return the last line of `report` with the spaces squeezed down.""" |
| 373 return self.squeezed_lines(report)[-1] |
| 374 |
| 375 |
| 376 class DebugControlString(DebugControl): |
| 377 """A `DebugControl` that writes to a StringIO, for testing.""" |
| 378 def __init__(self, options): |
| 379 super(DebugControlString, self).__init__(options, StringIO()) |
| 380 |
| 381 def get_output(self): |
| 382 """Get the output text from the `DebugControl`.""" |
| 383 return self.output.getvalue() |
OLD | NEW |