diff options
Diffstat (limited to 'tests')
42 files changed, 1999 insertions, 849 deletions
diff --git a/tests/__init__.py b/tests/__init__.py index 5a0e30f..1ff1e1b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,4 @@ -"""Automated tests. Run with nosetests.""" +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Automated tests. Run with pytest.""" diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 9410e07..0e6131f 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -5,13 +5,12 @@ import contextlib import datetime -import glob import os import random import re import shlex -import shutil import sys +import types from unittest_mixins import ( EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, @@ -19,24 +18,51 @@ from unittest_mixins import ( ) import coverage -from coverage.backunittest import TestCase +from coverage import env +from coverage.backunittest import TestCase, unittest from coverage.backward import StringIO, import_local_file, string_class, shlex_quote from coverage.cmdline import CoverageScript -from coverage.debug import _TEST_NAME_FILE, DebugControl +from coverage.debug import _TEST_NAME_FILE +from coverage.misc import StopEverything -from tests.helpers import run_command +from tests.helpers import run_command, SuperModuleCleaner # Status returns for the command line. OK, ERR = 0, 1 +def convert_skip_exceptions(method): + """A decorator for test methods to convert StopEverything to SkipTest.""" + def wrapper(*args, **kwargs): + """Run the test method, and convert exceptions.""" + try: + result = method(*args, **kwargs) + except StopEverything: + raise unittest.SkipTest("StopEverything!") + return result + return wrapper + + +class SkipConvertingMetaclass(type): + """Decorate all test methods to convert StopEverything to SkipTest.""" + def __new__(mcs, name, bases, attrs): + for attr_name, attr_value in attrs.items(): + if attr_name.startswith('test_') and isinstance(attr_value, types.FunctionType): + attrs[attr_name] = convert_skip_exceptions(attr_value) + + return super(SkipConvertingMetaclass, mcs).__new__(mcs, name, bases, attrs) + + +CoverageTestMethodsMixin = SkipConvertingMetaclass('CoverageTestMethodsMixin', (), {}) + class CoverageTest( EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, DelayedAssertionMixin, - TestCase + CoverageTestMethodsMixin, + TestCase, ): """A base class for coverage.py test cases.""" @@ -46,9 +72,14 @@ class CoverageTest( # Tell newer unittest implementations to print long helpful messages. longMessage = True + # Let stderr go to stderr, pytest will capture it for us. + show_stderr = True + def setUp(self): super(CoverageTest, self).setUp() + self.module_cleaner = SuperModuleCleaner() + # Attributes for getting info about what happened. self.last_command_status = None self.last_command_output = None @@ -67,25 +98,7 @@ class CoverageTest( one test. """ - # So that we can re-import files, clean them out first. - self.cleanup_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, modfile=None): - """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` is the file to - import if it isn't in the current directory. - - """ - return import_local_file(modname, modfile) + self.module_cleaner.clean_local_file_imports() def start_import_stop(self, cov, modname, modfile=None): """Start coverage, import a file, then stop coverage. @@ -100,7 +113,7 @@ class CoverageTest( cov.start() try: # pragma: nested # Import the Python file, executing it. - mod = self.import_local_file(modname, modfile) + mod = import_local_file(modname, modfile) finally: # pragma: nested # Stop coverage.py. cov.stop() @@ -237,17 +250,17 @@ class CoverageTest( with self.delayed_assertions(): self.assert_equal_args( analysis.arc_possibilities(), arcs, - "Possible arcs differ", + "Possible arcs differ: minus is actual, plus is expected" ) self.assert_equal_args( analysis.arcs_missing(), arcs_missing, - "Missing arcs differ" + "Missing arcs differ: minus is actual, plus is expected" ) self.assert_equal_args( analysis.arcs_unpredicted(), arcs_unpredicted, - "Unpredicted arcs differ" + "Unpredicted arcs differ: minus is actual, plus is expected" ) if report: @@ -259,11 +272,27 @@ class CoverageTest( return cov @contextlib.contextmanager - def assert_warnings(self, cov, warnings): - """A context manager to check that particular warnings happened in `cov`.""" + def assert_warnings(self, cov, warnings, not_warnings=()): + """A context manager to check that particular warnings happened in `cov`. + + `cov` is a Coverage instance. `warnings` is a list of regexes. Every + regex must match a warning that was issued by `cov`. It is OK for + extra warnings to be issued by `cov` that are not matched by any regex. + Warnings that are disabled are still considered issued by this function. + + `not_warnings` is a list of regexes that must not appear in the + warnings. This is only checked if there are some positive warnings to + test for in `warnings`. + + If `warnings` is empty, then `cov` is not allowed to issue any + warnings. + + """ saved_warnings = [] - def capture_warning(msg): + def capture_warning(msg, slug=None): """A fake implementation of Coverage._warn, to capture warnings.""" + if slug: + msg = "%s (%s)" % (msg, slug) saved_warnings.append(msg) original_warn = cov._warn @@ -274,12 +303,22 @@ class CoverageTest( except: raise else: - for warning_regex in warnings: - for saved in saved_warnings: - if re.search(warning_regex, saved): - break - else: - self.fail("Didn't find warning %r in %r" % (warning_regex, saved_warnings)) + if warnings: + for warning_regex in warnings: + for saved in saved_warnings: + if re.search(warning_regex, saved): + break + else: + self.fail("Didn't find warning %r in %r" % (warning_regex, saved_warnings)) + for warning_regex in not_warnings: + for saved in saved_warnings: + if re.search(warning_regex, saved): + self.fail("Found warning %r in %r" % (warning_regex, saved_warnings)) + else: + # No warnings expected. Raise if any warnings happened. + if saved_warnings: + self.fail("Unexpected warnings: %r" % (saved_warnings,)) + finally: cov._warn = original_warn def nice_file(self, *fparts): @@ -328,8 +367,7 @@ class CoverageTest( Returns None. """ - script = CoverageScript(_covpkg=_covpkg) - ret_actual = script.command_line(shlex.split(args)) + ret_actual = command_line(args, _covpkg=_covpkg) self.assertEqual(ret_actual, ret) coverage_command = "coverage" @@ -373,7 +411,7 @@ class CoverageTest( """ # Make sure "python" and "coverage" mean specifically what we want # them to mean. - split_commandline = cmd.split(" ", 1) + split_commandline = cmd.split() command_name = split_commandline[0] command_args = split_commandline[1:] @@ -383,30 +421,49 @@ class CoverageTest( # get executed as "python3.3 foo.py". This is important because # Python 3.x doesn't install as "python", so you might get a Python # 2 executable instead if you don't use the executable's basename. - command_name = os.path.basename(sys.executable) + command_words = [os.path.basename(sys.executable)] + + elif command_name == "coverage": + if env.JYTHON: # pragma: only jython + # Jython can't do reporting, so let's skip the test now. + if command_args and command_args[0] in ('report', 'html', 'xml', 'annotate'): + self.skipTest("Can't run reporting commands in Jython") + # Jython can't run "coverage" as a command because the shebang + # refers to another shebang'd Python script. So run them as + # modules. + command_words = "jython -m coverage".split() + else: + # The invocation requests the Coverage.py program. Substitute the + # actual Coverage.py main command name. + command_words = [self.coverage_command] - if command_name == "coverage": - # The invocation requests the Coverage.py program. Substitute the - # actual Coverage.py main command name. - command_name = self.coverage_command + else: + command_words = [command_name] - cmd = " ".join([shlex_quote(command_name)] + command_args) + cmd = " ".join([shlex_quote(w) for w in command_words] + command_args) # 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, 'tests/modules') - zipfile = self.nice_file(here, 'tests/zipmods.zip') - pypath = os.getenv('PYTHONPATH', '') + pythonpath_name = "PYTHONPATH" + if env.JYTHON: + pythonpath_name = "JYTHONPATH" # pragma: only jython + + testmods = self.nice_file(self.working_root(), 'tests/modules') + zipfile = self.nice_file(self.working_root(), 'tests/zipmods.zip') + pypath = os.getenv(pythonpath_name, '') if pypath: pypath += os.pathsep pypath += testmods + os.pathsep + zipfile - self.set_environ('PYTHONPATH', pypath) + self.set_environ(pythonpath_name, pypath) self.last_command_status, self.last_command_output = run_command(cmd) print(self.last_command_output) return self.last_command_status, self.last_command_output + def working_root(self): + """Where is the root of the coverage.py working tree?""" + return os.path.dirname(self.nice_file(coverage.__file__, "..")) + def report_from_command(self, cmd): """Return the report from the `cmd`, with some convenience added.""" report = self.run_command(cmd).replace('\\', '/') @@ -433,11 +490,14 @@ class CoverageTest( return self.squeezed_lines(report)[-1] -class DebugControlString(DebugControl): - """A `DebugControl` that writes to a StringIO, for testing.""" - def __init__(self, options): - super(DebugControlString, self).__init__(options, StringIO()) +def command_line(args, **kwargs): + """Run `args` through the CoverageScript command line. + + `kwargs` are the keyword arguments to the CoverageScript constructor. + + Returns the return code from CoverageScript.command_line. - def get_output(self): - """Get the output text from the `DebugControl`.""" - return self.output.getvalue() + """ + script = CoverageScript(**kwargs) + ret = script.command_line(shlex.split(args)) + return ret diff --git a/tests/farm/annotate/run_encodings.py b/tests/farm/annotate/run_encodings.py index 527cd88..46d8c64 100644 --- a/tests/farm/annotate/run_encodings.py +++ b/tests/farm/annotate/run_encodings.py @@ -1,10 +1,10 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -copy("src", "out") +copy("src", "out_encodings") run(""" coverage run utf8.py coverage annotate utf8.py - """, rundir="out") -compare("out", "gold_encodings", "*,cover") -clean("out") + """, rundir="out_encodings") +compare("out_encodings", "gold_encodings", "*,cover") +clean("out_encodings") diff --git a/tests/farm/html/gold_x_xml/coverage.xml b/tests/farm/html/gold_x_xml/coverage.xml index b3e9854..162824a 100644 --- a/tests/farm/html/gold_x_xml/coverage.xml +++ b/tests/farm/html/gold_x_xml/coverage.xml @@ -1,7 +1,7 @@ <?xml version="1.0" ?> -<coverage branch-rate="0" line-rate="0.6667" timestamp="1437745880639" version="4.0a7"> +<coverage branch-rate="0" branches-covered="0" branches-valid="0" complexity="0" line-rate="0.6667" lines-covered="2" lines-valid="3" timestamp="1437745880639" version="4.0a7"> <!-- Generated by coverage.py: https://coverage.readthedocs.io/en/coverage-4.0a7 --> - <!-- Based on https://raw.githubusercontent.com/cobertura/web/f0366e5e2cf18f111cbd61fc34ef720a6584ba02/htdocs/xml/coverage-03.dtd --> + <!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd --> <sources> <source>/Users/ned/coverage/trunk/tests/farm/html/src</source> </sources> diff --git a/tests/farm/html/gold_y_xml_branch/coverage.xml b/tests/farm/html/gold_y_xml_branch/coverage.xml index d8ff0bb..bcf1137 100644 --- a/tests/farm/html/gold_y_xml_branch/coverage.xml +++ b/tests/farm/html/gold_y_xml_branch/coverage.xml @@ -1,7 +1,7 @@ <?xml version="1.0" ?> -<coverage branch-rate="0.5" line-rate="0.8" timestamp="1437745880882" version="4.0a7"> +<coverage branch-rate="0.5" branches-covered="1" branches-valid="2" complexity="0" line-rate="0.8" lines-covered="4" lines-valid="5" timestamp="1437745880882" version="4.0a7"> <!-- Generated by coverage.py: https://coverage.readthedocs.io/en/coverage-4.0a7 --> - <!-- Based on https://raw.githubusercontent.com/cobertura/web/f0366e5e2cf18f111cbd61fc34ef720a6584ba02/htdocs/xml/coverage-03.dtd --> + <!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd --> <sources> <source>/Users/ned/coverage/trunk/tests/farm/html/src</source> </sources> diff --git a/tests/farm/html/src/partial.ini b/tests/farm/html/src/partial.ini new file mode 100644 index 0000000..cdb241b --- /dev/null +++ b/tests/farm/html/src/partial.ini @@ -0,0 +1,9 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +[run] +branch = True + +[report] +exclude_lines = + raise AssertionError diff --git a/tests/farm/html/src/partial.py b/tests/farm/html/src/partial.py index 66dddac..0f8fbe3c 100644 --- a/tests/farm/html/src/partial.py +++ b/tests/farm/html/src/partial.py @@ -1,9 +1,9 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -# partial branches +# partial branches and excluded lines -a = 3 +a = 6 while True: break @@ -18,4 +18,7 @@ if 0: never_happen() if 1: - a = 13 + a = 21 + +if a == 23: + raise AssertionError("Can't") diff --git a/tests/farm/run/run_chdir.py b/tests/farm/run/run_chdir.py index 9e3c751..1da4e9a 100644 --- a/tests/farm/run/run_chdir.py +++ b/tests/farm/run/run_chdir.py @@ -1,15 +1,15 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -copy("src", "out") +copy("src", "out_chdir") run(""" coverage run chdir.py coverage report - """, rundir="out", outfile="stdout.txt") -contains("out/stdout.txt", + """, rundir="out_chdir", outfile="stdout.txt") +contains("out_chdir/stdout.txt", "Line One", "Line Two", "chdir" ) -doesnt_contain("out/stdout.txt", "No such file or directory") -clean("out") +doesnt_contain("out_chdir/stdout.txt", "No such file or directory") +clean("out_chdir") diff --git a/tests/farm/run/run_timid.py b/tests/farm/run/run_timid.py index a632cea..0370cf8 100644 --- a/tests/farm/run/run_timid.py +++ b/tests/farm/run/run_timid.py @@ -17,16 +17,16 @@ import os if os.environ.get('COVERAGE_COVERAGE', ''): skip("Can't test timid during coverage measurement.") -copy("src", "out") +copy("src", "out_timid") run(""" python showtrace.py none coverage run showtrace.py regular coverage run --timid showtrace.py timid - """, rundir="out", outfile="showtraceout.txt") + """, rundir="out_timid", outfile="showtraceout.txt") # When running without coverage, no trace function # When running timidly, the trace function is always Python. -contains("out/showtraceout.txt", +contains("out_timid/showtraceout.txt", "none None", "timid PyTracer", ) @@ -34,10 +34,10 @@ contains("out/showtraceout.txt", if os.environ.get('COVERAGE_TEST_TRACER', 'c') == 'c': # If the C trace function is being tested, then regular running should have # the C function, which registers itself as f_trace. - contains("out/showtraceout.txt", "regular CTracer") + contains("out_timid/showtraceout.txt", "regular CTracer") else: # If the Python trace function is being tested, then regular running will # also show the Python function. - contains("out/showtraceout.txt", "regular PyTracer") + contains("out_timid/showtraceout.txt", "regular PyTracer") -clean("out") +clean("out_timid") diff --git a/tests/farm/run/run_xxx.py b/tests/farm/run/run_xxx.py index 62a862e..1db5b0d 100644 --- a/tests/farm/run/run_xxx.py +++ b/tests/farm/run/run_xxx.py @@ -1,15 +1,15 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -copy("src", "out") +copy("src", "out_xxx") run(""" coverage run xxx coverage report - """, rundir="out", outfile="stdout.txt") -contains("out/stdout.txt", + """, rundir="out_xxx", outfile="stdout.txt") +contains("out_xxx/stdout.txt", "xxx: 3 4 0 7", "\nxxx ", # The reporting line for xxx " 7 1 86%" # The reporting data for xxx ) -doesnt_contain("out/stdout.txt", "No such file or directory") -clean("out") +doesnt_contain("out_xxx/stdout.txt", "No such file or directory") +clean("out_xxx") diff --git a/tests/goldtest.py b/tests/goldtest.py index 27a082e..baaa8f0 100644 --- a/tests/goldtest.py +++ b/tests/goldtest.py @@ -38,5 +38,5 @@ class CoverageGoldTest(CoverageTest): # beginning of the test. clean(the_dir) - if not os.environ.get("COVERAGE_KEEP_OUTPUT"): # pragma: partial + if not os.environ.get("COVERAGE_KEEP_OUTPUT"): # pragma: part covered self.addCleanup(clean, the_dir) diff --git a/tests/helpers.py b/tests/helpers.py index f4bff2b..f10169a 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -3,11 +3,18 @@ """Helpers for coverage.py tests.""" +import glob +import itertools import os +import re +import shutil import subprocess import sys +from unittest_mixins import ModuleCleaner + from coverage import env +from coverage.backward import invalidate_import_caches, unicode_class from coverage.misc import output_encoding @@ -17,16 +24,14 @@ def run_command(cmd): Returns the exit status code and the combined stdout and stderr. """ - if env.PY2 and isinstance(cmd, unicode): + if env.PY2 and isinstance(cmd, unicode_class): cmd = cmd.encode(sys.getfilesystemencoding()) # In some strange cases (PyPy3 in a virtualenv!?) the stdout encoding of # the subprocess is set incorrectly to ascii. Use an environment variable # to force the encoding to be the same as ours. sub_env = dict(os.environ) - encoding = output_encoding() - if encoding: - sub_env['PYTHONIOENCODING'] = encoding + sub_env['PYTHONIOENCODING'] = output_encoding() proc = subprocess.Popen( cmd, @@ -53,11 +58,19 @@ class CheckUniqueFilenames(object): self.wrapped = wrapped @classmethod - def hook(cls, cov, method_name): - """Replace a method with our checking wrapper.""" - method = getattr(cov, method_name) + def hook(cls, obj, method_name): + """Replace a method with our checking wrapper. + + The method must take a string as a first argument. That argument + will be checked for uniqueness across all the calls to this method. + + The values don't have to be file names actually, just strings, but + we only use it for filename arguments. + + """ + method = getattr(obj, method_name) hook = cls(method) - setattr(cov, method_name, hook.wrapper) + setattr(obj, method_name, hook.wrapper) return hook def wrapper(self, filename, *args, **kwargs): @@ -68,3 +81,50 @@ class CheckUniqueFilenames(object): self.filenames.add(filename) ret = self.wrapped(filename, *args, **kwargs) return ret + + +def re_lines(text, pat, match=True): + """Return the text of lines that match `pat` in the string `text`. + + If `match` is false, the selection is inverted: only the non-matching + lines are included. + + Returns a string, the text of only the selected lines. + + """ + return "".join(l for l in text.splitlines(True) if bool(re.search(pat, l)) == match) + + +def re_line(text, pat): + """Return the one line in `text` that matches regex `pat`. + + Raises an AssertionError if more than one, or less than one, line matches. + + """ + lines = re_lines(text, pat).splitlines() + assert len(lines) == 1 + return lines[0] + + +class SuperModuleCleaner(ModuleCleaner): + """Remember the state of sys.modules and restore it later.""" + + 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.cleanup_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 itertools.chain(glob.glob('*.pyc'), glob.glob('*$py.class')): + os.remove(pyc) + if os.path.exists("__pycache__"): + shutil.rmtree("__pycache__") + + invalidate_import_caches() diff --git a/tests/modules/process_test/try_execfile.py b/tests/modules/process_test/try_execfile.py index 7090507..ec7dcbe 100644 --- a/tests/modules/process_test/try_execfile.py +++ b/tests/modules/process_test/try_execfile.py @@ -20,7 +20,10 @@ differences and get a clean diff. """ -import json, os, sys +import itertools +import json +import os +import sys # sys.path varies by execution environments. Coverage.py uses setuptools to # make console scripts, which means pkg_resources is imported. pkg_resources @@ -65,19 +68,28 @@ FN_VAL = my_function("fooey") loader = globals().get('__loader__') fullname = getattr(loader, 'fullname', None) or getattr(loader, 'name', None) +# A more compact grouped-by-first-letter list of builtins. +def word_group(w): + """Clump AB, CD, EF, etc.""" + return chr((ord(w[0]) + 1) & 0xFE) + +builtin_dir = [" ".join(s) for _, s in itertools.groupby(dir(__builtins__), key=word_group)] + globals_to_check = { + 'os.getcwd': os.getcwd(), '__name__': __name__, '__file__': __file__, '__doc__': __doc__, '__builtins__.has_open': hasattr(__builtins__, 'open'), - '__builtins__.dir': dir(__builtins__), + '__builtins__.dir': builtin_dir, '__loader__ exists': loader is not None, '__loader__.fullname': fullname, '__package__': __package__, 'DATA': DATA, 'FN_VAL': FN_VAL, '__main__.DATA': getattr(__main__, "DATA", "nothing"), - 'argv': sys.argv, + 'argv0': sys.argv[0], + 'argv1-n': sys.argv[1:], 'path': cleaned_sys_path, } diff --git a/tests/modules/usepkgs.py b/tests/modules/usepkgs.py index 4e94aca..222e68c 100644 --- a/tests/modules/usepkgs.py +++ b/tests/modules/usepkgs.py @@ -1,7 +1,7 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -import pkg1.p1a, pkg1.p1b +import pkg1.p1a, pkg1.p1b, pkg1.sub import pkg2.p2a, pkg2.p2b import othermods.othera, othermods.otherb import othermods.sub.osa, othermods.sub.osb diff --git a/tests/osinfo.py b/tests/osinfo.py index a7ebd2e..094fb09 100644 --- a/tests/osinfo.py +++ b/tests/osinfo.py @@ -34,8 +34,8 @@ if env.WINDOWS: ctypes.byref(mem_struct), ctypes.sizeof(mem_struct) ) - if not ret: - return 0 + if not ret: # pragma: part covered + return 0 # pragma: cant happen return mem_struct.PrivateUsage elif env.LINUX: @@ -50,13 +50,13 @@ elif env.LINUX: # Get pseudo file /proc/<pid>/status with open('/proc/%d/status' % os.getpid()) as t: v = t.read() - except IOError: + except IOError: # pragma: cant happen return 0 # non-Linux? # Get VmKey line e.g. 'VmRSS: 9999 kB\n ...' i = v.index(key) v = v[i:].split(None, 3) - if len(v) < 3: - return 0 # Invalid format? + if len(v) < 3: # pragma: part covered + return 0 # pragma: cant happen # Convert Vm value to bytes. return int(float(v[1]) * _scale[v[2].lower()]) diff --git a/tests/plugin1.py b/tests/plugin1.py index af4dfc5..63ebacf 100644 --- a/tests/plugin1.py +++ b/tests/plugin1.py @@ -24,7 +24,7 @@ class FileTracer(coverage.FileTracer): """A FileTracer emulating a simple static plugin.""" def __init__(self, filename): - """Claim that xyz.py was actually sourced from ABC.zz""" + """Claim that */*xyz.py was actually sourced from /src/*ABC.zz""" self._filename = filename self._source_filename = os.path.join( "/src", diff --git a/tests/test_api.py b/tests/test_api.py index 6f14210..9a3fc82 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,11 +11,11 @@ import warnings import coverage from coverage import env -from coverage.backward import StringIO +from coverage.backward import StringIO, import_local_file from coverage.misc import CoverageException from coverage.report import Reporter -from tests.coveragetest import CoverageTest +from tests.coveragetest import CoverageTest, CoverageTestMethodsMixin class ApiTest(CoverageTest): @@ -35,7 +35,7 @@ class ApiTest(CoverageTest): def assertFiles(self, files): """Assert that the files here are `files`, ignoring the usual junk.""" here = os.listdir(".") - here = self.clean_files(here, ["*.pyc", "__pycache__"]) + here = self.clean_files(here, ["*.pyc", "__pycache__", "*$py.class"]) self.assertCountEqual(here, files) def test_unexecuted_file(self): @@ -282,17 +282,70 @@ class ApiTest(CoverageTest): self.check_code1_code2(cov) def test_start_save_stop(self): - self.skipTest("Expected failure: https://bitbucket.org/ned/coveragepy/issue/79") self.make_code1_code2() cov = coverage.Coverage() cov.start() - self.import_local_file("code1") - cov.save() - self.import_local_file("code2") - cov.stop() - + import_local_file("code1") # pragma: nested + cov.save() # pragma: nested + import_local_file("code2") # pragma: nested + cov.stop() # pragma: nested self.check_code1_code2(cov) + def test_start_save_nostop(self): + self.make_code1_code2() + cov = coverage.Coverage() + cov.start() + import_local_file("code1") # pragma: nested + cov.save() # pragma: nested + import_local_file("code2") # pragma: nested + self.check_code1_code2(cov) # pragma: nested + # Then stop it, or the test suite gets out of whack. + cov.stop() # pragma: nested + + def test_two_getdata_only_warn_once(self): + self.make_code1_code2() + cov = coverage.Coverage(source=["."], omit=["code1.py"]) + cov.start() + import_local_file("code1") # pragma: nested + cov.stop() # pragma: nested + # We didn't collect any data, so we should get a warning. + with self.assert_warnings(cov, ["No data was collected"]): + cov.get_data() + # But calling get_data a second time with no intervening activity + # won't make another warning. + with self.assert_warnings(cov, []): + cov.get_data() + + def test_two_getdata_only_warn_once_nostop(self): + self.make_code1_code2() + cov = coverage.Coverage(source=["."], omit=["code1.py"]) + cov.start() + import_local_file("code1") # pragma: nested + # We didn't collect any data, so we should get a warning. + with self.assert_warnings(cov, ["No data was collected"]): # pragma: nested + cov.get_data() # pragma: nested + # But calling get_data a second time with no intervening activity + # won't make another warning. + with self.assert_warnings(cov, []): # pragma: nested + cov.get_data() # pragma: nested + # Then stop it, or the test suite gets out of whack. + cov.stop() # pragma: nested + + def test_two_getdata_warn_twice(self): + self.make_code1_code2() + cov = coverage.Coverage(source=["."], omit=["code1.py", "code2.py"]) + cov.start() + import_local_file("code1") # pragma: nested + # We didn't collect any data, so we should get a warning. + with self.assert_warnings(cov, ["No data was collected"]): # pragma: nested + cov.save() # pragma: nested + import_local_file("code2") # pragma: nested + # Calling get_data a second time after tracing some more will warn again. + with self.assert_warnings(cov, ["No data was collected"]): # pragma: nested + cov.get_data() # pragma: nested + # Then stop it, or the test suite gets out of whack. + cov.stop() # pragma: nested + def make_good_data_files(self): """Make some good data files.""" self.make_code1_code2() @@ -348,6 +401,49 @@ class ApiTest(CoverageTest): self.assertEqual(statements, [1, 2]) self.assertEqual(missing, [1, 2]) + def test_warnings(self): + self.make_file("hello.py", """\ + import sys, os + print("Hello") + """) + cov = coverage.Coverage(source=["sys", "xyzzy", "quux"]) + self.start_import_stop(cov, "hello") + cov.get_data() + + out = self.stdout() + self.assertIn("Hello\n", out) + + err = self.stderr() + self.assertIn(textwrap.dedent("""\ + Coverage.py warning: Module sys has no Python source. (module-not-python) + Coverage.py warning: Module xyzzy was never imported. (module-not-imported) + Coverage.py warning: Module quux was never imported. (module-not-imported) + Coverage.py warning: No data was collected. (no-data-collected) + """), err) + + def test_warnings_suppressed(self): + self.make_file("hello.py", """\ + import sys, os + print("Hello") + """) + self.make_file(".coveragerc", """\ + [run] + disable_warnings = no-data-collected, module-not-imported + """) + cov = coverage.Coverage(source=["sys", "xyzzy", "quux"]) + self.start_import_stop(cov, "hello") + cov.get_data() + + out = self.stdout() + self.assertIn("Hello\n", out) + + err = self.stderr() + self.assertIn(textwrap.dedent("""\ + Coverage.py warning: Module sys has no Python source. (module-not-python) + """), err) + self.assertNotIn("module-not-imported", err) + self.assertNotIn("no-data-collected", err) + class NamespaceModuleTest(CoverageTest): """Test PEP-420 namespace modules.""" @@ -376,16 +472,14 @@ class UsingModulesMixin(object): def setUp(self): super(UsingModulesMixin, self).setUp() - old_dir = os.getcwd() - os.chdir(self.nice_file(os.path.dirname(__file__), 'modules')) - self.addCleanup(os.chdir, old_dir) + self.chdir(self.nice_file(os.path.dirname(__file__), 'modules')) # Parent class saves and restores sys.path, we can just modify it. sys.path.append(".") sys.path.append("../moremodules") -class OmitIncludeTestsMixin(UsingModulesMixin): +class OmitIncludeTestsMixin(UsingModulesMixin, CoverageTestMethodsMixin): """Test methods for coverage methods taking include and omit.""" def filenames_in(self, summary, filenames): @@ -461,13 +555,20 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest): summary[k[:-3]] = v return summary - def test_source_package(self): + def test_source_package_as_dir(self): + # pkg1 is a directory, since we cd'd into tests/modules in setUp. lines = self.coverage_usepkgs(source=["pkg1"]) self.filenames_in(lines, "p1a p1b") self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") # Because source= was specified, we do search for unexecuted files. self.assertEqual(lines['p1c'], 0) + def test_source_package_as_package(self): + lines = self.coverage_usepkgs(source=["pkg1.sub"]) + self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") + # Because source= was specified, we do search for unexecuted files. + self.assertEqual(lines['runmod3'], 0) + def test_source_package_dotted(self): lines = self.coverage_usepkgs(source=["pkg1.p1b"]) self.filenames_in(lines, "p1b") diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 5ea2fe1..7df623b 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -143,10 +143,17 @@ class SimpleArcTest(CoverageTest): ) def test_what_is_the_sound_of_no_lines_clapping(self): + if env.JYTHON: + # Jython reports no lines for an empty file. + arcz_missing=".1 1." # pragma: only jython + else: + # Other Pythons report one line. + arcz_missing="" self.check_coverage("""\ # __init__.py """, arcz=".1 1.", + arcz_missing=arcz_missing, ) @@ -252,12 +259,12 @@ class LoopArcTest(CoverageTest): """, arcz=".1 12 23 34 45 36 63 57 7.", ) - # With "while True", 2.x thinks it's computation, 3.x thinks it's - # constant. + # With "while True", 2.x thinks it's computation, + # 3.x thinks it's constant. if env.PY3: arcz = ".1 12 23 34 45 36 63 57 7." else: - arcz = ".1 12 23 27 34 45 36 62 57 7." + arcz = ".1 12 23 34 45 36 62 57 7." self.check_coverage("""\ a, i = 1, 0 while True: @@ -270,6 +277,37 @@ class LoopArcTest(CoverageTest): arcz=arcz, ) + def test_zero_coverage_while_loop(self): + # https://bitbucket.org/ned/coveragepy/issue/502 + self.make_file("main.py", "print('done')") + self.make_file("zero.py", """\ + def method(self): + while True: + return 1 + """) + out = self.run_command("coverage run --branch --source=. main.py") + self.assertEqual(out, 'done\n') + report = self.report_from_command("coverage report -m") + squeezed = self.squeezed_lines(report) + self.assertIn("zero.py 3 3 0 0 0% 1-3", squeezed[3]) + + def test_bug_496_continue_in_constant_while(self): + # https://bitbucket.org/ned/coveragepy/issue/496 + if env.PY3: + arcz = ".1 12 23 34 45 53 46 6." + else: + arcz = ".1 12 23 34 45 52 46 6." + self.check_coverage("""\ + up = iter('ta') + while True: + char = next(up) + if char == 't': + continue + break + """, + arcz=arcz + ) + def test_for_if_else_for(self): self.check_coverage("""\ def branches_2(l): @@ -370,7 +408,7 @@ class LoopArcTest(CoverageTest): def test_other_comprehensions(self): if env.PYVERSION < (2, 7): - self.skipTest("Don't have set or dict comprehensions before 2.7") + self.skipTest("No set or dict comprehensions before 2.7") # Set comprehension: self.check_coverage("""\ o = ((1,2), (3,4)) @@ -394,7 +432,7 @@ class LoopArcTest(CoverageTest): def test_multiline_dict_comp(self): if env.PYVERSION < (2, 7): - self.skipTest("Don't have set or dict comprehensions before 2.7") + self.skipTest("No set or dict comprehensions before 2.7") if env.PYVERSION < (3, 5): arcz = "-42 2B B-4 2-4" else: @@ -762,16 +800,19 @@ class ExceptionArcTest(CoverageTest): def test_return_finally(self): self.check_coverage("""\ a = [1] - def func(): - try: - return 10 - finally: - a.append(6) - - assert func() == 10 - assert a == [1, 6] - """, - arcz=".1 12 28 89 9. -23 34 46 6-2", + def check_token(data): + if data: + try: + return 5 + finally: + a.append(7) + return 8 + assert check_token(False) == 8 + assert a == [1] + assert check_token(True) == 5 + assert a == [1, 7] + """, + arcz=".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2", ) def test_except_jump_finally(self): @@ -998,6 +1039,103 @@ class YieldTest(CoverageTest): ) +class OptimizedIfTest(CoverageTest): + """Tests of if statements being optimized away.""" + + def test_optimized_away_if_0(self): + self.check_coverage("""\ + a = 1 + if len([2]): + c = 3 + if 0: # this line isn't in the compiled code. + if len([5]): + d = 6 + else: + e = 8 + f = 9 + """, + lines=[1, 2, 3, 8, 9], + arcz=".1 12 23 28 38 89 9.", + arcz_missing="28", + ) + + def test_optimized_away_if_1(self): + self.check_coverage("""\ + a = 1 + if len([2]): + c = 3 + if 1: # this line isn't in the compiled code, + if len([5]): # but these are. + d = 6 + else: + e = 8 + f = 9 + """, + lines=[1, 2, 3, 5, 6, 9], + arcz=".1 12 23 25 35 56 69 59 9.", + arcz_missing="25 59", + ) + self.check_coverage("""\ + a = 1 + if 1: + b = 3 + c = 4 + d = 5 + """, + lines=[1, 3, 4, 5], + arcz=".1 13 34 45 5.", + ) + + def test_optimized_nested(self): + self.check_coverage("""\ + a = 1 + if 0: + if 0: + b = 4 + else: + c = 6 + else: + if 0: + d = 9 + else: + if 0: e = 11 + f = 12 + if 0: g = 13 + h = 14 + i = 15 + """, + lines=[1, 12, 14, 15], + arcz=".1 1C CE EF F.", + ) + + def test_constant_if(self): + if env.PYPY: + self.skipTest("PyPy doesn't optimize away 'if __debug__:'") + # CPython optimizes away "if __debug__:" + self.check_coverage("""\ + for value in [True, False]: + if value: + if __debug__: + x = 4 + else: + x = 6 + """, + arcz=".1 12 24 41 26 61 1.", + ) + # No Python optimizes away "if not __debug__:" + self.check_coverage("""\ + for value in [True, False]: + if value: + if not __debug__: + x = 4 + else: + x = 6 + """, + arcz=".1 12 23 31 34 41 26 61 1.", + arcz_missing="34 41", + ) + + class MiscArcTest(CoverageTest): """Miscellaneous arc-measuring tests.""" @@ -1067,6 +1205,9 @@ class MiscArcTest(CoverageTest): ) def test_pathologically_long_code_object(self): + if env.JYTHON: + self.skipTest("Bytecode concerns are irrelevant on Jython") + # https://bitbucket.org/ned/coveragepy/issue/359 # The structure of this file is such that an EXTENDED_ARG bytecode is # needed to encode the jump at the end. We weren't interpreting those @@ -1090,21 +1231,6 @@ class MiscArcTest(CoverageTest): arcs_missing=[], arcs_unpredicted=[], ) - def test_optimized_away_lines(self): - self.check_coverage("""\ - a = 1 - if len([2]): - c = 3 - if 0: # this line isn't in the compiled code. - if len([5]): - d = 6 - e = 7 - """, - lines=[1, 2, 3, 7], - arcz=".1 12 23 27 37 7.", - arcz_missing="27", - ) - def test_partial_generators(self): # https://bitbucket.org/ned/coveragepy/issues/475/generator-expression-is-marked-as-not # Line 2 is executed completely. @@ -1340,7 +1466,7 @@ class AsyncTest(CoverageTest): def __init__(self, obj): # 4 self._it = iter(obj) - async def __aiter__(self): # 7 + def __aiter__(self): # 7 return self async def __anext__(self): # A diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 3b982eb..2378887 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -5,11 +5,11 @@ import pprint import re -import shlex import sys import textwrap import mock +import pytest import coverage import coverage.cmdline @@ -18,7 +18,7 @@ from coverage.config import CoverageConfig from coverage.data import CoverageData, CoverageDataFiles from coverage.misc import ExceptionDuringRun -from tests.coveragetest import CoverageTest, OK, ERR +from tests.coveragetest import CoverageTest, OK, ERR, command_line class BaseCmdLineTest(CoverageTest): @@ -29,7 +29,7 @@ class BaseCmdLineTest(CoverageTest): # Make a dict mapping function names to the default values that cmdline.py # uses when calling the function. defaults = mock.Mock() - defaults.coverage( + defaults.Coverage( cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None, debug=None, concurrency=None, @@ -39,7 +39,7 @@ class BaseCmdLineTest(CoverageTest): ) defaults.html_report( directory=None, ignore_errors=None, include=None, omit=None, morfs=[], - title=None, + skip_covered=None, title=None ) defaults.report( ignore_errors=None, include=None, omit=None, morfs=[], @@ -54,9 +54,9 @@ class BaseCmdLineTest(CoverageTest): def model_object(self): """Return a Mock suitable for use in CoverageScript.""" mk = mock.Mock() - # We'll invoke .coverage as the constructor, and then keep using the + # We'll invoke .Coverage as the constructor, and then keep using the # same object as the resulting coverage object. - mk.coverage.return_value = mk + mk.Coverage.return_value = mk # The mock needs to get options, but shouldn't need to set them. config = CoverageConfig() @@ -73,11 +73,12 @@ class BaseCmdLineTest(CoverageTest): m = self.model_object() m.path_exists.return_value = path_exists - ret = coverage.cmdline.CoverageScript( + ret = command_line( + args, _covpkg=m, _run_python_file=m.run_python_file, _run_python_module=m.run_python_module, _help_fn=m.help_fn, _path_exists=m.path_exists, - ).command_line(shlex.split(args)) + ) return m, ret @@ -98,7 +99,7 @@ class BaseCmdLineTest(CoverageTest): # calls them with many. But most of them are just the defaults, which # we don't want to have to repeat in all tests. For each call, apply # the defaults. This lets the tests just mention the interesting ones. - for name, args, kwargs in m2.method_calls: + for name, _, kwargs in m2.method_calls: for k, v in self.DEFAULT_KWARGS.get(name, {}).items(): if k not in kwargs: kwargs[k] = v @@ -151,37 +152,37 @@ class CmdLineTest(BaseCmdLineTest): def test_annotate(self): # coverage annotate [-d DIR] [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("annotate", """\ - .coverage() + .Coverage() .load() .annotate() """) self.cmd_executes("annotate -d dir1", """\ - .coverage() + .Coverage() .load() .annotate(directory="dir1") """) self.cmd_executes("annotate -i", """\ - .coverage() + .Coverage() .load() .annotate(ignore_errors=True) """) self.cmd_executes("annotate --omit fooey", """\ - .coverage(omit=["fooey"]) + .Coverage(omit=["fooey"]) .load() .annotate(omit=["fooey"]) """) self.cmd_executes("annotate --omit fooey,booey", """\ - .coverage(omit=["fooey", "booey"]) + .Coverage(omit=["fooey", "booey"]) .load() .annotate(omit=["fooey", "booey"]) """) self.cmd_executes("annotate mod1", """\ - .coverage() + .Coverage() .load() .annotate(morfs=["mod1"]) """) self.cmd_executes("annotate mod1 mod2 mod3", """\ - .coverage() + .Coverage() .load() .annotate(morfs=["mod1", "mod2", "mod3"]) """) @@ -189,20 +190,20 @@ class CmdLineTest(BaseCmdLineTest): def test_combine(self): # coverage combine with args self.cmd_executes("combine datadir1", """\ - .coverage() + .Coverage() .combine(["datadir1"], strict=True) .save() """) # coverage combine, appending self.cmd_executes("combine --append datadir1", """\ - .coverage() + .Coverage() .load() .combine(["datadir1"], strict=True) .save() """) # coverage combine without args self.cmd_executes("combine", """\ - .coverage() + .Coverage() .combine(None, strict=True) .save() """) @@ -210,12 +211,12 @@ class CmdLineTest(BaseCmdLineTest): def test_combine_doesnt_confuse_options_with_args(self): # https://bitbucket.org/ned/coveragepy/issues/385/coverage-combine-doesnt-work-with-rcfile self.cmd_executes("combine --rcfile cov.ini", """\ - .coverage(config_file='cov.ini') + .Coverage(config_file='cov.ini') .combine(None, strict=True) .save() """) self.cmd_executes("combine --rcfile cov.ini data1 data2/more", """\ - .coverage(config_file='cov.ini') + .Coverage(config_file='cov.ini') .combine(["data1", "data2/more"], strict=True) .save() """) @@ -239,7 +240,7 @@ class CmdLineTest(BaseCmdLineTest): def test_erase(self): # coverage erase self.cmd_executes("erase", """\ - .coverage() + .Coverage() .erase() """) @@ -262,42 +263,42 @@ class CmdLineTest(BaseCmdLineTest): def test_html(self): # coverage html -d DIR [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("html", """\ - .coverage() + .Coverage() .load() .html_report() """) self.cmd_executes("html -d dir1", """\ - .coverage() + .Coverage() .load() .html_report(directory="dir1") """) self.cmd_executes("html -i", """\ - .coverage() + .Coverage() .load() .html_report(ignore_errors=True) """) self.cmd_executes("html --omit fooey", """\ - .coverage(omit=["fooey"]) + .Coverage(omit=["fooey"]) .load() .html_report(omit=["fooey"]) """) self.cmd_executes("html --omit fooey,booey", """\ - .coverage(omit=["fooey", "booey"]) + .Coverage(omit=["fooey", "booey"]) .load() .html_report(omit=["fooey", "booey"]) """) self.cmd_executes("html mod1", """\ - .coverage() + .Coverage() .load() .html_report(morfs=["mod1"]) """) self.cmd_executes("html mod1 mod2 mod3", """\ - .coverage() + .Coverage() .load() .html_report(morfs=["mod1", "mod2", "mod3"]) """) self.cmd_executes("html --title=Hello_there", """\ - .coverage() + .Coverage() .load() .html_report(title='Hello_there') """) @@ -305,42 +306,42 @@ class CmdLineTest(BaseCmdLineTest): def test_report(self): # coverage report [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] self.cmd_executes("report", """\ - .coverage() + .Coverage() .load() .report(show_missing=None) """) self.cmd_executes("report -i", """\ - .coverage() + .Coverage() .load() .report(ignore_errors=True) """) self.cmd_executes("report -m", """\ - .coverage() + .Coverage() .load() .report(show_missing=True) """) self.cmd_executes("report --omit fooey", """\ - .coverage(omit=["fooey"]) + .Coverage(omit=["fooey"]) .load() .report(omit=["fooey"]) """) self.cmd_executes("report --omit fooey,booey", """\ - .coverage(omit=["fooey", "booey"]) + .Coverage(omit=["fooey", "booey"]) .load() .report(omit=["fooey", "booey"]) """) self.cmd_executes("report mod1", """\ - .coverage() + .Coverage() .load() .report(morfs=["mod1"]) """) self.cmd_executes("report mod1 mod2 mod3", """\ - .coverage() + .Coverage() .load() .report(morfs=["mod1", "mod2", "mod3"]) """) self.cmd_executes("report --skip-covered", """\ - .coverage() + .Coverage() .load() .report(skip_covered=True) """) @@ -350,7 +351,7 @@ class CmdLineTest(BaseCmdLineTest): # run calls coverage.erase first. self.cmd_executes("run foo.py", """\ - .coverage() + .Coverage() .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -359,7 +360,7 @@ class CmdLineTest(BaseCmdLineTest): """) # run -a combines with an existing data file before saving. self.cmd_executes("run -a foo.py", """\ - .coverage() + .Coverage() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -369,7 +370,7 @@ class CmdLineTest(BaseCmdLineTest): """, path_exists=True) # run -a doesn't combine anything if the data file doesn't exist. self.cmd_executes("run -a foo.py", """\ - .coverage() + .Coverage() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -378,7 +379,7 @@ class CmdLineTest(BaseCmdLineTest): """, path_exists=False) # --timid sets a flag, and program arguments get passed through. self.cmd_executes("run --timid foo.py abc 123", """\ - .coverage(timid=True) + .Coverage(timid=True) .erase() .start() .run_python_file('foo.py', ['foo.py', 'abc', '123']) @@ -387,7 +388,7 @@ class CmdLineTest(BaseCmdLineTest): """) # -L sets a flag, and flags for the program don't confuse us. self.cmd_executes("run -p -L foo.py -a -b", """\ - .coverage(cover_pylib=True, data_suffix=True) + .Coverage(cover_pylib=True, data_suffix=True) .erase() .start() .run_python_file('foo.py', ['foo.py', '-a', '-b']) @@ -395,7 +396,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --branch foo.py", """\ - .coverage(branch=True) + .Coverage(branch=True) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -403,7 +404,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --rcfile=myrc.rc foo.py", """\ - .coverage(config_file="myrc.rc") + .Coverage(config_file="myrc.rc") .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -411,7 +412,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --include=pre1,pre2 foo.py", """\ - .coverage(include=["pre1", "pre2"]) + .Coverage(include=["pre1", "pre2"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -419,7 +420,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --omit=opre1,opre2 foo.py", """\ - .coverage(omit=["opre1", "opre2"]) + .Coverage(omit=["opre1", "opre2"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -427,7 +428,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --include=pre1,pre2 --omit=opre1,opre2 foo.py", """\ - .coverage(include=["pre1", "pre2"], omit=["opre1", "opre2"]) + .Coverage(include=["pre1", "pre2"], omit=["opre1", "opre2"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -435,7 +436,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --source=quux,hi.there,/home/bar foo.py", """\ - .coverage(source=["quux", "hi.there", "/home/bar"]) + .Coverage(source=["quux", "hi.there", "/home/bar"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -443,7 +444,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --concurrency=gevent foo.py", """\ - .coverage(concurrency='gevent') + .Coverage(concurrency='gevent') .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -451,7 +452,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --concurrency=multiprocessing foo.py", """\ - .coverage(concurrency='multiprocessing') + .Coverage(concurrency='multiprocessing') .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -461,16 +462,16 @@ class CmdLineTest(BaseCmdLineTest): def test_bad_concurrency(self): self.command_line("run --concurrency=nothing", ret=ERR) - out = self.stdout() - self.assertIn("option --concurrency: invalid choice: 'nothing'", out) + err = self.stderr() + self.assertIn("option --concurrency: invalid choice: 'nothing'", err) def test_no_multiple_concurrency(self): # You can't use multiple concurrency values on the command line. # I would like to have a better message about not allowing multiple # values for this option, but optparse is not that flexible. self.command_line("run --concurrency=multiprocessing,gevent foo.py", ret=ERR) - out = self.stdout() - self.assertIn("option --concurrency: invalid choice: 'multiprocessing,gevent'", out) + err = self.stderr() + self.assertIn("option --concurrency: invalid choice: 'multiprocessing,gevent'", err) def test_multiprocessing_needs_config_file(self): # You can't use command-line args to add options to multiprocessing @@ -479,12 +480,12 @@ class CmdLineTest(BaseCmdLineTest): self.command_line("run --concurrency=multiprocessing --branch foo.py", ret=ERR) self.assertIn( "Options affecting multiprocessing must be specified in a configuration file.", - self.stdout() + self.stderr() ) def test_run_debug(self): self.cmd_executes("run --debug=opt1 foo.py", """\ - .coverage(debug=["opt1"]) + .Coverage(debug=["opt1"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -492,7 +493,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --debug=opt1,opt2 foo.py", """\ - .coverage(debug=["opt1","opt2"]) + .Coverage(debug=["opt1","opt2"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -502,7 +503,7 @@ class CmdLineTest(BaseCmdLineTest): def test_run_module(self): self.cmd_executes("run -m mymodule", """\ - .coverage() + .Coverage() .erase() .start() .run_python_module('mymodule', ['mymodule']) @@ -510,7 +511,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run -m mymodule -qq arg1 arg2", """\ - .coverage() + .Coverage() .erase() .start() .run_python_module('mymodule', ['mymodule', '-qq', 'arg1', 'arg2']) @@ -518,7 +519,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --branch -m mymodule", """\ - .coverage(branch=True) + .Coverage(branch=True) .erase() .start() .run_python_module('mymodule', ['mymodule']) @@ -529,51 +530,51 @@ class CmdLineTest(BaseCmdLineTest): def test_run_nothing(self): self.command_line("run", ret=ERR) - self.assertIn("Nothing to do", self.stdout()) + self.assertIn("Nothing to do", self.stderr()) def test_cant_append_parallel(self): self.command_line("run --append --parallel-mode foo.py", ret=ERR) - self.assertIn("Can't append to data files in parallel mode.", self.stdout()) + self.assertIn("Can't append to data files in parallel mode.", self.stderr()) def test_xml(self): # coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("xml", """\ - .coverage() + .Coverage() .load() .xml_report() """) self.cmd_executes("xml -i", """\ - .coverage() + .Coverage() .load() .xml_report(ignore_errors=True) """) self.cmd_executes("xml -o myxml.foo", """\ - .coverage() + .Coverage() .load() .xml_report(outfile="myxml.foo") """) self.cmd_executes("xml -o -", """\ - .coverage() + .Coverage() .load() .xml_report(outfile="-") """) self.cmd_executes("xml --omit fooey", """\ - .coverage(omit=["fooey"]) + .Coverage(omit=["fooey"]) .load() .xml_report(omit=["fooey"]) """) self.cmd_executes("xml --omit fooey,booey", """\ - .coverage(omit=["fooey", "booey"]) + .Coverage(omit=["fooey", "booey"]) .load() .xml_report(omit=["fooey", "booey"]) """) self.cmd_executes("xml mod1", """\ - .coverage() + .Coverage() .load() .xml_report(morfs=["mod1"]) """) self.cmd_executes("xml mod1 mod2 mod3", """\ - .coverage() + .Coverage() .load() .xml_report(morfs=["mod1", "mod2", "mod3"]) """) @@ -661,9 +662,9 @@ class CmdLineStdoutTest(BaseCmdLineTest): def test_error(self): self.command_line("fooey kablooey", ret=ERR) - out = self.stdout() - self.assertIn("fooey", out) - self.assertIn("help", out) + err = self.stderr() + self.assertIn("fooey", err) + self.assertIn("help", err) class CmdMainTest(CoverageTest): @@ -693,13 +694,9 @@ class CmdMainTest(CoverageTest): def setUp(self): super(CmdMainTest, self).setUp() - self.old_CoverageScript = coverage.cmdline.CoverageScript + old_CoverageScript = coverage.cmdline.CoverageScript coverage.cmdline.CoverageScript = self.CoverageScriptStub - self.addCleanup(self.cleanup_coverage_script) - - def cleanup_coverage_script(self): - """Restore CoverageScript when the test is done.""" - coverage.cmdline.CoverageScript = self.old_CoverageScript + self.addCleanup(setattr, coverage.cmdline, 'CoverageScript', old_CoverageScript) def test_normal(self): ret = coverage.cmdline.main(['hello']) @@ -722,3 +719,59 @@ class CmdMainTest(CoverageTest): def test_exit(self): ret = coverage.cmdline.main(['exit']) self.assertEqual(ret, 23) + + +class CoverageReportingFake(object): + """A fake Coverage and Coverage.coverage test double.""" + # pylint: disable=missing-docstring + def __init__(self, report_result, html_result, xml_result): + self.report_result = report_result + self.html_result = html_result + self.xml_result = xml_result + + def Coverage(self, *args_unused, **kwargs_unused): + return self + + def set_option(self, optname, optvalue): + setattr(self, optname, optvalue) + + def get_option(self, optname): + return getattr(self, optname) + + def load(self): + pass + + def report(self, *args_unused, **kwargs_unused): + return self.report_result + + def html_report(self, *args_unused, **kwargs_unused): + return self.html_result + + def xml_report(self, *args_unused, **kwargs_unused): + return self.xml_result + + +@pytest.mark.parametrize("results, fail_under, cmd, ret", [ + # Command-line switch properly checks the result of reporting functions. + ((20, 30, 40), None, "report --fail-under=19", 0), + ((20, 30, 40), None, "report --fail-under=21", 2), + ((20, 30, 40), None, "html --fail-under=29", 0), + ((20, 30, 40), None, "html --fail-under=31", 2), + ((20, 30, 40), None, "xml --fail-under=39", 0), + ((20, 30, 40), None, "xml --fail-under=41", 2), + # Configuration file setting properly checks the result of reporting. + ((20, 30, 40), 19, "report", 0), + ((20, 30, 40), 21, "report", 2), + ((20, 30, 40), 29, "html", 0), + ((20, 30, 40), 31, "html", 2), + ((20, 30, 40), 39, "xml", 0), + ((20, 30, 40), 41, "xml", 2), + # Command-line overrides configuration. + ((20, 30, 40), 19, "report --fail-under=21", 2), +]) +def test_fail_under(results, fail_under, cmd, ret): + cov = CoverageReportingFake(*results) + if fail_under: + cov.set_option("report:fail_under", fail_under) + ret_actual = command_line(cmd, _covpkg=cov) + assert ret_actual == ret diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index e36db30..841b5df 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -3,9 +3,10 @@ """Tests for concurrency libraries.""" -import multiprocessing import threading +from flaky import flaky + import coverage from coverage import env from coverage.files import abs_file @@ -16,6 +17,11 @@ from tests.coveragetest import CoverageTest # These libraries aren't always available, we'll skip tests if they aren't. try: + import multiprocessing +except ImportError: # pragma: only jython + multiprocessing = None + +try: import eventlet except ImportError: eventlet = None @@ -25,7 +31,10 @@ try: except ImportError: gevent = None -import greenlet +try: + import greenlet +except ImportError: # pragma: only jython + greenlet = None def measurable_line(l): @@ -40,6 +49,9 @@ def measurable_line(l): return False if l.startswith('else:'): return False + if env.JYTHON and l.startswith(('try:', 'except:', 'except ', 'break', 'with ')): + # Jython doesn't measure these statements. + return False # pragma: only jython return True @@ -342,9 +354,15 @@ MULTI_CODE = """ """ +@flaky # Sometimes a test fails due to inherent randomness. Try one more time. class MultiprocessingTest(CoverageTest): """Test support of the multiprocessing module.""" + def setUp(self): + super(MultiprocessingTest, self).setUp() + if not multiprocessing: + self.skipTest("No multiprocessing in this Python") # pragma: only jython + def try_multiprocessing_code( self, code, expected_out, the_module, concurrency="multiprocessing" ): @@ -353,6 +371,7 @@ class MultiprocessingTest(CoverageTest): self.make_file(".coveragerc", """\ [run] concurrency = %s + source = . """ % concurrency) if env.PYVERSION >= (3, 4): diff --git a/tests/test_config.py b/tests/test_config.py index cf8a6a7..9224046 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -95,6 +95,15 @@ class ConfigTest(CoverageTest): cov = coverage.Coverage(data_file="fromarg.dat") self.assertEqual(cov.config.data_file, "fromarg.dat") + def test_debug_from_environment(self): + self.make_file(".coveragerc", """\ + [run] + debug = dataio, pids + """) + self.set_environ("COVERAGE_DEBUG", "callers, fooey") + cov = coverage.Coverage() + self.assertEqual(cov.config.debug, ["dataio", "pids", "callers", "fooey"]) + def test_parse_errors(self): # Im-parsable values raise CoverageException, with details. bad_configs_and_msgs = [ @@ -206,7 +215,7 @@ class ConfigTest(CoverageTest): [coverage:run] huh = what? """) - msg = r"Unrecognized option '\[coverage:run\] huh=' in config file setup.cfg" + msg = (r"Unrecognized option '\[coverage:run\] huh=' in config file setup.cfg") with self.assertRaisesRegex(CoverageException, msg): _ = coverage.Coverage() @@ -230,12 +239,13 @@ class ConfigFileTest(CoverageTest): branch = 1 cover_pylib = TRUE parallel = on - include = a/ , b/ concurrency = thread source = myapp plugins = plugins.a_plugin plugins.another + debug = callers, pids , dataio + disable_warnings = abcd , efgh [{section}report] ; these settings affect reporting. @@ -294,19 +304,32 @@ class ConfigFileTest(CoverageTest): examples/ """ + # Just some sample tox.ini text from the docs. + TOX_INI = """\ + [tox] + envlist = py{26,27,33,34,35}-{c,py}tracer + skip_missing_interpreters = True + + [testenv] + commands = + # Create tests/zipmods.zip, install the egg1 egg + python igor.py zip_mods install_egg + """ + def assert_config_settings_are_correct(self, cov): """Check that `cov` has all the settings from LOTSA_SETTINGS.""" self.assertTrue(cov.config.timid) self.assertEqual(cov.config.data_file, "something_or_other.dat") self.assertTrue(cov.config.branch) self.assertTrue(cov.config.cover_pylib) + self.assertEqual(cov.config.debug, ["callers", "pids", "dataio"]) self.assertTrue(cov.config.parallel) self.assertEqual(cov.config.concurrency, ["thread"]) self.assertEqual(cov.config.source, ["myapp"]) + self.assertEqual(cov.config.disable_warnings, ["abcd", "efgh"]) self.assertEqual(cov.get_exclude_list(), ["if 0:", r"pragma:?\s+no cover", "another_tab"]) self.assertTrue(cov.config.ignore_errors) - self.assertEqual(cov.config.include, ["a/", "b/"]) self.assertEqual(cov.config.omit, ["one", "another", "some_more", "yet_more"]) self.assertEqual(cov.config.precision, 3) @@ -338,29 +361,39 @@ class ConfigFileTest(CoverageTest): cov = coverage.Coverage() self.assert_config_settings_are_correct(cov) - def test_config_file_settings_in_setupcfg(self): - # Configuration will be read from setup.cfg from sections prefixed with - # "coverage:" + def check_config_file_settings_in_other_file(self, fname, contents): + """Check config will be read from another file, with prefixed sections.""" nested = self.LOTSA_SETTINGS.format(section="coverage:") - self.make_file("setup.cfg", nested + "\n" + self.SETUP_CFG) + fname = self.make_file(fname, nested + "\n" + contents) cov = coverage.Coverage() self.assert_config_settings_are_correct(cov) - def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self): - # Configuration will be read from setup.cfg from sections prefixed with - # "coverage:", even if the API said to read from a (non-existent) - # .coveragerc file. + def test_config_file_settings_in_setupcfg(self): + self.check_config_file_settings_in_other_file("setup.cfg", self.SETUP_CFG) + + def test_config_file_settings_in_toxini(self): + self.check_config_file_settings_in_other_file("tox.ini", self.TOX_INI) + + def check_other_config_if_coveragerc_specified(self, fname, contents): + """Check that config `fname` is read if .coveragerc is missing, but specified.""" nested = self.LOTSA_SETTINGS.format(section="coverage:") - self.make_file("setup.cfg", nested + "\n" + self.SETUP_CFG) + self.make_file(fname, nested + "\n" + contents) cov = coverage.Coverage(config_file=".coveragerc") self.assert_config_settings_are_correct(cov) - def test_setupcfg_only_if_not_coveragerc(self): + def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self): + self.check_other_config_if_coveragerc_specified("setup.cfg", self.SETUP_CFG) + + def test_config_file_settings_in_tox_if_coveragerc_specified(self): + self.check_other_config_if_coveragerc_specified("tox.ini", self.TOX_INI) + + def check_other_not_read_if_coveragerc(self, fname): + """Check config `fname` is not read if .coveragerc exists.""" self.make_file(".coveragerc", """\ [run] include = foo """) - self.make_file("setup.cfg", """\ + self.make_file(fname, """\ [coverage:run] omit = bar branch = true @@ -370,8 +403,15 @@ class ConfigFileTest(CoverageTest): self.assertEqual(cov.config.omit, None) self.assertEqual(cov.config.branch, False) - def test_setupcfg_only_if_prefixed(self): - self.make_file("setup.cfg", """\ + def test_setupcfg_only_if_not_coveragerc(self): + self.check_other_not_read_if_coveragerc("setup.cfg") + + def test_toxini_only_if_not_coveragerc(self): + self.check_other_not_read_if_coveragerc("tox.ini") + + def check_other_config_need_prefixes(self, fname): + """Check that `fname` sections won't be read if un-prefixed.""" + self.make_file(fname, """\ [run] omit = bar branch = true @@ -380,6 +420,21 @@ class ConfigFileTest(CoverageTest): self.assertEqual(cov.config.omit, None) self.assertEqual(cov.config.branch, False) + def test_setupcfg_only_if_prefixed(self): + self.check_other_config_need_prefixes("setup.cfg") + + def test_toxini_only_if_prefixed(self): + self.check_other_config_need_prefixes("tox.ini") + + def test_tox_ini_even_if_setup_cfg(self): + # There's a setup.cfg, but no coverage settings in it, so tox.ini + # is read. + nested = self.LOTSA_SETTINGS.format(section="coverage:") + self.make_file("tox.ini", self.TOX_INI + "\n" + nested) + self.make_file("setup.cfg", self.SETUP_CFG) + cov = coverage.Coverage() + self.assert_config_settings_are_correct(cov) + def test_non_ascii(self): self.make_file(".coveragerc", """\ [report] diff --git a/tests/test_coverage.py b/tests/test_coverage.py index a52aced..bda61fc 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -418,7 +418,7 @@ class SimpleStatementTest(CoverageTest): """, [1,2,3,4,5], "4") - def test_strange_unexecuted_continue(self): + def test_strange_unexecuted_continue(self): # pragma: not covered # Peephole optimization of jumps to jumps can mean that some statements # never hit the line tracer. The behavior is different in different # versions of Python, so don't run this test: @@ -567,7 +567,7 @@ class SimpleStatementTest(CoverageTest): def test_nonascii(self): self.check_coverage("""\ - # coding: utf8 + # coding: utf-8 a = 2 b = 3 """, diff --git a/tests/test_data.py b/tests/test_data.py index 4bccdcf..46999f6 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -13,10 +13,11 @@ import mock from coverage.backward import StringIO from coverage.data import CoverageData, CoverageDataFiles, debug_main, canonicalize_json_data +from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException -from tests.coveragetest import CoverageTest, DebugControlString +from tests.coveragetest import CoverageTest LINES_1 = { @@ -554,9 +555,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assertRegex( debug.get_output(), - r"^Creating CoverageData object\n" - r"Writing data to '.*\.coverage'\n" - r"Creating CoverageData object\n" + r"^Writing data to '.*\.coverage'\n" r"Reading data from '.*\.coverage'\n$" ) diff --git a/tests/test_debug.py b/tests/test_debug.py index 2d553ee..f733d72 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -4,13 +4,15 @@ """Tests of coverage/debug.py""" import os -import re + +import pytest import coverage from coverage.backward import StringIO -from coverage.debug import info_formatter, info_header, short_stack +from coverage.debug import filter_text, info_formatter, info_header, short_id, short_stack from tests.coveragetest import CoverageTest +from tests.helpers import re_lines class InfoFormatterTest(CoverageTest): @@ -39,15 +41,34 @@ class InfoFormatterTest(CoverageTest): lines = list(info_formatter(('info%d' % i, i) for i in range(3))) self.assertEqual(lines, ['info0: 0', 'info1: 1', 'info2: 2']) - def test_info_header(self): - self.assertEqual( - info_header("x"), - "-- x ---------------------------------------------------------" - ) - self.assertEqual( - info_header("hello there"), - "-- hello there -----------------------------------------------" - ) + +@pytest.mark.parametrize("label, header", [ + ("x", "-- x ---------------------------------------------------------"), + ("hello there", "-- hello there -----------------------------------------------"), +]) +def test_info_header(label, header): + assert info_header(label) == header + + +@pytest.mark.parametrize("id64, id16", [ + (0x1234, 0x1234), + (0x12340000, 0x1234), + (0xA5A55A5A, 0xFFFF), + (0x1234cba956780fed, 0x8008), +]) +def test_short_id(id64, id16): + assert short_id(id64) == id16 + + +@pytest.mark.parametrize("text, filters, result", [ + ("hello", [], "hello"), + ("hello\n", [], "hello\n"), + ("hello\nhello\n", [], "hello\nhello\n"), + ("hello\nbye\n", [lambda x: "="+x], "=hello\n=bye\n"), + ("hello\nbye\n", [lambda x: "="+x, lambda x: x+"\ndone\n"], "=hello\ndone\n=bye\ndone\n"), +]) +def test_filter_text(text, filters, result): + assert filter_text(text, filters) == result class DebugTraceTest(CoverageTest): @@ -70,7 +91,7 @@ class DebugTraceTest(CoverageTest): self.start_import_stop(cov, "f1") cov.save() - out_lines = debug_out.getvalue().splitlines() + out_lines = debug_out.getvalue() return out_lines def test_debug_no_trace(self): @@ -86,7 +107,7 @@ class DebugTraceTest(CoverageTest): self.assertIn("Tracing 'f1.py'", out_lines) # We should have lines like "Not tracing 'collector.py'..." - coverage_lines = lines_matching( + coverage_lines = re_lines( out_lines, r"^Not tracing .*: is part of coverage.py$" ) @@ -96,28 +117,29 @@ class DebugTraceTest(CoverageTest): out_lines = self.f1_debug_output(["trace", "pid"]) # Now our lines are always prefixed with the process id. - pid_prefix = "^pid %5d: " % os.getpid() - pid_lines = lines_matching(out_lines, pid_prefix) + pid_prefix = r"^%5d\.[0-9a-f]{4}: " % os.getpid() + pid_lines = re_lines(out_lines, pid_prefix) self.assertEqual(pid_lines, out_lines) # We still have some tracing, and some not tracing. - self.assertTrue(lines_matching(out_lines, pid_prefix + "Tracing ")) - self.assertTrue(lines_matching(out_lines, pid_prefix + "Not tracing ")) + self.assertTrue(re_lines(out_lines, pid_prefix + "Tracing ")) + self.assertTrue(re_lines(out_lines, pid_prefix + "Not tracing ")) def test_debug_callers(self): out_lines = self.f1_debug_output(["pid", "dataop", "dataio", "callers"]) - print("\n".join(out_lines)) + print(out_lines) # For every real message, there should be a stack # trace with a line like "f1_debug_output : /Users/ned/coverage/tests/test_debug.py @71" - real_messages = lines_matching(out_lines, r"^pid\s+\d+: ") + real_messages = re_lines(out_lines, r" @\d+", match=False).splitlines() frame_pattern = r"\s+f1_debug_output : .*tests[/\\]test_debug.py @\d+$" - frames = lines_matching(out_lines, frame_pattern) + frames = re_lines(out_lines, frame_pattern).splitlines() self.assertEqual(len(real_messages), len(frames)) # The last message should be "Writing data", and the last frame should # be write_file in data.py. - self.assertRegex(real_messages[-1], r"^pid\s+\d+: Writing data") - self.assertRegex(out_lines[-1], r"\s+write_file : .*coverage[/\\]data.py @\d+$") + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") + last_line = out_lines.splitlines()[-1] + self.assertRegex(last_line, r"\s+write_file : .*coverage[/\\]data.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) @@ -130,20 +152,23 @@ class DebugTraceTest(CoverageTest): """.split() for label in labels: label_pat = r"^\s*%s: " % label - self.assertEqual(len(lines_matching(out_lines, label_pat)), 1) + self.assertEqual( + len(re_lines(out_lines, label_pat).splitlines()), + 1 + ) def test_debug_sys(self): out_lines = self.f1_debug_output(["sys"]) labels = """ - version coverage cover_dirs pylib_dirs tracer config_files + version coverage cover_paths pylib_paths tracer config_files configs_read data_path python platform implementation executable cwd path environment command_line cover_match pylib_match """.split() for label in labels: label_pat = r"^\s*%s: " % label self.assertEqual( - len(lines_matching(out_lines, label_pat)), + len(re_lines(out_lines, label_pat).splitlines()), 1, msg="Incorrect lines for %r" % label, ) @@ -181,8 +206,3 @@ class ShortStackTest(CoverageTest): def test_short_stack_skip(self): stack = f_one(skip=1).splitlines() self.assertIn("f_two", stack[-1]) - - -def lines_matching(lines, pat): - """Gives the list of lines from `lines` that match `pat`.""" - return [l for l in lines if re.search(pat, l)] diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 889d6cf..bad3da9 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -10,6 +10,7 @@ import os.path import re import sys +from coverage import env from coverage.backward import binary_bytes from coverage.execfile import run_python_file, run_python_module from coverage.misc import NoCode, NoSource @@ -43,7 +44,8 @@ class RunFileTest(CoverageTest): self.assertEqual(mod_globs['__main__.DATA'], "xyzzy") # Argv should have the proper values. - self.assertEqual(mod_globs['argv'], [TRY_EXECFILE, "arg1", "arg2"]) + self.assertEqual(mod_globs['argv0'], TRY_EXECFILE) + self.assertEqual(mod_globs['argv1-n'], ["arg1", "arg2"]) # __builtins__ should have the right values, like open(). self.assertEqual(mod_globs['__builtins__.has_open'], True) @@ -102,6 +104,9 @@ class RunPycFileTest(CoverageTest): def make_pyc(self): """Create a .pyc file, and return the relative path to it.""" + if env.JYTHON: + self.skipTest("Can't make .pyc files on Jython") + self.make_file("compiled.py", """\ def doit(): print("I am here!") @@ -145,6 +150,25 @@ class RunPycFileTest(CoverageTest): with self.assertRaisesRegex(NoCode, "No file to run: 'xyzzy.pyc'"): run_python_file("xyzzy.pyc", []) + def test_running_py_from_binary(self): + # Use make_file to get the bookkeeping. Ideally, it would + # be able to write binary files. + bf = self.make_file("binary") + with open(bf, "wb") as f: + f.write(b'\x7fELF\x02\x01\x01\x00\x00\x00') + + msg = ( + r"Couldn't run 'binary' as Python code: " + r"(TypeError|ValueError): " + r"(" + r"compile\(\) expected string without null bytes" # for py2 + r"|" + r"source code string cannot contain null bytes" # for py3 + r")" + ) + with self.assertRaisesRegex(Exception, msg): + run_python_file(bf, [bf]) + class RunModuleTest(CoverageTest): """Test run_python_module.""" diff --git a/tests/test_farm.py b/tests/test_farm.py index ae9e915..1b52bc2 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -1,7 +1,7 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -"""Run tests in the farm sub-directory. Designed for nose.""" +"""Run tests in the farm sub-directory. Designed for pytest.""" import difflib import filecmp @@ -11,22 +11,28 @@ import os import re import shutil import sys -import unittest -from nose.plugins.skip import SkipTest +import pytest -from unittest_mixins import ModuleAwareMixin, SysPathAwareMixin, change_dir, saved_sys_path +from unittest_mixins import ModuleAwareMixin, SysPathAwareMixin, change_dir from tests.helpers import run_command from tests.backtest import execfile # pylint: disable=redefined-builtin +from coverage import env +from coverage.backunittest import unittest from coverage.debug import _TEST_NAME_FILE -def test_farm(clean_only=False): - """A test-generating function for nose to find and run.""" - for fname in glob.glob("tests/farm/*/*.py"): - case = FarmTestCase(fname, clean_only) - yield (case,) +# Look for files that become tests. +TEST_FILES = glob.glob("tests/farm/*/*.py") + + +@pytest.mark.parametrize("filename", TEST_FILES) +def test_farm(filename): + if env.JYTHON: + # All of the farm tests use reporting, so skip them all. + skip("Farm tests don't run on Jython") + FarmTestCase(filename).run_fully() # "rU" was deprecated in 3.4 @@ -51,8 +57,7 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): cleaning-only, or run and leave the results for debugging). This class is a unittest.TestCase so that we can use behavior-modifying - mixins, but it's only useful as a nose test function. Yes, this is - confusing. + mixins, but it's only useful as a test function. Yes, this is confusing. """ @@ -75,38 +80,38 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): self.ok = True def setUp(self): - """Test set up, run by nose before __call__.""" + """Test set up, run by the test runner before __call__.""" super(FarmTestCase, self).setUp() # Modules should be importable from the current directory. sys.path.insert(0, '') def tearDown(self): - """Test tear down, run by nose after __call__.""" + """Test tear down, run by the test runner after __call__.""" # Make sure the test is cleaned up, unless we never want to, or if the # test failed. - if not self.dont_clean and self.ok: # pragma: part covered + if not self.dont_clean and self.ok: # pragma: part covered self.clean_only = True self() super(FarmTestCase, self).tearDown() - # This object will be run by nose via the __call__ method, and nose - # doesn't do cleanups in that case. Do them now. + # This object will be run via the __call__ method, and test runners + # don't do cleanups in that case. Do them now. self.doCleanups() - def runTest(self): + def runTest(self): # pragma: not covered """Here to make unittest.TestCase happy, but will never be invoked.""" raise Exception("runTest isn't used in this class!") def __call__(self): """Execute the test from the run.py file.""" - if _TEST_NAME_FILE: # pragma: debugging + if _TEST_NAME_FILE: # pragma: debugging with open(_TEST_NAME_FILE, "w") as f: f.write(self.description.replace("/", "_")) # Prepare a dictionary of globals for the run.py files to use. fns = """ - copy run runfunc clean skip + copy run clean skip compare contains contains_any doesnt_contain """.split() if self.clean_only: @@ -114,7 +119,7 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): glo['clean'] = clean else: glo = dict((fn, globals()[fn]) for fn in fns) - if self.dont_clean: # pragma: not covered + if self.dont_clean: # pragma: debugging glo['clean'] = noop with change_dir(self.dir): @@ -124,7 +129,7 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): self.ok = False raise - def run_fully(self): # pragma: not covered + def run_fully(self): """Run as a full test case, with setUp and tearDown.""" self.setUp() try: @@ -142,8 +147,6 @@ def noop(*args_unused, **kwargs_unused): def copy(src, dst): """Copy a directory.""" - if os.path.exists(dst): - shutil.rmtree(dst) shutil.copytree(src, dst) @@ -168,37 +171,15 @@ def run(cmds, rundir="src", outfile=None): if outfile: fout.write(output) if retcode: - raise Exception("command exited abnormally") + raise Exception("command exited abnormally") # pragma: only failure finally: if outfile: fout.close() -def runfunc(fn, rundir="src", addtopath=None): - """Run a function. - - `fn` is a callable. - `rundir` is the directory in which to run the function. - - """ - with change_dir(rundir): - with saved_sys_path(): - if addtopath is not None: - sys.path.insert(0, addtopath) - fn() - - -def compare( - dir1, dir2, file_pattern=None, size_within=0, - left_extra=False, right_extra=False, scrubs=None -): +def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scrubs=None): """Compare files matching `file_pattern` in `dir1` and `dir2`. - `dir2` is interpreted as a prefix, with Python version numbers appended - to find the actual directory to compare with. "foo" will compare - against "foo_v241", "foo_v24", "foo_v2", or "foo", depending on which - directory is found first. - `size_within` is a percentage delta for the file sizes. If non-zero, then the file contents are not compared (since they are expected to often be different), but the file sizes must be within this amount. @@ -206,8 +187,7 @@ def compare( within 10 percent of each other to compare equal. `left_extra` true means the left directory can have extra files in it - without triggering an assertion. `right_extra` means the right - directory can. + without triggering an assertion. `scrubs` is a list of pairs, regexes to find and literal strings to replace them with to scrub the files of unimportant differences. @@ -216,15 +196,6 @@ def compare( matches. """ - # Search for a dir2 with a version suffix. - version_suff = ''.join(map(str, sys.version_info[:3])) - while version_suff: - trydir = dir2 + '_v' + version_suff - if os.path.exists(trydir): - dir2 = trydir - break - version_suff = version_suff[:-1] - assert os.path.exists(dir1), "Left directory missing: %s" % dir1 assert os.path.exists(dir2), "Right directory missing: %s" % dir2 @@ -248,9 +219,9 @@ def compare( if (big - little) / float(little) > size_within/100.0: # print "%d %d" % (big, little) # print "Left: ---\n%s\n-----\n%s" % (left, right) - wrong_size.append("%s (%s,%s)" % (f, size_l, size_r)) + wrong_size.append("%s (%s,%s)" % (f, size_l, size_r)) # pragma: only failure if wrong_size: - print("File sizes differ between %s and %s: %s" % ( + print("File sizes differ between %s and %s: %s" % ( # pragma: only failure dir1, dir2, ", ".join(wrong_size) )) @@ -270,7 +241,7 @@ def compare( if scrubs: left = scrub(left, scrubs) right = scrub(right, scrubs) - if left != right: + if left != right: # pragma: only failure text_diff.append(f) left = left.splitlines() right = right.splitlines() @@ -279,8 +250,7 @@ def compare( if not left_extra: assert not left_only, "Files in %s only: %s" % (dir1, left_only) - if not right_extra: - assert not right_only, "Files in %s only: %s" % (dir2, right_only) + assert not right_only, "Files in %s only: %s" % (dir2, right_only) def contains(filename, *strlist): @@ -308,7 +278,10 @@ def contains_any(filename, *strlist): for s in strlist: if s in text: return - assert False, "Missing content in %s: %r [1 of %d]" % (filename, strlist[0], len(strlist),) + + assert False, ( # pragma: only failure + "Missing content in %s: %r [1 of %d]" % (filename, strlist[0], len(strlist),) + ) def doesnt_contain(filename, *strlist): @@ -334,7 +307,7 @@ def clean(cleandir): if os.path.exists(cleandir): try: shutil.rmtree(cleandir) - except OSError: # pragma: not covered + except OSError: # pragma: cant happen if tries == 1: raise else: @@ -345,7 +318,7 @@ def clean(cleandir): def skip(msg=None): """Skip the current test.""" - raise SkipTest(msg) + raise unittest.SkipTest(msg) # Helpers @@ -371,12 +344,12 @@ def scrub(strdata, scrubs): """ for rgx_find, rgx_replace in scrubs: - strdata = re.sub(rgx_find, re.escape(rgx_replace), strdata) + strdata = re.sub(rgx_find, rgx_replace.replace("\\", "\\\\"), strdata) return strdata -def main(): # pragma: not covered - """Command-line access to test_farm. +def main(): # pragma: debugging + """Command-line access to farm tests. Commands: @@ -392,21 +365,19 @@ def main(): # pragma: not covered if op == 'run': # Run the test for real. - for test_case in sys.argv[2:]: - case = FarmTestCase(test_case) - case.run_fully() + for filename in sys.argv[2:]: + FarmTestCase(filename).run_fully() elif op == 'out': # Run the test, but don't clean up, so we can examine the output. - for test_case in sys.argv[2:]: - case = FarmTestCase(test_case, dont_clean=True) - case.run_fully() + for filename in sys.argv[2:]: + FarmTestCase(filename, dont_clean=True).run_fully() elif op == 'clean': # Run all the tests, but just clean. - for test in test_farm(clean_only=True): - test[0].run_fully() + for filename in TEST_FILES: + FarmTestCase(filename, clean_only=True).run_fully() else: print(main.__doc__) # So that we can run just one farm run.py at a time. -if __name__ == '__main__': +if __name__ == '__main__': # pragma: debugging main() diff --git a/tests/test_files.py b/tests/test_files.py index 2d22730..dadb22b 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -38,7 +38,7 @@ class FilesTest(CoverageTest): a1 = self.abs_path("sub/proj1/file1.py") a2 = self.abs_path("sub/proj2/file2.py") d = os.path.normpath("sub/proj1") - os.chdir(d) + self.chdir(d) files.set_relative_directory() self.assertEqual(files.relative_filename(a1), "file1.py") self.assertEqual(files.relative_filename(a2), a2) @@ -163,18 +163,18 @@ class PathAliasesTest(CoverageTest): """ self.assertEqual(aliases.map(inp), files.canonical_filename(out)) - def assert_not_mapped(self, aliases, inp): + def assert_unchanged(self, aliases, inp): """Assert that `inp` mapped through `aliases` is unchanged.""" self.assertEqual(aliases.map(inp), inp) def test_noop(self): aliases = PathAliases() - self.assert_not_mapped(aliases, '/ned/home/a.py') + self.assert_unchanged(aliases, '/ned/home/a.py') def test_nomatch(self): aliases = PathAliases() aliases.add('/home/*/src', './mysrc') - self.assert_not_mapped(aliases, '/home/foo/a.py') + self.assert_unchanged(aliases, '/home/foo/a.py') def test_wildcard(self): aliases = PathAliases() @@ -188,7 +188,7 @@ class PathAliasesTest(CoverageTest): def test_no_accidental_match(self): aliases = PathAliases() aliases.add('/home/*/src', './mysrc') - self.assert_not_mapped(aliases, '/home/foo/srcetc') + self.assert_unchanged(aliases, '/home/foo/srcetc') def test_multiple_patterns(self): aliases = PathAliases() @@ -284,4 +284,4 @@ class WindowsFileTest(CoverageTest): super(WindowsFileTest, self).setUp() def test_actual_path(self): - self.assertEquals(actual_path(r'c:\Windows'), actual_path(r'C:\wINDOWS')) + self.assertEqual(actual_path(r'c:\Windows'), actual_path(r'C:\wINDOWS')) diff --git a/tests/test_html.py b/tests/test_html.py index 1df602f..9bb8f39 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -6,6 +6,7 @@ import datetime import glob +import json import os import os.path import re @@ -46,7 +47,7 @@ class HtmlTestHelpers(CoverageTest): self.clean_local_file_imports() cov = coverage.Coverage(**(covargs or {})) self.start_import_stop(cov, "main_file") - cov.html_report(**(htmlargs or {})) + return cov.html_report(**(htmlargs or {})) def remove_html_files(self): """Remove the HTML files created as part of the HTML report.""" @@ -101,11 +102,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): # At least one of our tests monkey-patches the version of coverage.py, # so grab it here to restore it later. self.real_coverage_version = coverage.__version__ - self.addCleanup(self.cleanup_coverage_version) - - def cleanup_coverage_version(self): - """A cleanup.""" - coverage.__version__ = self.real_coverage_version + self.addCleanup(setattr, coverage, "__version__", self.real_coverage_version) def test_html_created(self): # Test basic HTML generation: files should be created. @@ -208,6 +205,44 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): fixed_index2 = index2.replace("XYZZY", self.real_coverage_version) self.assertMultiLineEqual(index1, fixed_index2) + def test_file_becomes_100(self): + self.create_initial_files() + self.run_coverage() + + # Now change a file and do it again + self.make_file("main_file.py", """\ + import helper1, helper2 + # helper1 is now 100% + helper1.func1(12) + helper1.func1(23) + """) + + self.run_coverage(htmlargs=dict(skip_covered=True)) + + # The 100% file, skipped, shouldn't be here. + self.assert_doesnt_exist("htmlcov/helper1_py.html") + + def test_status_format_change(self): + self.create_initial_files() + self.run_coverage() + self.remove_html_files() + + with open("htmlcov/status.json") as status_json: + status_data = json.load(status_json) + + self.assertEqual(status_data['format'], 1) + status_data['format'] = 2 + with open("htmlcov/status.json", "w") as status_json: + json.dump(status_data, status_json) + + self.run_coverage() + + # All the files have been reported again. + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/helper1_py.html") + self.assert_exists("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/helper2_py.html") + class HtmlTitleTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML title support.""" @@ -260,18 +295,20 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): """Test the behavior when measuring unparsable files.""" def test_dotpy_not_python(self): + self.make_file("main.py", "import innocuous") self.make_file("innocuous.py", "a = 1") cov = coverage.Coverage() - self.start_import_stop(cov, "innocuous") + self.start_import_stop(cov, "main") self.make_file("innocuous.py", "<h1>This isn't python!</h1>") msg = "Couldn't parse '.*innocuous.py' as Python source: .* at line 1" with self.assertRaisesRegex(NotPython, msg): cov.html_report() def test_dotpy_not_python_ignored(self): + self.make_file("main.py", "import innocuous") self.make_file("innocuous.py", "a = 2") cov = coverage.Coverage() - self.start_import_stop(cov, "innocuous") + self.start_import_stop(cov, "main") self.make_file("innocuous.py", "<h1>This isn't python!</h1>") cov.html_report(ignore_errors=True) self.assertEqual( @@ -337,7 +374,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): self.make_file("main.py", "import sub.not_ascii") self.make_file("sub/__init__.py") self.make_file("sub/not_ascii.py", """\ - # coding: utf8 + # coding: utf-8 a = 1 # Isn't this great?! """) cov = coverage.Coverage() @@ -346,7 +383,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): # Create the undecodable version of the file. make_file is too helpful, # so get down and dirty with bytes. with open("sub/not_ascii.py", "wb") as f: - f.write(b"# coding: utf8\na = 1 # Isn't this great?\xcb!\n") + f.write(b"# coding: utf-8\na = 1 # Isn't this great?\xcb!\n") with open("sub/not_ascii.py", "rb") as f: undecodable = f.read() @@ -425,18 +462,58 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): self.run_coverage() self.assert_exists("htmlcov/status.dat") + def test_report_skip_covered_no_branches(self): + self.make_file("main_file.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + self.run_coverage(htmlargs=dict(skip_covered=True)) + self.assert_exists("htmlcov/index.html") + self.assert_doesnt_exist("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/not_covered_py.html") + + def test_report_skip_covered_100(self): + self.make_file("main_file.py", """ + def normal(): + print("z") + normal() + """) + res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) + self.assertEqual(res, 100.0) + self.assert_doesnt_exist("htmlcov/main_file_py.html") + + def test_report_skip_covered_branches(self): + self.make_file("main_file.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True)) + self.assert_exists("htmlcov/index.html") + self.assert_doesnt_exist("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/not_covered_py.html") + class HtmlStaticFileTest(CoverageTest): """Tests of the static file copying for the HTML report.""" def setUp(self): super(HtmlStaticFileTest, self).setUp() - self.original_path = list(coverage.html.STATIC_PATH) - self.addCleanup(self.cleanup_static_path) - - def cleanup_static_path(self): - """A cleanup.""" - coverage.html.STATIC_PATH = self.original_path + original_path = list(coverage.html.STATIC_PATH) + self.addCleanup(setattr, coverage.html, 'STATIC_PATH', original_path) def test_copying_static_files_from_system(self): # Make a new place for static files. @@ -686,7 +763,7 @@ class HtmlGoldTests(CoverageGoldTest): with change_dir("src"): # pylint: disable=import-error - cov = coverage.Coverage(branch=True) + cov = coverage.Coverage(config_file="partial.ini") cov.start() import partial # pragma: nested cov.stop() # pragma: nested @@ -700,6 +777,8 @@ class HtmlGoldTests(CoverageGoldTest): '<p id="t14" class="stm run hide_run">', # The "if 0" and "if 1" statements are optimized away. '<p id="t17" class="pln">', + # The "raise AssertionError" is excluded by regex in the .ini. + '<p id="t24" class="exc">', ) contains( "out/partial/index.html", diff --git a/tests/test_misc.py b/tests/test_misc.py index 38be345..939b1c9 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -3,11 +3,10 @@ """Tests of miscellaneous stuff.""" -import sys +import pytest -import coverage -from coverage.version import _make_url, _make_version -from coverage.misc import Hasher, file_be_gone +from coverage.misc import contract, dummy_decorator_with_args, file_be_gone +from coverage.misc import format_lines, Hasher, one_of from tests.coveragetest import CoverageTest @@ -34,6 +33,13 @@ class HasherTest(CoverageTest): h2.update(b"Goodbye!") self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + def test_unicode_hashing(self): + h1 = Hasher() + h1.update(u"Hello, world! \N{SNOWMAN}") + h2 = Hasher() + h2.update(u"Goodbye!") + self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + def test_dict_hashing(self): h1 = Hasher() h1.update({'a': 17, 'b': 23}) @@ -62,63 +68,63 @@ class RemoveFileTest(CoverageTest): file_be_gone(".") -class VersionTest(CoverageTest): - """Tests of version.py""" - - run_in_temp_dir = False - - def test_version_info(self): - # Make sure we didn't screw up the version_info tuple. - self.assertIsInstance(coverage.version_info, tuple) - self.assertEqual([type(d) for d in coverage.version_info], [int, int, int, str, int]) - self.assertIn(coverage.version_info[3], ['alpha', 'beta', 'candidate', 'final']) - - def test_make_version(self): - self.assertEqual(_make_version(4, 0, 0, 'alpha', 0), "4.0a0") - self.assertEqual(_make_version(4, 0, 0, 'alpha', 1), "4.0a1") - self.assertEqual(_make_version(4, 0, 0, 'final', 0), "4.0") - self.assertEqual(_make_version(4, 1, 2, 'beta', 3), "4.1.2b3") - self.assertEqual(_make_version(4, 1, 2, 'final', 0), "4.1.2") - self.assertEqual(_make_version(5, 10, 2, 'candidate', 7), "5.10.2rc7") - - def test_make_url(self): - self.assertEqual( - _make_url(4, 0, 0, 'final', 0), - "https://coverage.readthedocs.io" - ) - self.assertEqual( - _make_url(4, 1, 2, 'beta', 3), - "https://coverage.readthedocs.io/en/coverage-4.1.2b3" - ) - - -class SetupPyTest(CoverageTest): - """Tests of setup.py""" +class ContractTest(CoverageTest): + """Tests of our contract decorators.""" run_in_temp_dir = False - def test_metadata(self): - status, output = self.run_command_status( - "python setup.py --description --version --url --author" - ) - self.assertEqual(status, 0) - out = output.splitlines() - self.assertIn("measurement", out[0]) - self.assertEqual(out[1], coverage.__version__) - self.assertEqual(out[2], coverage.__url__) - self.assertIn("Ned Batchelder", out[3]) - - def test_more_metadata(self): - # Let's be sure we pick up our own setup.py - # CoverageTest restores the original sys.path for us. - sys.path.insert(0, '') - from setup import setup_args - - classifiers = setup_args['classifiers'] - self.assertGreater(len(classifiers), 7) - self.assert_starts_with(classifiers[-1], "Development Status ::") - - long_description = setup_args['long_description'].splitlines() - self.assertGreater(len(long_description), 7) - self.assertNotEqual(long_description[0].strip(), "") - self.assertNotEqual(long_description[-1].strip(), "") + def test_bytes(self): + @contract(text='bytes|None') + def need_bytes(text=None): # pylint: disable=missing-docstring + return text + + assert need_bytes(b"Hey") == b"Hey" + assert need_bytes() is None + with pytest.raises(Exception): + need_bytes(u"Oops") + + def test_unicode(self): + @contract(text='unicode|None') + def need_unicode(text=None): # pylint: disable=missing-docstring + return text + + assert need_unicode(u"Hey") == u"Hey" + assert need_unicode() is None + with pytest.raises(Exception): + need_unicode(b"Oops") + + def test_one_of(self): + @one_of("a, b, c") + def give_me_one(a=None, b=None, c=None): # pylint: disable=missing-docstring + return (a, b, c) + + assert give_me_one(a=17) == (17, None, None) + assert give_me_one(b=set()) == (None, set(), None) + assert give_me_one(c=17) == (None, None, 17) + with pytest.raises(AssertionError): + give_me_one(a=17, b=set()) + with pytest.raises(AssertionError): + give_me_one() + + def test_dummy_decorator_with_args(self): + @dummy_decorator_with_args("anything", this=17, that="is fine") + def undecorated(a=None, b=None): # pylint: disable=missing-docstring + return (a, b) + + assert undecorated() == (None, None) + assert undecorated(17) == (17, None) + assert undecorated(b=23) == (None, 23) + assert undecorated(b=42, a=3) == (3, 42) + + +@pytest.mark.parametrize("statements, lines, result", [ + (set([1,2,3,4,5,10,11,12,13,14]), set([1,2,5,10,11,13,14]), "1-2, 5-11, 13-14"), + ([1,2,3,4,5,10,11,12,13,14,98,99], [1,2,5,10,11,13,14,99], "1-2, 5-11, 13-14, 99"), + ([1,2,3,4,98,99,100,101,102,103,104], [1,2,99,102,103,104], "1-2, 99, 102-104"), + ([17], [17], "17"), + ([90,91,92,93,94,95], [90,91,92,93,94,95], "90-95"), + ([1, 2, 3, 4, 5], [], ""), + ([1, 2, 3, 4, 5], [4], "4"), +]) +def test_format_lines(statements, lines, result): + assert format_lines(statements, lines) == result diff --git a/tests/test_oddball.py b/tests/test_oddball.py index 87c65b0..b54f4ef 100644 --- a/tests/test_oddball.py +++ b/tests/test_oddball.py @@ -5,7 +5,12 @@ import sys +from flaky import flaky +import pytest + import coverage +from coverage import env +from coverage.backward import import_local_file from coverage.files import abs_file from tests.coveragetest import CoverageTest @@ -115,7 +120,7 @@ class RecursionTest(CoverageTest): pytrace = (cov.collector.tracer_name() == "PyTracer") expected_missing = [3] - if pytrace: + if pytrace: # pragma: no metacov expected_missing += [9, 10, 11] _, statements, missing, _ = cov.analysis("recur.py") @@ -123,7 +128,7 @@ class RecursionTest(CoverageTest): self.assertEqual(missing, expected_missing) # Get a warning about the stackoverflow effect on the tracing function. - if pytrace: + if pytrace: # pragma: no metacov self.assertEqual(cov._warnings, ["Trace function changed, measurement is likely wrong: None"] ) @@ -140,7 +145,11 @@ class MemoryLeakTest(CoverageTest): It may still fail occasionally, especially on PyPy. """ + @flaky def test_for_leaks(self): + if env.JYTHON: + self.skipTest("Don't bother on Jython") + # Our original bad memory leak only happened on line numbers > 255, so # make a code object with more lines than that. Ugly string mumbo # jumbo to get 300 blank lines at the beginning.. @@ -176,17 +185,56 @@ class MemoryLeakTest(CoverageTest): # Running the code 10k times shouldn't grow the ram much more than # running it 10 times. ram_growth = (ram_10k - ram_10) - (ram_10 - ram_0) - if ram_growth > 100000: # pragma: only failure - fails += 1 - - if fails > 8: # pragma: only failure - self.fail("RAM grew by %d" % (ram_growth)) + if ram_growth > 100000: + fails += 1 # pragma: only failure + + if fails > 8: + self.fail("RAM grew by %d" % (ram_growth)) # pragma: only failure + + +class MemoryFumblingTest(CoverageTest): + """Test that we properly manage the None refcount.""" + + def test_dropping_none(self): # pragma: not covered + if not env.C_TRACER: + self.skipTest("Only the C tracer has refcounting issues") + # TODO: Mark this so it will only be run sometimes. + self.skipTest("This is too expensive for now (30s)") + # Start and stop coverage thousands of times to flush out bad + # reference counting, maybe. + self.make_file("the_code.py", """\ + import random + def f(): + if random.random() > .5: + x = 1 + else: + x = 2 + """) + self.make_file("main.py", """\ + import coverage + import sys + from the_code import f + for i in range(10000): + cov = coverage.Coverage(branch=True) + cov.start() + f() + cov.stop() + cov.erase() + print("Final None refcount: %d" % (sys.getrefcount(None))) + """) + status, out = self.run_command_status("python main.py") + self.assertEqual(status, 0) + self.assertIn("Final None refcount", out) + self.assertNotIn("Fatal", out) class PyexpatTest(CoverageTest): """Pyexpat screws up tracing. Make sure we've counter-defended properly.""" def test_pyexpat(self): + if env.JYTHON: + self.skipTest("Pyexpat isn't a problem on Jython") + # pyexpat calls the trace function explicitly (inexplicably), and does # it wrong for exceptions. Parsing a DOCTYPE for some reason throws # an exception internally, and triggers its wrong behavior. This test @@ -286,7 +334,7 @@ class ExceptionTest(CoverageTest): # Import all the modules before starting coverage, so the def lines # won't be in all the results. for mod in "oops fly catch doit".split(): - self.import_local_file(mod) + import_local_file(mod) # Each run nests the functions differently to get different # combinations of catching exceptions and letting them fly. @@ -337,6 +385,13 @@ class ExceptionTest(CoverageTest): lines = data.lines(abs_file(filename)) clean_lines[filename] = sorted(lines) + if env.JYTHON: # pragma: only jython + # Jython doesn't report on try or except lines, so take those + # out of the expected lines. + invisible = [202, 206, 302, 304] + for lines in lines_expected.values(): + lines[:] = [l for l in lines if l not in invisible] + self.assertEqual(clean_lines, lines_expected) @@ -346,14 +401,11 @@ class DoctestTest(CoverageTest): def setUp(self): super(DoctestTest, self).setUp() - # Oh, the irony! This test case exists because Python 2.4's - # doctest module doesn't play well with coverage. But nose fixes - # the problem by monkeypatching doctest. I want to undo the - # monkeypatch to be sure I'm getting the doctest module that users - # of coverage will get. Deleting the imported module here is - # enough: when the test imports doctest again, it will get a fresh - # copy without the monkeypatch. - del sys.modules['doctest'] + # This test case exists because Python 2.4's doctest module didn't play + # well with coverage. Nose fixes the problem by monkeypatching doctest. + # I want to be sure there's no monkeypatch and that I'm getting the + # doctest module that users of coverage will get. + assert 'doctest' not in sys.modules def test_doctest(self): self.check_coverage('''\ @@ -380,35 +432,38 @@ class DoctestTest(CoverageTest): class GettraceTest(CoverageTest): """Tests that we work properly with `sys.gettrace()`.""" - def test_round_trip(self): - self.check_coverage('''\ - import sys - def foo(n): - return 3*n - def bar(n): - return 5*n - a = foo(6) - sys.settrace(sys.gettrace()) - a = bar(8) - ''', - [1, 2, 3, 4, 5, 6, 7, 8], "") - - def test_multi_layers(self): - self.check_coverage('''\ + def test_round_trip_in_untraced_function(self): + # https://bitbucket.org/ned/coveragepy/issues/575/running-doctest-prevents-complete-coverage + self.make_file("main.py", """import sample""") + self.make_file("sample.py", """\ + from swap import swap_it + def doit(): + print(3) + swap_it() + print(5) + def doit_soon(): + print(7) + doit() + print(9) + print(10) + doit_soon() + print(12) + """) + self.make_file("swap.py", """\ import sys - def level1(): - a = 3 - level2() - b = 5 - def level2(): - c = 7 + def swap_it(): sys.settrace(sys.gettrace()) - d = 9 - e = 10 - level1() - f = 12 - ''', - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "") + """) + + # Use --source=sample to prevent measurement of swap.py. + cov = coverage.Coverage(source=["sample"]) + self.start_import_stop(cov, "main") + + self.assertEqual(self.stdout(), "10\n7\n3\n5\n9\n12\n") + + _, statements, missing, _ = cov.analysis("sample.py") + self.assertEqual(statements, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + self.assertEqual(missing, []) def test_setting_new_trace_function(self): # https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from @@ -433,8 +488,10 @@ class GettraceTest(CoverageTest): old = sys.gettrace() test_unsets_trace() sys.settrace(old) + a = 21 + b = 22 ''', - lines=[1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20], + lines=[1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22], missing="4-5, 11-12", ) @@ -450,6 +507,38 @@ class GettraceTest(CoverageTest): ), ) + @pytest.mark.expensive + def test_atexit_gettrace(self): # pragma: no metacov + # This is not a test of coverage at all, but of our understanding + # of this edge-case behavior in various Pythons. + if env.METACOV: + self.skipTest("Can't set trace functions during meta-coverage") + + self.make_file("atexit_gettrace.py", """\ + import atexit, sys + + def trace_function(frame, event, arg): + return trace_function + sys.settrace(trace_function) + + def show_trace_function(): + tfn = sys.gettrace() + if tfn is not None: + tfn = tfn.__name__ + print(tfn) + atexit.register(show_trace_function) + + # This will show what the trace function is at the end of the program. + """) + status, out = self.run_command_status("python atexit_gettrace.py") + self.assertEqual(status, 0) + if env.PYPY and env.PYPYVERSION >= (5, 4): + # Newer PyPy clears the trace function before atexit runs. + self.assertEqual(out, "None\n") + else: + # Other Pythons leave the trace function in place. + self.assertEqual(out, "trace_function\n") + class ExecTest(CoverageTest): """Tests of exec.""" diff --git a/tests/test_parser.py b/tests/test_parser.py index 5fee823..aa96b59 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -193,7 +193,7 @@ class ParserMissingArcDescriptionTest(CoverageTest): def parse_text(self, source): """Parse Python source, and return the parser object.""" - parser = PythonParser(textwrap.dedent(source)) + parser = PythonParser(text=textwrap.dedent(source)) parser.parse_source() return parser diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py index ccbe01b..ddb652e 100644 --- a/tests/test_phystokens.py +++ b/tests/test_phystokens.py @@ -5,6 +5,7 @@ import os.path import re +import textwrap from coverage import env from coverage.phystokens import source_token_lines, source_encoding @@ -14,18 +15,35 @@ from coverage.python import get_python_source from tests.coveragetest import CoverageTest +# A simple program and its token stream. SIMPLE = u"""\ # yay! def foo(): say('two = %d' % 2) """ +SIMPLE_TOKENS = [ + [('com', "# yay!")], + [('key', 'def'), ('ws', ' '), ('nam', 'foo'), ('op', '('), ('op', ')'), ('op', ':')], + [('ws', ' '), ('nam', 'say'), ('op', '('), + ('str', "'two = %d'"), ('ws', ' '), ('op', '%'), + ('ws', ' '), ('num', '2'), ('op', ')')], +] + +# Mixed-whitespace program, and its token stream. MIXED_WS = u"""\ def hello(): a="Hello world!" \tb="indented" """ +MIXED_WS_TOKENS = [ + [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ('op', ')'), ('op', ':')], + [('ws', ' '), ('nam', 'a'), ('op', '='), ('str', '"Hello world!"')], + [('ws', ' '), ('nam', 'b'), ('op', '='), ('str', '"indented"')], +] + +# Where this file is, so we can find other files next to it. HERE = os.path.dirname(__file__) @@ -52,28 +70,16 @@ class PhysTokensTest(CoverageTest): self.check_tokenization(get_python_source(fname)) def test_simple(self): - self.assertEqual(list(source_token_lines(SIMPLE)), - [ - [('com', "# yay!")], - [('key', 'def'), ('ws', ' '), ('nam', 'foo'), ('op', '('), - ('op', ')'), ('op', ':')], - [('ws', ' '), ('nam', 'say'), ('op', '('), - ('str', "'two = %d'"), ('ws', ' '), ('op', '%'), - ('ws', ' '), ('num', '2'), ('op', ')')] - ]) + self.assertEqual(list(source_token_lines(SIMPLE)), SIMPLE_TOKENS) self.check_tokenization(SIMPLE) + def test_missing_final_newline(self): + # We can tokenize source that is missing the final newline. + self.assertEqual(list(source_token_lines(SIMPLE.rstrip())), SIMPLE_TOKENS) + def test_tab_indentation(self): # Mixed tabs and spaces... - self.assertEqual(list(source_token_lines(MIXED_WS)), - [ - [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), - ('op', ')'), ('op', ':')], - [('ws', ' '), ('nam', 'a'), ('op', '='), - ('str', '"Hello world!"')], - [('ws', ' '), ('nam', 'b'), ('op', '='), - ('str', '"indented"')], - ]) + self.assertEqual(list(source_token_lines(MIXED_WS)), MIXED_WS_TOKENS) def test_tokenize_real_file(self): # Check the tokenization of a real file (large, btw). @@ -97,13 +103,15 @@ else: ENCODING_DECLARATION_SOURCES = [ # Various forms from http://www.python.org/dev/peps/pep-0263/ - (1, b"# coding=cp850\n\n"), - (1, b"#!/usr/bin/python\n# -*- coding: cp850 -*-\n"), - (1, b"#!/usr/bin/python\n# vim: set fileencoding=cp850:\n"), - (1, b"# This Python file uses this encoding: cp850\n"), - (1, b"# This file uses a different encoding:\n# coding: cp850\n"), - (1, b"\n# coding=cp850\n\n"), - (2, b"# -*- coding:cp850 -*-\n# vim: fileencoding=cp850\n"), + (1, b"# coding=cp850\n\n", "cp850"), + (1, b"# coding=latin-1\n", "iso-8859-1"), + (1, b"# coding=iso-latin-1\n", "iso-8859-1"), + (1, b"#!/usr/bin/python\n# -*- coding: cp850 -*-\n", "cp850"), + (1, b"#!/usr/bin/python\n# vim: set fileencoding=cp850:\n", "cp850"), + (1, b"# This Python file uses this encoding: cp850\n", "cp850"), + (1, b"# This file uses a different encoding:\n# coding: cp850\n", "cp850"), + (1, b"\n# coding=cp850\n\n", "cp850"), + (2, b"# -*- coding:cp850 -*-\n# vim: fileencoding=cp850\n", "cp850"), ] class SourceEncodingTest(CoverageTest): @@ -112,15 +120,15 @@ class SourceEncodingTest(CoverageTest): run_in_temp_dir = False def test_detect_source_encoding(self): - for _, source in ENCODING_DECLARATION_SOURCES: + for _, source, expected in ENCODING_DECLARATION_SOURCES: self.assertEqual( source_encoding(source), - 'cp850', + expected, "Wrong encoding in %r" % source ) def test_detect_source_encoding_not_in_comment(self): - if env.PYPY and env.PY3: + if env.PYPY and env.PY3: # pragma: no metacov # PyPy3 gets this case wrong. Not sure what I can do about it, # so skip the test. self.skipTest("PyPy3 is wrong about non-comment encoding. Skip it.") @@ -142,9 +150,19 @@ class SourceEncodingTest(CoverageTest): source = b"\xEF\xBB\xBFtext = 'hello'\n" self.assertEqual(source_encoding(source), 'utf-8-sig') - # But it has to be the only authority. + def test_bom_with_encoding(self): + source = b"\xEF\xBB\xBF# coding: utf-8\ntext = 'hello'\n" + self.assertEqual(source_encoding(source), 'utf-8-sig') + + def test_bom_is_wrong(self): + # A BOM with an explicit non-utf8 encoding is an error. source = b"\xEF\xBB\xBF# coding: cp850\n" - with self.assertRaises(SyntaxError): + with self.assertRaisesRegex(SyntaxError, "encoding problem: utf-8"): + source_encoding(source) + + def test_unknown_encoding(self): + source = b"# coding: klingon\n" + with self.assertRaisesRegex(SyntaxError, "unknown encoding: klingon"): source_encoding(source) @@ -154,7 +172,7 @@ class NeuterEncodingDeclarationTest(CoverageTest): run_in_temp_dir = False def test_neuter_encoding_declaration(self): - for lines_diff_expected, source in ENCODING_DECLARATION_SOURCES: + for lines_diff_expected, source, _ in ENCODING_DECLARATION_SOURCES: neutered = neuter_encoding_declaration(source.decode("ascii")) neutered = neutered.encode("ascii") @@ -177,6 +195,67 @@ class NeuterEncodingDeclarationTest(CoverageTest): "Wrong encoding in %r" % neutered ) + def test_two_encoding_declarations(self): + input_src = textwrap.dedent(u"""\ + # -*- coding: ascii -*- + # -*- coding: utf-8 -*- + # -*- coding: utf-16 -*- + """) + expected_src = textwrap.dedent(u"""\ + # (deleted declaration) -*- + # (deleted declaration) -*- + # -*- coding: utf-16 -*- + """) + output_src = neuter_encoding_declaration(input_src) + self.assertEqual(expected_src, output_src) + + def test_one_encoding_declaration(self): + input_src = textwrap.dedent(u"""\ + # -*- coding: utf-16 -*- + # Just a comment. + # -*- coding: ascii -*- + """) + expected_src = textwrap.dedent(u"""\ + # (deleted declaration) -*- + # Just a comment. + # -*- coding: ascii -*- + """) + output_src = neuter_encoding_declaration(input_src) + self.assertEqual(expected_src, output_src) + + +class Bug529Test(CoverageTest): + """Test of bug 529""" + + def test_bug_529(self): + # Don't over-neuter coding declarations. This happened with a test + # file which contained code in multi-line strings, all with coding + # declarations. The neutering of the file also changed the multi-line + # strings, which it shouldn't have. + self.make_file("the_test.py", '''\ + # -*- coding: utf-8 -*- + import unittest + class Bug529Test(unittest.TestCase): + def test_two_strings_are_equal(self): + src1 = u"""\\ + # -*- coding: utf-8 -*- + # Just a comment. + """ + src2 = u"""\\ + # -*- coding: utf-8 -*- + # Just a comment. + """ + self.assertEqual(src1, src2) + + if __name__ == "__main__": + unittest.main() + ''') + status, out = self.run_command_status("coverage run the_test.py") + self.assertEqual(status, 0) + self.assertIn("OK", out) + # If this test fails, the output will be super-confusing, because it + # has a failing unit test contained within the failing unit test. + class CompileUnicodeTest(CoverageTest): """Tests of compiling Unicode strings.""" diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 8ea0a8f..5486216 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -333,9 +333,8 @@ class GoodPluginTest(FileTracerTest): # quux_5.html will be omitted from the results. assert render("quux_5.html", 3) == "[quux_5.html @ 3]" - # In Python 2, either kind of string should be OK. - if sys.version_info[0] == 2: - assert render(u"uni_3.html", 2) == "[uni_3.html @ 2]" + # For Python 2, make sure unicode is working. + assert render(u"uni_3.html", 2) == "[uni_3.html @ 2]" """) # will try to read the actual source files, so make some @@ -372,11 +371,10 @@ class GoodPluginTest(FileTracerTest): self.assertNotIn("quux_5.html", cov.data.line_counts()) - if env.PY2: - _, statements, missing, _ = cov.analysis("uni_3.html") - self.assertEqual(statements, [1, 2, 3]) - self.assertEqual(missing, [1]) - self.assertIn("uni_3.html", cov.data.line_counts()) + _, statements, missing, _ = cov.analysis("uni_3.html") + self.assertEqual(statements, [1, 2, 3]) + self.assertEqual(missing, [1]) + self.assertIn("uni_3.html", cov.data.line_counts()) def test_plugin2_with_branch(self): self.make_render_and_caller() @@ -507,6 +505,58 @@ class GoodPluginTest(FileTracerTest): self.assertEqual(report, expected) self.assertEqual(total, 50) + def test_find_unexecuted(self): + self.make_file("unexecuted_plugin.py", """\ + import os + import coverage.plugin + class Plugin(coverage.CoveragePlugin): + def file_tracer(self, filename): + if filename.endswith("foo.py"): + return MyTracer(filename) + def file_reporter(self, filename): + return MyReporter(filename) + def find_executable_files(self, src_dir): + # Check that src_dir is the right value + files = os.listdir(src_dir) + assert "foo.py" in files + assert "unexecuted_plugin.py" in files + return ["chimera.py"] + + class MyTracer(coverage.plugin.FileTracer): + def __init__(self, filename): + self.filename = filename + def source_filename(self): + return self.filename + def line_number_range(self, frame): + return (999, 999) + + class MyReporter(coverage.FileReporter): + def lines(self): + return set([99, 999, 9999]) + + def coverage_init(reg, options): + reg.add_file_tracer(Plugin()) + """) + self.make_file("foo.py", "a = 1\n") + cov = coverage.Coverage(source=['.']) + cov.set_option("run:plugins", ["unexecuted_plugin"]) + self.start_import_stop(cov, "foo") + + # The file we executed claims to have run line 999. + _, statements, missing, _ = cov.analysis("foo.py") + self.assertEqual(statements, [99, 999, 9999]) + self.assertEqual(missing, [99, 9999]) + + # The completely missing file is in the results. + _, statements, missing, _ = cov.analysis("chimera.py") + self.assertEqual(statements, [99, 999, 9999]) + self.assertEqual(missing, [99, 999, 9999]) + + # But completely new filenames are not in the results. + self.assertEqual(len(cov.get_data().measured_files()), 3) + with self.assertRaises(CoverageException): + cov.analysis("fictional.py") + class BadPluginTest(FileTracerTest): """Test error handling around plugins.""" @@ -542,7 +592,7 @@ class BadPluginTest(FileTracerTest): self.start_import_stop(cov, "simple") return cov - def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None): + def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None, excmsgs=None): """Run a file, and see that the plugin failed. `module_name` and `plugin_name` is the module and name of the plugin to @@ -551,7 +601,10 @@ class BadPluginTest(FileTracerTest): `our_error` is True if the error reported to the user will be an explicit error in our test code, marked with an '# Oh noes!' comment. - `excmsg`, if provided, is text that should appear in the stderr. + `excmsg`, if provided, is text that must appear in the stderr. + + `excmsgs`, if provided, is a list of messages, one of which must + appear in the stderr. The plugin will be disabled, and we check that a warning is output explaining why. @@ -560,7 +613,6 @@ class BadPluginTest(FileTracerTest): self.run_plugin(module_name) stderr = self.stderr() - print(stderr) # for diagnosing test failures. if our_error: errors = stderr.count("# Oh noes!") @@ -578,6 +630,8 @@ class BadPluginTest(FileTracerTest): if excmsg: self.assertIn(excmsg, stderr) + if excmsgs: + self.assertTrue(any(em in stderr for em in excmsgs), "expected one of %r" % excmsgs) def test_file_tracer_has_no_file_tracer_method(self): self.make_file("bad_plugin.py", """\ @@ -650,7 +704,9 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, excmsg="'float' object has no attribute", + ) def test_has_dynamic_source_filename_fails(self): self.make_file("bad_plugin.py", """\ @@ -698,7 +754,15 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, + excmsgs=[ + "expected str, bytes or os.PathLike object, not float", + "'float' object has no attribute", + "object of type 'float' has no len()", + "'float' object is unsubscriptable", + ], + ) def test_dynamic_source_filename_fails(self): self.make_file("bad_plugin.py", """\ @@ -737,7 +801,9 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple", + ) def test_line_number_range_returns_triple(self): self.make_file("bad_plugin.py", """\ @@ -757,7 +823,9 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple", + ) def test_line_number_range_returns_pair_of_strings(self): self.make_file("bad_plugin.py", """\ @@ -777,4 +845,6 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, excmsg="an integer is required", + ) diff --git a/tests/test_process.py b/tests/test_process.py index aa2045f..8a0f4e3 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1,9 +1,10 @@ -# coding: utf8 +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt """Tests for process behavior of coverage.py.""" +import distutils.sysconfig # pylint: disable=import-error import glob import os import os.path @@ -11,13 +12,14 @@ import re import sys import textwrap +import pytest + import coverage from coverage import env, CoverageData from coverage.misc import output_encoding from tests.coveragetest import CoverageTest - -TRY_EXECFILE = os.path.join(os.path.dirname(__file__), "modules/process_test/try_execfile.py") +from tests.helpers import re_lines class ProcessTest(CoverageTest): @@ -388,8 +390,7 @@ class ProcessTest(CoverageTest): out2 = self.run_command("python throw.py") if env.PYPY: # Pypy has an extra frame in the traceback for some reason - lines2 = out2.splitlines() - out2 = "".join(l+"\n" for l in lines2 if "toplevel" not in l) + out2 = re_lines(out2, "toplevel", match=False) self.assertMultiLineEqual(out, out2) # But also make sure that the output is what we expect. @@ -436,118 +437,6 @@ class ProcessTest(CoverageTest): self.assertEqual(status, status2) self.assertEqual(status, 0) - def assert_execfile_output(self, out): - """Assert that the output we got is a successful run of try_execfile.py""" - self.assertIn('"DATA": "xyzzy"', out) - - def test_coverage_run_is_like_python(self): - with open(TRY_EXECFILE) as f: - self.make_file("run_me.py", f.read()) - out_cov = self.run_command("coverage run run_me.py") - out_py = self.run_command("python run_me.py") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dashm_is_like_python_dashm(self): - # These -m commands assume the coverage tree is on the path. - out_cov = self.run_command("coverage run -m process_test.try_execfile") - out_py = self.run_command("python -m process_test.try_execfile") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dir_is_like_python_dir(self): - with open(TRY_EXECFILE) as f: - self.make_file("with_main/__main__.py", f.read()) - out_cov = self.run_command("coverage run with_main") - out_py = self.run_command("python with_main") - - # The coverage.py results are not identical to the Python results, and - # I don't know why. For now, ignore those failures. If someone finds - # a real problem with the discrepancies, we can work on it some more. - ignored = r"__file__|__loader__|__package__" - # PyPy includes the current directory in the path when running a - # directory, while CPython and coverage.py do not. Exclude that from - # the comparison also... - if env.PYPY: - ignored += "|"+re.escape(os.getcwd()) - out_cov = remove_matching_lines(out_cov, ignored) - out_py = remove_matching_lines(out_py, ignored) - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dashm_equal_to_doubledashsource(self): - """regression test for #328 - - When imported by -m, a module's __name__ is __main__, but we need the - --source machinery to know and respect the original name. - """ - # These -m commands assume the coverage tree is on the path. - out_cov = self.run_command( - "coverage run --source process_test.try_execfile -m process_test.try_execfile" - ) - out_py = self.run_command("python -m process_test.try_execfile") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dashm_superset_of_doubledashsource(self): - """Edge case: --source foo -m foo.bar""" - # These -m commands assume the coverage tree is on the path. - out_cov = self.run_command( - "coverage run --source process_test -m process_test.try_execfile" - ) - out_py = self.run_command("python -m process_test.try_execfile") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual(self.line_count(out), 6, out) - - def test_coverage_run_script_imports_doubledashsource(self): - # This file imports try_execfile, which compiles it to .pyc, so the - # first run will have __file__ == "try_execfile.py" and the second will - # have __file__ == "try_execfile.pyc", which throws off the comparison. - # Setting dont_write_bytecode True stops the compilation to .pyc and - # keeps the test working. - self.make_file("myscript", """\ - import sys; sys.dont_write_bytecode = True - import process_test.try_execfile - """) - - # These -m commands assume the coverage tree is on the path. - out_cov = self.run_command( - "coverage run --source process_test myscript" - ) - out_py = self.run_command("python myscript") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual(self.line_count(out), 6, out) - - def test_coverage_run_dashm_is_like_python_dashm_off_path(self): - # https://bitbucket.org/ned/coveragepy/issue/242 - self.make_file("sub/__init__.py", "") - with open(TRY_EXECFILE) as f: - self.make_file("sub/run_me.py", f.read()) - out_cov = self.run_command("coverage run -m sub.run_me") - out_py = self.run_command("python -m sub.run_me") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self): - if sys.version_info < (2, 7): - # Coverage.py isn't bug-for-bug compatible in the behavior of -m for - # Pythons < 2.7 - self.skipTest("-m doesn't work the same < Python 2.7") - # https://bitbucket.org/ned/coveragepy/issue/207 - self.make_file("package/__init__.py", "print('init')") - self.make_file("package/__main__.py", "print('main')") - out_cov = self.run_command("coverage run -m package") - out_py = self.run_command("python -m package") - self.assertMultiLineEqual(out_cov, out_py) - def test_fork(self): if not hasattr(os, 'fork'): self.skipTest("Can't test os.fork since it doesn't exist.") @@ -595,21 +484,6 @@ class ProcessTest(CoverageTest): data.read_file(".coverage") self.assertEqual(data.line_counts()['fork.py'], 9) - def test_warnings(self): - self.make_file("hello.py", """\ - import sys, os - print("Hello") - """) - out = self.run_command("coverage run --source=sys,xyzzy,quux hello.py") - - self.assertIn("Hello\n", out) - self.assertIn(textwrap.dedent("""\ - Coverage.py warning: Module sys has no Python source. - Coverage.py warning: Module xyzzy was never imported. - Coverage.py warning: Module quux was never imported. - Coverage.py warning: No data was collected. - """), out) - def test_warnings_during_reporting(self): # While fixing issue #224, the warnings were being printed far too # often. Make sure they're not any more. @@ -692,7 +566,8 @@ class ProcessTest(CoverageTest): self.assertEqual(len(infos), 1) self.assertEqual(infos[0]['note'], u"These are musical notes: ♫𝅗𝅥♩") - def test_fullcoverage(self): # pragma: not covered + @pytest.mark.expensive + def test_fullcoverage(self): # pragma: no metacov if env.PY2: # This doesn't work on Python 2. self.skipTest("fullcoverage doesn't work on Python 2.") # It only works with the C tracer, and if we aren't measuring ourselves. @@ -721,6 +596,26 @@ class ProcessTest(CoverageTest): # about 5. self.assertGreater(data.line_counts()['os.py'], 50) + def test_lang_c(self): + if env.PY3 and sys.version_info < (3, 4): + # Python 3.3 can't compile the non-ascii characters in the file name. + self.skipTest("3.3 can't handle this test") + # LANG=C forces getfilesystemencoding on Linux to 'ascii', which causes + # failures with non-ascii file names. We don't want to make a real file + # with strange characters, though, because that gets the test runners + # tangled up. This will isolate the concerns to the coverage.py code. + # https://bitbucket.org/ned/coveragepy/issues/533/exception-on-unencodable-file-name + self.make_file("weird_file.py", r""" + globs = {} + code = "a = 1\nb = 2\n" + exec(compile(code, "wut\xe9\xea\xeb\xec\x01\x02.py", 'exec'), globs) + print(globs['a']) + print(globs['b']) + """) + self.set_environ("LANG", "C") + out = self.run_command("coverage run weird_file.py") + self.assertEqual(out, "1\n2\n") + def test_deprecation_warnings(self): # Test that coverage doesn't trigger deprecation warnings. # https://bitbucket.org/ned/coveragepy/issue/305/pendingdeprecationwarning-the-imp-module @@ -753,7 +648,8 @@ class ProcessTest(CoverageTest): out = self.run_command("python run_twice.py") self.assertEqual( out, - "Coverage.py warning: Module foo was previously imported, but not measured.\n" + "Coverage.py warning: Module foo was previously imported, but not measured. " + "(module-not-measured)\n" ) def test_module_name(self): @@ -766,11 +662,223 @@ class ProcessTest(CoverageTest): self.assertIn("Use 'coverage help' for help", out) +TRY_EXECFILE = os.path.join(os.path.dirname(__file__), "modules/process_test/try_execfile.py") + +class EnvironmentTest(CoverageTest): + """Tests using try_execfile.py to test the execution environment.""" + + def assert_tryexecfile_output(self, out1, out2): + """Assert that the output we got is a successful run of try_execfile.py. + + `out1` and `out2` must be the same, modulo a few slight known platform + differences. + + """ + # First, is this even credible try_execfile.py output? + self.assertIn('"DATA": "xyzzy"', out1) + + if env.JYTHON: # pragma: only jython + # Argv0 is different for Jython, remove that from the comparison. + out1 = re_lines(out1, r'\s+"argv0":', match=False) + out2 = re_lines(out2, r'\s+"argv0":', match=False) + + self.assertMultiLineEqual(out1, out2) + + def test_coverage_run_is_like_python(self): + with open(TRY_EXECFILE) as f: + self.make_file("run_me.py", f.read()) + out_cov = self.run_command("coverage run run_me.py") + out_py = self.run_command("python run_me.py") + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dashm_is_like_python_dashm(self): + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command("coverage run -m process_test.try_execfile") + out_py = self.run_command("python -m process_test.try_execfile") + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dir_is_like_python_dir(self): + with open(TRY_EXECFILE) as f: + self.make_file("with_main/__main__.py", f.read()) + + out_cov = self.run_command("coverage run with_main") + out_py = self.run_command("python with_main") + + # The coverage.py results are not identical to the Python results, and + # I don't know why. For now, ignore those failures. If someone finds + # a real problem with the discrepancies, we can work on it some more. + ignored = r"__file__|__loader__|__package__" + # PyPy includes the current directory in the path when running a + # directory, while CPython and coverage.py do not. Exclude that from + # the comparison also... + if env.PYPY: + ignored += "|"+re.escape(os.getcwd()) + out_cov = re_lines(out_cov, ignored, match=False) + out_py = re_lines(out_py, ignored, match=False) + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dashm_equal_to_doubledashsource(self): + """regression test for #328 + + When imported by -m, a module's __name__ is __main__, but we need the + --source machinery to know and respect the original name. + """ + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source process_test.try_execfile -m process_test.try_execfile" + ) + out_py = self.run_command("python -m process_test.try_execfile") + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dashm_superset_of_doubledashsource(self): + """Edge case: --source foo -m foo.bar""" + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source process_test -m process_test.try_execfile" + ) + out_py = self.run_command("python -m process_test.try_execfile") + self.assert_tryexecfile_output(out_cov, out_py) + + st, out = self.run_command_status("coverage report") + self.assertEqual(st, 0) + self.assertEqual(self.line_count(out), 6, out) + + def test_coverage_run_script_imports_doubledashsource(self): + # This file imports try_execfile, which compiles it to .pyc, so the + # first run will have __file__ == "try_execfile.py" and the second will + # have __file__ == "try_execfile.pyc", which throws off the comparison. + # Setting dont_write_bytecode True stops the compilation to .pyc and + # keeps the test working. + self.make_file("myscript", """\ + import sys; sys.dont_write_bytecode = True + import process_test.try_execfile + """) + + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source process_test myscript" + ) + out_py = self.run_command("python myscript") + self.assert_tryexecfile_output(out_cov, out_py) + + st, out = self.run_command_status("coverage report") + self.assertEqual(st, 0) + self.assertEqual(self.line_count(out), 6, out) + + def test_coverage_run_dashm_is_like_python_dashm_off_path(self): + # https://bitbucket.org/ned/coveragepy/issue/242 + self.make_file("sub/__init__.py", "") + with open(TRY_EXECFILE) as f: + self.make_file("sub/run_me.py", f.read()) + + out_cov = self.run_command("coverage run -m sub.run_me") + out_py = self.run_command("python -m sub.run_me") + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self): + if sys.version_info < (2, 7): + # Coverage.py isn't bug-for-bug compatible in the behavior + # of -m for Pythons < 2.7 + self.skipTest("-m doesn't work the same < Python 2.7") + # https://bitbucket.org/ned/coveragepy/issue/207 + self.make_file("package/__init__.py", "print('init')") + self.make_file("package/__main__.py", "print('main')") + out_cov = self.run_command("coverage run -m package") + out_py = self.run_command("python -m package") + self.assertMultiLineEqual(out_cov, out_py) + + +class ExcepthookTest(CoverageTest): + """Tests of sys.excepthook support.""" + + def test_excepthook(self): + self.make_file("excepthook.py", """\ + import sys + + def excepthook(*args): + print('in excepthook') + if maybe == 2: + print('definitely') + + sys.excepthook = excepthook + + maybe = 1 + raise RuntimeError('Error Outside') + """) + cov_st, cov_out = self.run_command_status("coverage run excepthook.py") + py_st, py_out = self.run_command_status("python excepthook.py") + if not env.JYTHON: + self.assertEqual(cov_st, py_st) + self.assertEqual(cov_st, 1) + + self.assertIn("in excepthook", py_out) + self.assertEqual(cov_out, py_out) + + # Read the coverage file and see that excepthook.py has 7 lines + # executed. + data = coverage.CoverageData() + data.read_file(".coverage") + self.assertEqual(data.line_counts()['excepthook.py'], 7) + + def test_excepthook_exit(self): + if env.PYPY or env.JYTHON: + self.skipTest("non-CPython handles excepthook exits differently, punt for now.") + self.make_file("excepthook_exit.py", """\ + import sys + + def excepthook(*args): + print('in excepthook') + sys.exit(0) + + sys.excepthook = excepthook + + raise RuntimeError('Error Outside') + """) + cov_st, cov_out = self.run_command_status("coverage run excepthook_exit.py") + py_st, py_out = self.run_command_status("python excepthook_exit.py") + self.assertEqual(cov_st, py_st) + self.assertEqual(cov_st, 0) + + self.assertIn("in excepthook", py_out) + self.assertEqual(cov_out, py_out) + + def test_excepthook_throw(self): + if env.PYPY: + self.skipTest("PyPy handles excepthook throws differently, punt for now.") + self.make_file("excepthook_throw.py", """\ + import sys + + def excepthook(*args): + # Write this message to stderr so that we don't have to deal + # with interleaved stdout/stderr comparisons in the assertions + # in the test. + sys.stderr.write('in excepthook\\n') + raise RuntimeError('Error Inside') + + sys.excepthook = excepthook + + raise RuntimeError('Error Outside') + """) + cov_st, cov_out = self.run_command_status("coverage run excepthook_throw.py") + py_st, py_out = self.run_command_status("python excepthook_throw.py") + if not env.JYTHON: + self.assertEqual(cov_st, py_st) + self.assertEqual(cov_st, 1) + + self.assertIn("in excepthook", py_out) + self.assertEqual(cov_out, py_out) + + class AliasedCommandTest(CoverageTest): """Tests of the version-specific command aliases.""" run_in_temp_dir = False + def setUp(self): + super(AliasedCommandTest, self).setUp() + if env.JYTHON: + self.skipTest("Coverage command names don't work on Jython") + def test_major_version_works(self): # "coverage2" works on py2 cmd = "coverage%d" % sys.version_info[0] @@ -779,6 +887,7 @@ class AliasedCommandTest(CoverageTest): def test_wrong_alias_doesnt_work(self): # "coverage3" doesn't work on py2 + assert sys.version_info[0] in [2, 3] # Let us know when Python 4 is out... badcmd = "coverage%d" % (5 - sys.version_info[0]) out = self.run_command(badcmd) self.assertNotIn("Code coverage for Python", out) @@ -850,125 +959,40 @@ class FailUnderTest(CoverageTest): ) def test_report(self): - st, _ = self.run_command_status("coverage report --fail-under=42") - self.assertEqual(st, 0) st, _ = self.run_command_status("coverage report --fail-under=43") self.assertEqual(st, 0) st, _ = self.run_command_status("coverage report --fail-under=44") self.assertEqual(st, 2) - def test_html_report(self): - st, _ = self.run_command_status("coverage html --fail-under=42") - self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage html --fail-under=43") - self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage html --fail-under=44") - self.assertEqual(st, 2) - - def test_xml_report(self): - st, _ = self.run_command_status("coverage xml --fail-under=42") - self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage xml --fail-under=43") - self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage xml --fail-under=44") - self.assertEqual(st, 2) - - def test_fail_under_in_config(self): - self.make_file(".coveragerc", "[report]\nfail_under = 43\n") - st, _ = self.run_command_status("coverage report") - self.assertEqual(st, 0) - - self.make_file(".coveragerc", "[report]\nfail_under = 44\n") - st, _ = self.run_command_status("coverage report") - self.assertEqual(st, 2) - class FailUnderNoFilesTest(CoverageTest): """Test that nothing to report results in an error exit status.""" - def setUp(self): - super(FailUnderNoFilesTest, self).setUp() - self.make_file(".coveragerc", "[report]\nfail_under = 99\n") - def test_report(self): + self.make_file(".coveragerc", "[report]\nfail_under = 99\n") st, out = self.run_command_status("coverage report") self.assertIn('No data to report.', out) self.assertEqual(st, 1) - def test_xml(self): - st, out = self.run_command_status("coverage xml") - self.assertIn('No data to report.', out) - self.assertEqual(st, 1) - - def test_html(self): - st, out = self.run_command_status("coverage html") - self.assertIn('No data to report.', out) - self.assertEqual(st, 1) - class FailUnderEmptyFilesTest(CoverageTest): """Test that empty files produce the proper fail_under exit status.""" - def setUp(self): - super(FailUnderEmptyFilesTest, self).setUp() - + def test_report(self): self.make_file(".coveragerc", "[report]\nfail_under = 99\n") self.make_file("empty.py", "") st, _ = self.run_command_status("coverage run empty.py") self.assertEqual(st, 0) - - def test_report(self): st, _ = self.run_command_status("coverage report") self.assertEqual(st, 2) - def test_xml(self): - st, _ = self.run_command_status("coverage xml") - self.assertEqual(st, 2) - - def test_html(self): - st, _ = self.run_command_status("coverage html") - self.assertEqual(st, 2) - - -class FailUnder100Test(CoverageTest): - """Tests of the --fail-under switch.""" - - def test_99_8(self): - self.make_file("ninety_nine_eight.py", - "".join("v{i} = {i}\n".format(i=i) for i in range(498)) + - "if v0 > 498:\n v499 = 499\n" - ) - st, _ = self.run_command_status("coverage run ninety_nine_eight.py") - self.assertEqual(st, 0) - st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual( - self.last_line_squeezed(out), - "ninety_nine_eight.py 500 1 99%" - ) - - st, _ = self.run_command_status("coverage report --fail-under=100") - self.assertEqual(st, 2) - - - def test_100(self): - self.make_file("one_hundred.py", - "".join("v{i} = {i}\n".format(i=i) for i in range(500)) - ) - st, _ = self.run_command_status("coverage run one_hundred.py") - self.assertEqual(st, 0) - st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual( - self.last_line_squeezed(out), - "one_hundred.py 500 0 100%" - ) - - st, _ = self.run_command_status("coverage report --fail-under=100") - self.assertEqual(st, 0) - class UnicodeFilePathsTest(CoverageTest): """Tests of using non-ascii characters in the names of files.""" + def setUp(self): + super(UnicodeFilePathsTest, self).setUp() + if env.JYTHON: + self.skipTest("Jython doesn't like accented file names") + def test_accented_dot_py(self): # Make a file with a non-ascii character in the filename. self.make_file(u"h\xe2t.py", "print('accented')") @@ -998,7 +1022,6 @@ class UnicodeFilePathsTest(CoverageTest): ) if env.PY2: - # pylint: disable=redefined-variable-type report_expected = report_expected.encode(output_encoding()) out = self.run_command("coverage report") @@ -1037,7 +1060,6 @@ class UnicodeFilePathsTest(CoverageTest): ) if env.PY2: - # pylint: disable=redefined-variable-type report_expected = report_expected.encode(output_encoding()) out = self.run_command("coverage report") @@ -1046,17 +1068,35 @@ class UnicodeFilePathsTest(CoverageTest): def possible_pth_dirs(): """Produce a sequence of directories for trying to write .pth files.""" - # First look through sys.path, and we find a .pth file, then it's a good + # First look through sys.path, and if we find a .pth file, then it's a good # place to put ours. - for d in sys.path: - g = glob.glob(os.path.join(d, "*.pth")) - if g: - yield d + for pth_dir in sys.path: # pragma: part covered + pth_files = glob.glob(os.path.join(pth_dir, "*.pth")) + if pth_files: + yield pth_dir # If we're still looking, then try the Python library directory. # https://bitbucket.org/ned/coveragepy/issue/339/pth-test-malfunctions - import distutils.sysconfig # pylint: disable=import-error - yield distutils.sysconfig.get_python_lib() + yield distutils.sysconfig.get_python_lib() # pragma: cant happen + + +def find_writable_pth_directory(): + """Find a place to write a .pth file.""" + for pth_dir in possible_pth_dirs(): # pragma: part covered + try_it = os.path.join(pth_dir, "touch_{0}.it".format(WORKER)) + with open(try_it, "w") as f: + try: + f.write("foo") + except (IOError, OSError): # pragma: cant happen + continue + + os.remove(try_it) + return pth_dir + + return None # pragma: cant happen + +WORKER = os.environ.get('PYTEST_XDIST_WORKER', '') +PTH_DIR = find_writable_pth_directory() class ProcessCoverageMixin(object): @@ -1064,19 +1104,14 @@ class ProcessCoverageMixin(object): def setUp(self): super(ProcessCoverageMixin, self).setUp() - # Find a place to put a .pth file. + + # Create the .pth file. + self.assertTrue(PTH_DIR) pth_contents = "import coverage; coverage.process_startup()\n" - for pth_dir in possible_pth_dirs(): # pragma: part covered - pth_path = os.path.join(pth_dir, "subcover.pth") - with open(pth_path, "w") as pth: - try: - pth.write(pth_contents) - self.pth_path = pth_path - break - except (IOError, OSError): # pragma: not covered - pass - else: # pragma: not covered - raise Exception("Couldn't find a place for the .pth file") + pth_path = os.path.join(PTH_DIR, "subcover_{0}.pth".format(WORKER)) + with open(pth_path, "w") as pth: + pth.write(pth_contents) + self.pth_path = pth_path self.addCleanup(os.remove, self.pth_path) @@ -1095,11 +1130,12 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): """) # sub.py will write a few lines. self.make_file("sub.py", """\ - with open("out.txt", "w") as f: - f.write("Hello, world!\\n") + f = open("out.txt", "w") + f.write("Hello, world!\\n") + f.close() """) - def test_subprocess_with_pth_files(self): # pragma: not covered + def test_subprocess_with_pth_files(self): # pragma: no metacov if env.METACOV: self.skipTest("Can't test sub-process pth file suppport during metacoverage") @@ -1124,9 +1160,9 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): self.assert_exists(".mycovdata") data = coverage.CoverageData() data.read_file(".mycovdata") - self.assertEqual(data.line_counts()['sub.py'], 2) + self.assertEqual(data.line_counts()['sub.py'], 3) - def test_subprocess_with_pth_files_and_parallel(self): # pragma: not covered + def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov # https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of if env.METACOV: self.skipTest("Can't test sub-process pth file suppport during metacoverage") @@ -1148,12 +1184,12 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): self.assert_exists(".coverage") data = coverage.CoverageData() data.read_file(".coverage") - self.assertEqual(data.line_counts()['sub.py'], 2) + self.assertEqual(data.line_counts()['sub.py'], 3) # assert that there are *no* extra data files left over after a combine data_files = glob.glob(os.getcwd() + '/.coverage*') - self.assertEquals(len(data_files), 1, - "Expected only .coverage after combine, looks like there are " + \ + self.assertEqual(len(data_files), 1, + "Expected only .coverage after combine, looks like there are " "extra data files that were not cleaned up: %r" % data_files) @@ -1173,7 +1209,7 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): def assert_pth_and_source_work_together( self, dashm, package, source - ): # pragma: not covered + ): # pragma: no metacov """Run the test for a particular combination of factors. The arguments are all strings: @@ -1205,14 +1241,17 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): # Main will run sub.py. self.make_file(path("main.py"), """\ import %s - if True: pass + a = 2 + b = 3 """ % fullname('sub')) if package: self.make_file(path("__init__.py"), "") # sub.py will write a few lines. self.make_file(path("sub.py"), """\ - with open("out.txt", "w") as f: - f.write("Hello, world!") + # Avoid 'with' so Jython can play along. + f = open("out.txt", "w") + f.write("Hello, world!") + f.close() """) self.make_file("coverage.ini", """\ [run] @@ -1237,7 +1276,7 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): data.read_file(".coverage") summary = data.line_counts() print(summary) - self.assertEqual(summary[source + '.py'], 2) + self.assertEqual(summary[source + '.py'], 3) self.assertEqual(len(summary), 1) def test_dashm_main(self): @@ -1263,9 +1302,3 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): def test_script_pkg_sub(self): self.assert_pth_and_source_work_together('', 'pkg', 'sub') - - -def remove_matching_lines(text, pat): - """Return `text` with all lines matching `pat` removed.""" - lines = [l for l in text.splitlines(True) if not re.search(pat, l)] - return "".join(lines) diff --git a/tests/test_python.py b/tests/test_python.py index ee1e1f9..9027aa6 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -6,7 +6,10 @@ import os import sys -from coverage.python import get_zip_bytes +import pytest + +from coverage import env +from coverage.python import get_zip_bytes, source_for_file from tests.coveragetest import CoverageTest @@ -28,3 +31,31 @@ class GetZipBytesTest(CoverageTest): self.assertIn('All OK', zip_text) # Run the code to see that we really got it encoded properly. __import__("encoded_"+encoding) + + +def test_source_for_file(tmpdir): + path = tmpdir.join("a.py") + src = str(path) + assert source_for_file(src) == src + assert source_for_file(src + 'c') == src + assert source_for_file(src + 'o') == src + unknown = src + 'FOO' + assert source_for_file(unknown) == unknown + + +@pytest.mark.skipif(not env.WINDOWS, reason="not windows") +def test_source_for_file_windows(tmpdir): + path = tmpdir.join("a.py") + src = str(path) + + # On windows if a pyw exists, it is an acceptable source + path_windows = tmpdir.ensure("a.pyw") + assert str(path_windows) == source_for_file(src + 'c') + + # If both pyw and py exist, py is preferred + path.ensure(file=True) + assert source_for_file(src + 'c') == src + + +def test_source_for_file_jython(): + assert source_for_file("a$py.class") == "a.py" diff --git a/tests/test_results.py b/tests/test_results.py index 54c2f6d..280694d 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -3,7 +3,10 @@ """Tests for coverage.py's results analysis.""" -from coverage.results import Numbers +import pytest + +from coverage.results import Numbers, should_fail_under + from tests.coveragetest import CoverageTest @@ -40,6 +43,8 @@ class NumbersTest(CoverageTest): self.assertAlmostEqual(n3.pc_covered, 86.666666666) def test_pc_covered_str(self): + # Numbers._precision is a global, which is bad. + Numbers.set_precision(0) n0 = Numbers(n_files=1, n_statements=1000, n_missing=0) n1 = Numbers(n_files=1, n_statements=1000, n_missing=1) n999 = Numbers(n_files=1, n_statements=1000, n_missing=999) @@ -50,7 +55,7 @@ class NumbersTest(CoverageTest): self.assertEqual(n1000.pc_covered_str, "0") def test_pc_covered_str_precision(self): - assert Numbers._precision == 0 + # Numbers._precision is a global, which is bad. Numbers.set_precision(1) n0 = Numbers(n_files=1, n_statements=10000, n_missing=0) n1 = Numbers(n_files=1, n_statements=10000, n_missing=1) @@ -71,3 +76,23 @@ class NumbersTest(CoverageTest): n_branches=10, n_missing_branches=3, n_partial_branches=1000, ) self.assertEqual(n.ratio_covered, (160, 210)) + + +@pytest.mark.parametrize("total, fail_under, result", [ + # fail_under==0 means anything is fine! + (0, 0, False), + (0.001, 0, False), + # very small fail_under is possible to fail. + (0.001, 0.01, True), + # Rounding should work properly. + (42.1, 42, False), + (42.1, 43, True), + (42.857, 42, False), + (42.857, 43, False), + (42.857, 44, True), + # Values near 100 should only be treated as 100 if they are 100. + (99.8, 100, True), + (100.0, 100, False), +]) +def test_should_fail_under(total, fail_under, result): + assert should_fail_under(total, fail_under) == result diff --git a/tests/test_setup.py b/tests/test_setup.py new file mode 100644 index 0000000..6533418 --- /dev/null +++ b/tests/test_setup.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Tests of miscellaneous stuff.""" + +import sys + +import coverage + +from tests.coveragetest import CoverageTest + + +class SetupPyTest(CoverageTest): + """Tests of setup.py""" + + run_in_temp_dir = False + + def setUp(self): + super(SetupPyTest, self).setUp() + # Force the most restrictive interpretation. + self.set_environ('LC_ALL', 'C') + + def test_metadata(self): + status, output = self.run_command_status( + "python setup.py --description --version --url --author" + ) + self.assertEqual(status, 0) + out = output.splitlines() + self.assertIn("measurement", out[0]) + self.assertEqual(out[1], coverage.__version__) + self.assertEqual(out[2], coverage.__url__) + self.assertIn("Ned Batchelder", out[3]) + + def test_more_metadata(self): + # Let's be sure we pick up our own setup.py + # CoverageTest restores the original sys.path for us. + sys.path.insert(0, '') + from setup import setup_args + + classifiers = setup_args['classifiers'] + self.assertGreater(len(classifiers), 7) + self.assert_starts_with(classifiers[-1], "Development Status ::") + + long_description = setup_args['long_description'].splitlines() + self.assertGreater(len(long_description), 7) + self.assertNotEqual(long_description[0].strip(), "") + self.assertNotEqual(long_description[-1].strip(), "") diff --git a/tests/test_summary.py b/tests/test_summary.py index bda6568..7c9f4c1 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -1,4 +1,4 @@ -# coding: utf8 +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt @@ -361,6 +361,27 @@ class SummaryTest(CoverageTest): squeezed = self.squeezed_lines(report) self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + def test_report_skip_covered_longfilename(self): + self.make_file("long_______________filename.py", """ + def foo(): + pass + foo() + """) + out = self.run_command("coverage run --branch long_______________filename.py") + self.assertEqual(out, "") + report = self.report_from_command("coverage report --skip-covered") + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------- + # + # 1 file skipped due to complete coverage. + + self.assertEqual(self.line_count(report), 4, report) + lines = self.report_lines(report) + self.assertEqual(lines[0], "Name Stmts Miss Branch BrPart Cover") + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + def test_report_skip_covered_no_data(self): report = self.report_from_command("coverage report --skip-covered") @@ -381,22 +402,25 @@ class SummaryTest(CoverageTest): self.make_file("mycode.py", "This isn't python at all!") report = self.report_from_command("coverage report mycode.py") + # mycode NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # Name Stmts Miss Cover # ---------------------------- - # mycode NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # No data to report. - last = self.squeezed_lines(report)[-2] + errmsg = self.squeezed_lines(report)[0] # The actual file name varies run to run. - last = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", last) + errmsg = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", errmsg) # The actual error message varies version to version - last = re.sub(r": '.*' at", ": 'error' at", last) + errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg) self.assertEqual( - last, + errmsg, "mycode.py NotPython: Couldn't parse 'mycode.py' as Python source: 'error' at line 1" ) def test_accenteddotpy_not_python(self): + if env.JYTHON: + self.skipTest("Jython doesn't like accented file names") + # We run a .py file with a non-ascii name, and when reporting, we can't # parse it as Python. We should get an error message in the report. @@ -405,24 +429,23 @@ class SummaryTest(CoverageTest): self.make_file(u"accented\xe2.py", "This isn't python at all!") report = self.report_from_command(u"coverage report accented\xe2.py") + # xxxx NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # Name Stmts Miss Cover # ---------------------------- - # xxxx NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # No data to report. - last = self.squeezed_lines(report)[-2] + errmsg = self.squeezed_lines(report)[0] # The actual file name varies run to run. - last = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", last) + errmsg = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", errmsg) # The actual error message varies version to version - last = re.sub(r": '.*' at", ": 'error' at", last) + errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg) expected = ( u"accented\xe2.py NotPython: " u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1" ) if env.PY2: - # pylint: disable=redefined-variable-type expected = expected.encode(output_encoding()) - self.assertEqual(last, expected) + self.assertEqual(errmsg, expected) def test_dotpy_not_python_ignored(self): # We run a .py file, and when reporting, we can't parse it as Python, @@ -564,7 +587,7 @@ class SummaryTest(CoverageTest): # Python 3 puts the .pyc files in a __pycache__ directory, and will # not import from there without source. It will import a .pyc from # the source location though. - if not os.path.exists("mod.pyc"): + if env.PY3 and not env.JYTHON: pycs = glob.glob("__pycache__/mod.*.pyc") self.assertEqual(len(pycs), 1) os.rename(pycs[0], "mod.pyc") @@ -656,7 +679,7 @@ class TestSummaryReporterConfiguration(CoverageTest): HERE = os.path.dirname(__file__) LINES_1 = { - os.path.join(HERE, "test_api.py"): dict.fromkeys(range(300)), + os.path.join(HERE, "test_api.py"): dict.fromkeys(range(400)), os.path.join(HERE, "test_backward.py"): dict.fromkeys(range(20)), os.path.join(HERE, "test_coverage.py"): dict.fromkeys(range(15)), } @@ -738,6 +761,14 @@ class TestSummaryReporterConfiguration(CoverageTest): report = self.get_summary_text(data, opts) self.assert_ordering(report, "test_backward.py", "test_coverage.py", "test_api.py") + def test_sort_report_by_missing(self): + # Sort the text report by the Missing column. + data = self.get_coverage_data(self.LINES_1) + opts = CoverageConfig() + opts.from_args(sort='Miss') + report = self.get_summary_text(data, opts) + self.assert_ordering(report, "test_backward.py", "test_api.py", "test_coverage.py") + def test_sort_report_by_cover(self): # Sort the text report by the Cover column. data = self.get_coverage_data(self.LINES_1) diff --git a/tests/test_templite.py b/tests/test_templite.py index 1df942e..bcc65f9 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -1,4 +1,4 @@ -# -*- coding: utf8 -*- +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt diff --git a/tests/test_testing.py b/tests/test_testing.py index c5858bf..05bf0c9 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -8,11 +8,15 @@ import datetime import os import sys +import pytest + import coverage -from coverage.backunittest import TestCase +from coverage.backunittest import TestCase, unittest from coverage.files import actual_path +from coverage.misc import StopEverything -from tests.coveragetest import CoverageTest +from tests.coveragetest import CoverageTest, convert_skip_exceptions +from tests.helpers import CheckUniqueFilenames, re_lines, re_line class TestingTest(TestCase): @@ -110,14 +114,38 @@ class CoverageTestTest(CoverageTest): with self.assert_warnings(cov, ["Not me"]): cov._warn("Hello there!") + # Try checking a warning that shouldn't appear: happy case. + with self.assert_warnings(cov, ["Hi"], not_warnings=["Bye"]): + cov._warn("Hi") + + # But it should fail if the unexpected warning does appear. + warn_regex = r"Found warning 'Bye' in \['Hi', 'Bye'\]" + with self.assertRaisesRegex(AssertionError, warn_regex): + with self.assert_warnings(cov, ["Hi"], not_warnings=["Bye"]): + cov._warn("Hi") + cov._warn("Bye") + # assert_warnings shouldn't hide a real exception. with self.assertRaises(ZeroDivisionError): with self.assert_warnings(cov, ["Hello there!"]): raise ZeroDivisionError("oops") + def test_assert_no_warnings(self): + cov = coverage.Coverage() + + # Happy path: no warnings. + with self.assert_warnings(cov, []): + pass + + # If you said there would be no warnings, and there were, fail! + warn_regex = r"Unexpected warnings: \['Watch out!'\]" + with self.assertRaisesRegex(AssertionError, warn_regex): + with self.assert_warnings(cov, []): + cov._warn("Watch out!") + def test_sub_python_is_this_python(self): # Try it with a Python command. - os.environ['COV_FOOBAR'] = 'XYZZY' + self.set_environ('COV_FOOBAR', 'XYZZY') self.make_file("showme.py", """\ import os, sys print(sys.executable) @@ -130,17 +158,97 @@ class CoverageTestTest(CoverageTest): self.assertEqual(out[2], 'XYZZY') # Try it with a "coverage debug sys" command. - out = self.run_command("coverage debug sys").splitlines() - # "environment: COV_FOOBAR = XYZZY" or "COV_FOOBAR = XYZZY" - executable = next(l for l in out if "executable:" in l) # pragma: part covered + out = self.run_command("coverage debug sys") + + executable = re_line(out, "executable:") executable = executable.split(":", 1)[1].strip() - self.assertTrue(same_python_executable(executable, sys.executable)) - environ = next(l for l in out if "COV_FOOBAR" in l) # pragma: part covered + self.assertTrue(_same_python_executable(executable, sys.executable)) + + # "environment: COV_FOOBAR = XYZZY" or "COV_FOOBAR = XYZZY" + environ = re_line(out, "COV_FOOBAR") _, _, environ = environ.rpartition(":") self.assertEqual(environ.strip(), "COV_FOOBAR = XYZZY") -def same_python_executable(e1, e2): +class CheckUniqueFilenamesTest(CoverageTest): + """Tests of CheckUniqueFilenames.""" + + run_in_temp_dir = False + + class Stub(object): + """A stand-in for the class we're checking.""" + def __init__(self, x): + self.x = x + + def method(self, filename, a=17, b="hello"): + """The method we'll wrap, with args to be sure args work.""" + return (self.x, filename, a, b) + + def test_detect_duplicate(self): + stub = self.Stub(23) + CheckUniqueFilenames.hook(stub, "method") + + # Two method calls with different names are fine. + assert stub.method("file1") == (23, "file1", 17, "hello") + assert stub.method("file2", 1723, b="what") == (23, "file2", 1723, "what") + + # A duplicate file name trips an assertion. + with self.assertRaises(AssertionError): + stub.method("file1") + + +@pytest.mark.parametrize("text, pat, result", [ + ("line1\nline2\nline3\n", "line", "line1\nline2\nline3\n"), + ("line1\nline2\nline3\n", "[13]", "line1\nline3\n"), + ("line1\nline2\nline3\n", "X", ""), +]) +def test_re_lines(text, pat, result): + assert re_lines(text, pat) == result + +@pytest.mark.parametrize("text, pat, result", [ + ("line1\nline2\nline3\n", "line", ""), + ("line1\nline2\nline3\n", "[13]", "line2\n"), + ("line1\nline2\nline3\n", "X", "line1\nline2\nline3\n"), +]) +def test_re_lines_inverted(text, pat, result): + assert re_lines(text, pat, match=False) == result + +@pytest.mark.parametrize("text, pat, result", [ + ("line1\nline2\nline3\n", "2", "line2"), +]) +def test_re_line(text, pat, result): + assert re_line(text, pat) == result + +@pytest.mark.parametrize("text, pat", [ + ("line1\nline2\nline3\n", "line"), # too many matches + ("line1\nline2\nline3\n", "X"), # no matches +]) +def test_re_line_bad(text, pat): + with pytest.raises(AssertionError): + re_line(text, pat) + + +def test_convert_skip_exceptions(): + @convert_skip_exceptions + def some_method(ret=None, exc=None): + """Be like a test case.""" + if exc: + raise exc("yikes!") + return ret + + # Normal flow is normal. + assert some_method(ret=[17, 23]) == [17, 23] + + # Exceptions are raised normally. + with pytest.raises(ValueError): + some_method(exc=ValueError) + + # But a StopEverything becomes a SkipTest. + with pytest.raises(unittest.SkipTest): + some_method(exc=StopEverything) + + +def _same_python_executable(e1, e2): """Determine if `e1` and `e2` refer to the same Python executable. Either path could include symbolic links. The two paths might not refer diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..eb8de87 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,39 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Tests of version.py.""" + +import coverage +from coverage.version import _make_url, _make_version + +from tests.coveragetest import CoverageTest + + +class VersionTest(CoverageTest): + """Tests of version.py""" + + run_in_temp_dir = False + + def test_version_info(self): + # Make sure we didn't screw up the version_info tuple. + self.assertIsInstance(coverage.version_info, tuple) + self.assertEqual([type(d) for d in coverage.version_info], [int, int, int, str, int]) + self.assertIn(coverage.version_info[3], ['alpha', 'beta', 'candidate', 'final']) + + def test_make_version(self): + self.assertEqual(_make_version(4, 0, 0, 'alpha', 0), "4.0a0") + self.assertEqual(_make_version(4, 0, 0, 'alpha', 1), "4.0a1") + self.assertEqual(_make_version(4, 0, 0, 'final', 0), "4.0") + self.assertEqual(_make_version(4, 1, 2, 'beta', 3), "4.1.2b3") + self.assertEqual(_make_version(4, 1, 2, 'final', 0), "4.1.2") + self.assertEqual(_make_version(5, 10, 2, 'candidate', 7), "5.10.2rc7") + + def test_make_url(self): + self.assertEqual( + _make_url(4, 0, 0, 'final', 0), + "https://coverage.readthedocs.io" + ) + self.assertEqual( + _make_url(4, 1, 2, 'beta', 3), + "https://coverage.readthedocs.io/en/coverage-4.1.2b3" + ) diff --git a/tests/test_xml.py b/tests/test_xml.py index dd14b92..c3493e7 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -1,3 +1,4 @@ +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt @@ -8,11 +9,13 @@ import os.path import re import coverage +from coverage.backward import import_local_file from coverage.files import abs_file from tests.coveragetest import CoverageTest from tests.goldtest import CoverageGoldTest from tests.goldtest import change_dir, compare +from tests.helpers import re_line, re_lines class XmlTestHelpers(CoverageTest): @@ -165,8 +168,8 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): self.make_file("also/over/there/bar.py", "b = 2") cov = coverage.Coverage(source=["src/main", "also/over/there", "not/really"]) cov.start() - mod_foo = self.import_local_file("foo", "src/main/foo.py") # pragma: nested - mod_bar = self.import_local_file("bar", "also/over/there/bar.py") # pragma: nested + mod_foo = import_local_file("foo", "src/main/foo.py") # pragma: nested + mod_bar = import_local_file("bar", "also/over/there/bar.py") # pragma: nested cov.stop() # pragma: nested cov.xml_report([mod_foo, mod_bar], outfile="-") xml = self.stdout() @@ -184,6 +187,14 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): xml ) + def test_nonascii_directory(self): + # https://bitbucket.org/ned/coveragepy/issues/573/cant-generate-xml-report-if-some-source + self.make_file("테스트/program.py", "a = 1") + with change_dir("테스트"): + cov = coverage.Coverage() + self.start_import_stop(cov, "program") + cov.xml_report() + class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): """Tests about the package structure reported in the coverage.xml file.""" @@ -194,7 +205,7 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): cov.xml_report(outfile="-") packages_and_classes = re_lines(self.stdout(), r"<package |<class ") scrubs = r' branch-rate="0"| complexity="0"| line-rate="[\d.]+"' - return clean("".join(packages_and_classes), scrubs) + return clean(packages_and_classes, scrubs) def assert_package_and_class_tags(self, cov, result): """Check the XML package and class tags from `cov` match `result`.""" @@ -282,19 +293,6 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): """) -def re_lines(text, pat): - """Return a list of lines that match `pat` in the string `text`.""" - lines = [l for l in text.splitlines(True) if re.search(pat, l)] - return lines - - -def re_line(text, pat): - """Return the one line in `text` that matches regex `pat`.""" - lines = re_lines(text, pat) - assert len(lines) == 1 - return lines[0] - - def clean(text, scrub=None): """Clean text to prepare it for comparison. |