From 79e6fe6fa159d2d7c3eefdbc494ba1269935fae1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Feb 2018 15:42:30 -0500 Subject: [PATCH 001/952] Coverage 4.5.1 --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index 8f9ad85cc..411bf391a 100644 --- a/.hgtags +++ b/.hgtags @@ -65,3 +65,4 @@ dd2d866194d2eca05862230e6003c6e04fc2fdc0 coverage-4.3.2 ed196840b79136f17ab493699ec83dcf7dbfe973 coverage-4.4.1 b65ae46a6504b8d577e967bd3fdcfcaceec95528 coverage-4.4.2 102b2250a123537e640cd014f5df281822e79cec coverage-4.5 +dda8b38e71d0bd2bde79d644f7265e1c02ce02f9 coverage-4.5.1 From 695e8cb5e09582b0cb18902e2b7d6a1f5b22f140 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Feb 2018 15:44:01 -0500 Subject: [PATCH 002/952] Bump version --- CHANGES.rst | 5 +++++ coverage/version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e4243b9b3..f2fcfc630 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,11 @@ Change history for Coverage.py .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- +Unreleased +---------- + +None yet + .. _changes_451: diff --git a/coverage/version.py b/coverage/version.py index 7dc59e276..dbeb64fbf 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (4, 5, 1, 'final', 0) +version_info = (4, 5, 2, 'alpha', 0) def _make_version(major, minor, micro, releaselevel, serial): From d56999aa4c75bfe356e9c1d36c60ecebf3ee85f0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Feb 2018 16:02:12 -0500 Subject: [PATCH 003/952] Tweaks to the shipping instructions --- howto.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/howto.txt b/howto.txt index 14c51916e..2707be51e 100644 --- a/howto.txt +++ b/howto.txt @@ -74,13 +74,17 @@ - Update readthedocs - visit https://readthedocs.org/projects/coverage/versions/ - find the latest tag in the inactive list, edit it, make it active. + - keep just the latest version of each x.y release, make the rest inactive. - IF NOT BETA: + - visit https://readthedocs.org/projects/coverage/builds/ + - wait for the new tag build to finish successfully. - visit https://readthedocs.org/dashboard/coverage/versions/ - change the default version to the new version - Update bitbucket: - Issue tracker should get new version number in picker. # Note: don't delete old version numbers: it marks changes on the tickets # with that number. +- Visit the fixed issues on bitbucket and mention the version it was fixed in. - Announce on coveragepy-announce@googlegroups.com . - Announce on TIP. From a1b1ad8b1ff316db9f22b00d620c793a07f37bb6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 19 Feb 2018 08:05:55 -0500 Subject: [PATCH 004/952] A little better debug logging --- coverage/debug.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index e68736f6e..6e6e80130 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -215,7 +215,7 @@ def __init__(self, outfile, show_process, filters): self.write("New process: executable: %s\n" % (sys.executable,)) self.write("New process: cmd: %s\n" % (cmd,)) if hasattr(os, 'getppid'): - self.write("New process: parent pid: %s\n" % (os.getppid(),)) + self.write("New process: pid: %s, parent pid: %s\n" % (os.getpid(), os.getppid())) SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' @@ -234,7 +234,8 @@ def the_one(cls, fileobj=None, show_process=True, filters=()): # on a class attribute. Yes, this is aggressively gross. the_one = sys.modules.get(cls.SYS_MOD_NAME) if the_one is None: - assert fileobj is not None + if fileobj is None: + fileobj = open("/tmp/debug_log.txt", "a") sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters) return the_one From b99e3c140dbde77389d9e49d9bf6eb77c2983ebe Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 19 Feb 2018 15:24:55 -0500 Subject: [PATCH 005/952] Make a test a little more specific --- tests/test_process.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_process.py b/tests/test_process.py index 18564cb83..35dddd07e 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -676,9 +676,11 @@ def foo(): pass """) self.make_file("run_twice.py", """\ + import sys import coverage - for _ in [1, 2]: + for i in [1, 2]: + sys.stderr.write("Run %s\\n" % i) inst = coverage.Coverage(source=['foo']) inst.load() inst.start() @@ -689,6 +691,8 @@ def foo(): out = self.run_command("python run_twice.py") self.assertEqual( out, + "Run 1\n" + "Run 2\n" "Coverage.py warning: Module foo was previously imported, but not measured " "(module-not-measured)\n" ) From 5b3c821cd1633f1e64bebc2e61060677bceb200e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 19 Feb 2018 17:36:29 -0500 Subject: [PATCH 006/952] Fix english, and give a test a name that isn't a prefix of other names --- tests/test_concurrency.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 71006042f..76e1d9e45 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -335,7 +335,7 @@ def work(x): import sys def process_worker_main(args): - # Need to pause, or the tasks go too quick, and some processes + # Need to pause, or the tasks go too quickly, and some processes # in the pool don't get any work, and then don't record data. time.sleep(0.02) ret = work(*args) @@ -359,7 +359,7 @@ def process_worker_main(args): """ -@flaky(max_runs=10) # Sometimes a test fails due to inherent randomness. Try one more time. +@flaky(max_runs=10) # Sometimes a test fails due to inherent randomness. Try more times. class MultiprocessingTest(CoverageTest): """Test support of the multiprocessing module.""" @@ -403,7 +403,7 @@ def try_multiprocessing_code( last_line = self.squeezed_lines(out)[-1] self.assertRegex(last_line, r"multi.py \d+ 0 100%") - def test_multiprocessing(self): + def test_multiprocessing_simple(self): nprocs = 3 upto = 30 code = (SQUARE_OR_CUBE_WORK + MULTI_CODE).format(NPROCS=nprocs, UPTO=upto) From e00f04edd11bff44860f8f5315e52cb8f02468af Mon Sep 17 00:00:00 2001 From: Ori Avtalion Date: Tue, 20 Feb 2018 12:44:30 +0000 Subject: [PATCH 007/952] Use https for codecov.io image --HG-- branch : salty_horse/use-https-for-codecovio-image-1519130663808 --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 4dac3ea85..d1c1698d6 100644 --- a/README.rst +++ b/README.rst @@ -102,8 +102,8 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. .. |license| image:: https://img.shields.io/pypi/l/coverage.svg :target: https://pypi.python.org/pypi/coverage :alt: License -.. |codecov| image:: http://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2 - :target: http://codecov.io/github/nedbat/coveragepy?branch=master +.. |codecov| image:: https://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2 + :target: https://codecov.io/github/nedbat/coveragepy?branch=master :alt: Coverage! .. |saythanks| image:: https://img.shields.io/badge/saythanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/nedbat From 6c9ac4a84b53a2d8ca36c3cdc5decd965d24b04d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 20 Feb 2018 08:26:35 -0500 Subject: [PATCH 008/952] A new warning for files already imported before coverage starts --- CHANGES.rst | 4 ++- coverage/control.py | 77 +++++++++++++++++++++++++++++-------------- coverage/multiproc.py | 1 + doc/cmd.rst | 7 ++++ igor.py | 5 +-- tests/test_api.py | 1 + tests/test_process.py | 24 ++++++++++++++ 7 files changed, 90 insertions(+), 29 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f2fcfc630..126ced944 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,7 +18,9 @@ Change history for Coverage.py Unreleased ---------- -None yet +- A new warning (already-imported) is issued if measurable files have already + been imported before coverage.py started measurement. See + :ref:`cmd_warnings` for more information. .. _changes_451: diff --git a/coverage/control.py b/coverage/control.py index b82c80476..daa00bd0a 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -74,6 +74,10 @@ class Coverage(object): cov.html_report(directory='covhtml') """ + # A global to know if we have ever checked for files imported before + # coverage has been started. + _checked_preimported = False + def __init__( self, data_file=None, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, @@ -164,6 +168,7 @@ def __init__( # Is it ok for no data to be collected? self._warn_no_data = True self._warn_unimported_source = True + self._warn_preimported_source = True # A record of all the warnings that have been issued. self._warnings = [] @@ -434,7 +439,7 @@ def _name_for_module(self, module_globals, filename): else: return dunder_name - def _should_trace_internal(self, filename, frame): + def _should_trace_internal(self, filename, frame=None): """Decide whether to trace execution in `filename`, with a reason. This function is called from the trace function. As each new file name @@ -452,22 +457,23 @@ def nope(disp, reason): disp.reason = reason return disp - # Compiled Python files have two file names: frame.f_code.co_filename is - # the file name at the time the .pyc was compiled. The second name is - # __file__, which is where the .pyc was actually loaded from. Since - # .pyc files can be moved after compilation (for example, by being - # installed), we look for __file__ in the frame and prefer it to the - # co_filename value. - dunder_file = frame.f_globals and frame.f_globals.get('__file__') - if dunder_file: - filename = source_for_file(dunder_file) - if original_filename and not original_filename.startswith('<'): - orig = os.path.basename(original_filename) - if orig != os.path.basename(filename): - # Files shouldn't be renamed when moved. This happens when - # exec'ing code. If it seems like something is wrong with - # the frame's file name, then just use the original. - filename = original_filename + if frame is not None: + # Compiled Python files have two file names: frame.f_code.co_filename is + # the file name at the time the .pyc was compiled. The second name is + # __file__, which is where the .pyc was actually loaded from. Since + # .pyc files can be moved after compilation (for example, by being + # installed), we look for __file__ in the frame and prefer it to the + # co_filename value. + dunder_file = frame.f_globals and frame.f_globals.get('__file__') + if dunder_file: + filename = source_for_file(dunder_file) + if original_filename and not original_filename.startswith('<'): + orig = os.path.basename(original_filename) + if orig != os.path.basename(filename): + # Files shouldn't be renamed when moved. This happens when + # exec'ing code. If it seems like something is wrong with + # the frame's file name, then just use the original. + filename = original_filename if not filename: # Empty string is pretty useless. @@ -534,22 +540,21 @@ def nope(disp, reason): "Plugin %r didn't set source_filename for %r" % (plugin, disp.original_filename) ) - reason = self._check_include_omit_etc_internal( - disp.source_filename, frame, - ) + module_globals = frame.f_globals if frame is not None else {} + reason = self._check_include_omit_etc_internal(disp.source_filename, module_globals) if reason: nope(disp, reason) return disp - def _check_include_omit_etc_internal(self, filename, frame): + def _check_include_omit_etc_internal(self, filename, module_globals): """Check a file name against the include, omit, etc, rules. Returns a string or None. String means, don't trace, and is the reason why. None means no reason found to not trace. """ - modulename = self._name_for_module(frame.f_globals, filename) + modulename = self._name_for_module(module_globals, filename) # If the user specified source or include, then that's authoritative # about the outer bound of what to measure and we don't have to apply @@ -599,7 +604,8 @@ def _check_include_omit_etc(self, filename, frame): Returns a boolean: True if the file should be traced, False if not. """ - reason = self._check_include_omit_etc_internal(filename, frame) + module_globals = frame.f_globals if frame is not None else {} + reason = self._check_include_omit_etc_internal(filename, module_globals) if self.debug.should('trace'): if not reason: msg = "Including %r" % (filename,) @@ -698,9 +704,31 @@ def start(self): if self._auto_load: self.load() + # See if we think some code that would eventually be measured has already been imported. + if not Coverage._checked_preimported and self._warn_preimported_source: + if self.include or self.source or self.source_pkgs: + self._check_for_already_imported_files() + Coverage._checked_preimported = True + self.collector.start() self._started = True + def _check_for_already_imported_files(self): + """Examine sys.modules looking for files that will be measured.""" + warned = set() + for mod in list(sys.modules.values()): + filename = getattr(mod, "__file__", None) + if filename is None: + continue + if filename in warned: + continue + + disp = self._should_trace_internal(filename) + if disp.trace: + msg = "Already imported a file that will be measured: {0}".format(filename) + self._warn(msg, slug="already-imported") + warned.add(filename) + def stop(self): """Stop measuring code coverage.""" if self._started: @@ -1277,10 +1305,11 @@ def process_startup(): cov = Coverage(config_file=cps) process_startup.coverage = cov - cov.start() cov._warn_no_data = False cov._warn_unimported_source = False + cov._warn_preimported_source = False cov._auto_save = True + cov.start() return cov diff --git a/coverage/multiproc.py b/coverage/multiproc.py index fe8373189..986ee9d43 100644 --- a/coverage/multiproc.py +++ b/coverage/multiproc.py @@ -33,6 +33,7 @@ def _bootstrap(self): from coverage import Coverage # avoid circular import rcfile = os.environ[COVERAGE_RCFILE_ENV] cov = Coverage(data_suffix=True, config_file=rcfile) + cov._warn_preimported_source = False cov.start() debug = cov.debug try: diff --git a/doc/cmd.rst b/doc/cmd.rst index ef4c1135d..baf1ca083 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -171,6 +171,13 @@ could affect the measurement process. The possible warnings include: when coverage started. This meant coverage.py couldn't monitor its execution. +* "Already imported a file that will be measured: XXX (already-imported)" + + File XXX had already been imported when coverage.py started measurement. Your + setting for ``--source`` or ``--include`` indicates that you wanted to + measure that file. Lines will be missing from the coverage report since the + execution during import hadn't been measured. + * "--include is ignored because --source is set (include-ignored)" Both ``--include`` and ``--source`` were specified while running code. Both diff --git a/igor.py b/igor.py index 43ce3303a..3f5ce12b5 100644 --- a/igor.py +++ b/igor.py @@ -122,11 +122,8 @@ def run_tests_with_coverage(tracer, *runner_args): import coverage cov = coverage.Coverage(config_file="metacov.ini", data_suffix=False) - # Cheap trick: the coverage.py code itself is excluded from measurement, - # but if we clobber the cover_prefix in the coverage object, we can defeat - # the self-detection. - cov.cover_prefix = "Please measure coverage.py!" cov._warn_unimported_source = False + cov._warn_preimported_source = False cov.start() try: diff --git a/tests/test_api.py b/tests/test_api.py index b461c503b..7c2672d87 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -5,6 +5,7 @@ import fnmatch import os +import os.path import sys import textwrap import warnings diff --git a/tests/test_process.py b/tests/test_process.py index 35dddd07e..70329b59e 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -585,6 +585,30 @@ def test_warning_trace_function_changed(self): self.assertIn("Trace function changed", out) + def test_warn_preimported(self): + self.make_file("hello.py", """\ + import goodbye + import coverage + cov = coverage.Coverage(include=["good*"]) + cov.start() + print(goodbye.f()) + cov.stop() + """) + self.make_file("goodbye.py", """\ + def f(): + return "Goodbye!" + """) + goodbye_path = os.path.abspath("goodbye.py") + + out = self.run_command("python hello.py") + self.assertIn("Goodbye!", out) + + msg = ( + "Coverage.py warning: " + "Already imported a file that will be measured: {0} " + "(already-imported)").format(goodbye_path) + self.assertIn(msg, out) + def test_note(self): if env.PYPY and env.PY3 and env.PYPYVERSION[:3] == (5, 10, 0): # https://bitbucket.org/pypy/pypy/issues/2729/pypy3-510-incorrectly-decodes-astral-plane From 9552e3d1c9f054203f577ef0ef697ef836d504b8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 20 Feb 2018 08:29:12 -0500 Subject: [PATCH 009/952] A new feature means next version will be 4.6 --- coverage/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/version.py b/coverage/version.py index dbeb64fbf..8a1b39e1f 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (4, 5, 2, 'alpha', 0) +version_info = (4, 6, 0, 'alpha', 0) def _make_version(major, minor, micro, releaselevel, serial): From cc7ed848cdaa8329edb503db686f3ee2b4be47ae Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 20 Feb 2018 12:03:12 -0500 Subject: [PATCH 010/952] Pragma away some test code that won't be covered --- tests/test_api.py | 2 +- tests/test_process.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 7c2672d87..feb8b2e67 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -582,7 +582,7 @@ def test_source_include_exclusive(self): cov = coverage.Coverage(source=["pkg1"], include=["pkg2"]) with self.assert_warnings(cov, ["--include is ignored because --source is set"]): cov.start() - cov.stop() + cov.stop() # pragma: nested def test_source_package_as_dir(self): # pkg1 is a directory, since we cd'd into tests/modules in setUp. diff --git a/tests/test_process.py b/tests/test_process.py index 70329b59e..85adcf474 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -767,7 +767,7 @@ def test_coverage_run_dashm_is_like_python_dashm(self): self.assert_tryexecfile_output(out_cov, out_py) def test_coverage_run_dir_is_like_python_dir(self): - if sys.version_info == (3, 5, 4, 'final', 0): + if sys.version_info == (3, 5, 4, 'final', 0): # pragma: not covered self.skipTest("3.5.4 broke this: https://bugs.python.org/issue32551") with open(TRY_EXECFILE) as f: self.make_file("with_main/__main__.py", f.read()) From 13e0bb052e7ce46b646f97c19a82c0a0d9b63eb8 Mon Sep 17 00:00:00 2001 From: Thijs Triemstra Date: Tue, 20 Feb 2018 19:54:57 -0500 Subject: [PATCH 011/952] enable pip cache in appveyor build --- appveyor.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index f6b40660b..6ff592afd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,6 +5,9 @@ version: '{branch}-{build}' shallow_clone: true +cache: + - '%LOCALAPPDATA%\pip\Cache' + environment: CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\run_with_env.cmd" From 98f31d8461b2bb80cd9c2c57f9979e3bb1acf8f5 Mon Sep 17 00:00:00 2001 From: Thijs Triemstra Date: Tue, 20 Feb 2018 19:54:58 -0500 Subject: [PATCH 012/952] enable pip cache in travis build and remove duplicate sudo statement --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e28d48b0..bb5a4b158 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ language: python +cache: pip sudo: false python: - '2.6' @@ -18,8 +19,6 @@ env: - COVERAGE_COVERAGE=no - COVERAGE_COVERAGE=yes -sudo: false - install: - pip install -r requirements/ci.pip From 23963b47d4774546e28be0eafdd191f753e0866f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 20 Feb 2018 19:55:36 -0500 Subject: [PATCH 013/952] Thanks, Thijs Triemstra --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 68f189ca5..d3112a144 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -99,6 +99,7 @@ Stephen Finucane Steve Leonard Steve Peak Ted Wexler +Thijs Triemstra Titus Brown Ville Skyttä Yury Selivanov From edf8ff449138ce039e2a1445cab89b53d2333aa3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 22 Feb 2018 06:38:12 -0500 Subject: [PATCH 014/952] More pragmas to fine-tune coverage of test code --- metacov.ini | 4 ++++ tests/test_concurrency.py | 22 +++++++++++----------- tests/test_process.py | 4 ++-- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/metacov.ini b/metacov.ini index 55d0225eb..eebfc0fd4 100644 --- a/metacov.ini +++ b/metacov.ini @@ -35,6 +35,10 @@ exclude_lines = # OS error conditions that we can't (or don't care to) replicate. pragma: cant happen + # Obscure bugs in specific versions of interpreters, and so probably no + # longer tested. + pragma: obscure + # Jython needs special care. pragma: only jython skip.*Jython diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 76e1d9e45..88f2b50d2 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -464,7 +464,7 @@ def test_coverage_stop_in_threads(): has_started_coverage = [] has_stopped_coverage = [] - def run_thread(): + def run_thread(): # pragma: nested """Check that coverage is stopping properly in threads.""" deadline = time.time() + 5 ident = threading.currentThread().ident @@ -480,11 +480,11 @@ def run_thread(): cov = coverage.coverage() cov.start() - t = threading.Thread(target=run_thread) - t.start() + t = threading.Thread(target=run_thread) # pragma: nested + t.start() # pragma: nested - time.sleep(0.1) - cov.stop() + time.sleep(0.1) # pragma: nested + cov.stop() # pragma: nested time.sleep(0.1) assert has_started_coverage == [t.ident] @@ -513,7 +513,7 @@ def test_thread_safe_save_data(tmpdir): for module_name in module_names: import_local_file(module_name) - def random_load(): + def random_load(): # pragma: nested """Import modules randomly to stress coverage.""" while should_run[0]: module_name = random.choice(module_names) @@ -529,12 +529,12 @@ def random_load(): cov = coverage.coverage() cov.start() - threads = [threading.Thread(target=random_load) for _ in range(10)] - should_run[0] = True - for t in threads: + threads = [threading.Thread(target=random_load) for _ in range(10)] # pragma: nested + should_run[0] = True # pragma: nested + for t in threads: # pragma: nested t.start() - time.sleep(duration) + time.sleep(duration) # pragma: nested cov.stop() @@ -546,7 +546,7 @@ def random_load(): for t in threads: t.join() - if (not imported) and duration < 10: + if (not imported) and duration < 10: # pragma: only failure duration *= 2 finally: diff --git a/tests/test_process.py b/tests/test_process.py index 85adcf474..2cabe0b9d 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -610,7 +610,7 @@ def f(): self.assertIn(msg, out) def test_note(self): - if env.PYPY and env.PY3 and env.PYPYVERSION[:3] == (5, 10, 0): + if env.PYPY and env.PY3 and env.PYPYVERSION[:3] == (5, 10, 0): # pragma: obscure # https://bitbucket.org/pypy/pypy/issues/2729/pypy3-510-incorrectly-decodes-astral-plane self.skipTest("Avoid incorrect decoding astral plane JSON chars") self.make_file(".coveragerc", """\ @@ -767,7 +767,7 @@ def test_coverage_run_dashm_is_like_python_dashm(self): self.assert_tryexecfile_output(out_cov, out_py) def test_coverage_run_dir_is_like_python_dir(self): - if sys.version_info == (3, 5, 4, 'final', 0): # pragma: not covered + if sys.version_info == (3, 5, 4, 'final', 0): # pragma: obscure self.skipTest("3.5.4 broke this: https://bugs.python.org/issue32551") with open(TRY_EXECFILE) as f: self.make_file("with_main/__main__.py", f.read()) From 6fbe4f09334060e1f89d4d9a7aab2cfae40f26d8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 24 Feb 2018 07:45:54 -0500 Subject: [PATCH 015/952] Missed a detail of file_reporter in the docs --- coverage/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coverage/plugin.py b/coverage/plugin.py index db7ca0a76..415246ab6 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -134,7 +134,8 @@ def file_reporter(self, filename): # pylint: disable=unused-argument This will only be invoked if `filename` returns non-None from :meth:`file_tracer`. It's an error to return None from this method. - Returns a :class:`FileReporter` object to use to report on `filename`. + Returns a :class:`FileReporter` object to use to report on `filename`, + or the string `"python"` to have coverage.py treat the file as Python. """ _needs_to_implement(self, "file_reporter") From ab1b883cb4ce7cc7a90c6e41545e0bfd1a8d7d05 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 24 Feb 2018 09:41:28 -0500 Subject: [PATCH 016/952] Refactoring to prep for more refactoring --- coverage/control.py | 14 +++++++------- coverage/python.py | 28 +++++++++++++++++----------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index daa00bd0a..cfc00cd2a 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -30,7 +30,7 @@ from coverage.misc import file_be_gone, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins -from coverage.python import PythonFileReporter, source_for_file +from coverage.python import PythonFileReporter, source_for_file, source_for_morf from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -344,11 +344,6 @@ def _init(self): for mod in [contracts, six]: self.cover_paths.append(self._canonical_path(mod)) - # Set the reporting precision. - Numbers.set_precision(self.config.precision) - - atexit.register(self._atexit) - # Create the matchers we need for _should_trace if self.source or self.source_pkgs: self.source_match = TreeMatcher(self.source) @@ -363,6 +358,11 @@ def _init(self): if self.omit: self.omit_match = FnmatchMatcher(self.omit) + # Set the reporting precision. + Numbers.set_precision(self.config.precision) + + atexit.register(self._atexit) + # The user may want to debug things, show info if desired. self._write_startup_debug() @@ -394,7 +394,7 @@ def _canonical_path(self, morf, directory=False): case return its enclosing directory. """ - morf_path = PythonFileReporter(morf, self).filename + morf_path = canonical_filename(source_for_morf(morf)) if morf_path.endswith("__init__.py") or directory: morf_path = os.path.split(morf_path)[0] return morf_path diff --git a/coverage/python.py b/coverage/python.py index 372347f5b..5edfc54dd 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -97,7 +97,7 @@ def get_zip_bytes(filename): def source_for_file(filename): - """Return the source file for `filename`. + """Return the source filename for `filename`. Given a file name being traced, return the best guess as to the source file to attribute it to. @@ -129,22 +129,28 @@ def source_for_file(filename): return filename +def source_for_morf(morf): + """Get the source filename for the module-or-file `morf`.""" + if hasattr(morf, '__file__'): + filename = morf.__file__ + elif isinstance(morf, types.ModuleType): + # A module should have had .__file__, otherwise we can't use it. + # This could be a PEP-420 namespace package. + raise CoverageException("Module {0} has no file".format(morf)) + else: + filename = morf + + filename = source_for_file(files.unicode_filename(filename)) + return filename + + class PythonFileReporter(FileReporter): """Report support for a Python file.""" def __init__(self, morf, coverage=None): self.coverage = coverage - if hasattr(morf, '__file__'): - filename = morf.__file__ - elif isinstance(morf, types.ModuleType): - # A module should have had .__file__, otherwise we can't use it. - # This could be a PEP-420 namespace package. - raise CoverageException("Module {0} has no file".format(morf)) - else: - filename = morf - - filename = source_for_file(files.unicode_filename(filename)) + filename = source_for_morf(morf) super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) From bd613312d29b21b3bce5bb6f7c6561244a6c6830 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 25 Feb 2018 07:34:57 -0500 Subject: [PATCH 017/952] Huge refactor of code out of control into inorout --HG-- branch : inorout --- coverage/collector.py | 6 +- coverage/control.py | 457 ++-------------------------------------- coverage/disposition.py | 37 ++++ coverage/inorout.py | 440 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 499 insertions(+), 441 deletions(-) create mode 100644 coverage/disposition.py create mode 100644 coverage/inorout.py diff --git a/coverage/collector.py b/coverage/collector.py index 72ab32b61..0c3ca9c2f 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -9,6 +9,7 @@ from coverage import env from coverage.backward import litems, range # pylint: disable=redefined-builtin from coverage.debug import short_stack +from coverage.disposition import FileDisposition from coverage.files import abs_file from coverage.misc import CoverageException, isolate_module from coverage.pytracer import PyTracer @@ -33,11 +34,6 @@ CTracer = None -class FileDisposition(object): - """A simple value type for recording what to do with a file.""" - pass - - def should_start_context(frame): """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" fn_name = frame.f_code.co_name diff --git a/coverage/control.py b/coverage/control.py index cfc00cd2a..01feef2be 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -5,14 +5,10 @@ import atexit -import inspect -import itertools import os import platform -import re import sys import time -import traceback from coverage import env from coverage.annotate import AnnotateReporter @@ -21,16 +17,15 @@ from coverage.config import read_coverage_config from coverage.data import CoverageData, CoverageDataFiles from coverage.debug import DebugControl, write_formatted_info -from coverage.files import TreeMatcher, FnmatchMatcher -from coverage.files import PathAliases, find_python_files, prep_patterns -from coverage.files import canonical_filename, set_relative_directory -from coverage.files import ModuleMatcher, abs_file +from coverage.disposition import disposition_debug_msg +from coverage.files import PathAliases, set_relative_directory, abs_file from coverage.html import HtmlReporter +from coverage.inorout import InOrOut from coverage.misc import CoverageException, bool_or_none, join_regex from coverage.misc import file_be_gone, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins -from coverage.python import PythonFileReporter, source_for_file, source_for_morf +from coverage.python import PythonFileReporter from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -43,22 +38,6 @@ os = isolate_module(os) -# Pypy has some unusual stuff in the "stdlib". Consider those locations -# when deciding where the stdlib is. These modules are not used for anything, -# they are modules importable from the pypy lib directories, so that we can -# find those directories. -_structseq = _pypy_irc_topic = None -if env.PYPY: - try: - import _structseq - except ImportError: - pass - - try: - import _pypy_irc_topic - except ImportError: - pass - class Coverage(object): """Programmatic access to coverage.py. @@ -159,12 +138,6 @@ def __init__( self._auto_load = self._auto_save = auto_data self._data_suffix = data_suffix - # The matchers for _should_trace. - self.source_match = None - self.source_pkgs_match = None - self.pylib_match = self.cover_match = None - self.include_match = self.omit_match = None - # Is it ok for no data to be collected? self._warn_no_data = True self._warn_unimported_source = True @@ -174,12 +147,9 @@ def __init__( self._warnings = [] # Other instance attributes, set later. - self.omit = self.include = self.source = None - self.source_pkgs_unmatched = None - self.source_pkgs = None self.data = self.data_files = self.collector = None self.plugins = None - self.pylib_paths = self.cover_paths = None + self.inorout = None self.data_suffix = self.run_suffix = None self._exclude_re = None self.debug = None @@ -238,19 +208,6 @@ def _init(self): # this is a bit childish. :) plugin.configure([self, self.config][int(time.time()) % 2]) - # The source argument can be directories or package names. - self.source = [] - self.source_pkgs = [] - for src in self.config.source or []: - if os.path.isdir(src): - self.source.append(canonical_filename(src)) - else: - self.source_pkgs.append(src) - self.source_pkgs_unmatched = self.source_pkgs[:] - - self.omit = prep_patterns(self.config.run_omit) - self.include = prep_patterns(self.config.run_include) - concurrency = self.config.concurrency or [] if "multiprocessing" in concurrency: if not patch_multiprocessing: @@ -285,6 +242,12 @@ def _init(self): for plugin in self.plugins.file_tracers: plugin._coverage_enabled = False + # Create the file classifying substructure. + self.inorout = InOrOut(warn=self._warn) + self.inorout.configure(self.config) + self.inorout.plugins = self.plugins + self.inorout.disp_class = self.collector.file_disposition_class + # Suffixes are a bit tricky. We want to use the data suffix only when # collecting data, not when combining data. So we save it as # `self.run_suffix` now, and promote it to `self.data_suffix` if we @@ -306,58 +269,6 @@ def _init(self): basename=self.config.data_file, warn=self._warn, debug=self.debug, ) - # The directories for files considered "installed with the interpreter". - self.pylib_paths = set() - if not self.config.cover_pylib: - # Look at where some standard modules are located. That's the - # indication for "installed with the interpreter". In some - # environments (virtualenv, for example), these modules may be - # spread across a few locations. Look at all the candidate modules - # we've imported, and take all the different ones. - for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback): - if m is not None and hasattr(m, "__file__"): - self.pylib_paths.add(self._canonical_path(m, directory=True)) - - if _structseq and not hasattr(_structseq, '__file__'): - # PyPy 2.4 has no __file__ in the builtin modules, but the code - # objects still have the file names. So dig into one to find - # the path to exclude. - structseq_new = _structseq.structseq_new - try: - structseq_file = structseq_new.func_code.co_filename - except AttributeError: - structseq_file = structseq_new.__code__.co_filename - self.pylib_paths.add(self._canonical_path(structseq_file)) - - # To avoid tracing the coverage.py code itself, we skip anything - # located where we are. - self.cover_paths = [self._canonical_path(__file__, directory=True)] - if env.TESTING: - # Don't include our own test code. - self.cover_paths.append(os.path.join(self.cover_paths[0], "tests")) - - # When testing, we use PyContracts, which should be considered - # part of coverage.py, and it uses six. Exclude those directories - # just as we exclude ourselves. - import contracts - import six - for mod in [contracts, six]: - self.cover_paths.append(self._canonical_path(mod)) - - # Create the matchers we need for _should_trace - if self.source or self.source_pkgs: - self.source_match = TreeMatcher(self.source) - self.source_pkgs_match = ModuleMatcher(self.source_pkgs) - else: - if self.cover_paths: - self.cover_match = TreeMatcher(self.cover_paths) - if self.pylib_paths: - self.pylib_match = TreeMatcher(self.pylib_paths) - if self.include: - self.include_match = FnmatchMatcher(self.include) - if self.omit: - self.omit_match = FnmatchMatcher(self.omit) - # Set the reporting precision. Numbers.set_precision(self.config.precision) @@ -386,216 +297,15 @@ def _write_startup_debug(self): if wrote_any: write_formatted_info(self.debug, "end", ()) - def _canonical_path(self, morf, directory=False): - """Return the canonical path of the module or file `morf`. - - If the module is a package, then return its directory. If it is a - module, then return its file, unless `directory` is True, in which - case return its enclosing directory. - - """ - morf_path = canonical_filename(source_for_morf(morf)) - if morf_path.endswith("__init__.py") or directory: - morf_path = os.path.split(morf_path)[0] - return morf_path - - def _name_for_module(self, module_globals, filename): - """Get the name of the module for a set of globals and file name. - - For configurability's sake, we allow __main__ modules to be matched by - their importable name. - - If loaded via runpy (aka -m), we can usually recover the "original" - full dotted module name, otherwise, we resort to interpreting the - file name to get the module's name. In the case that the module name - can't be determined, None is returned. - - """ - if module_globals is None: # pragma: only ironpython - # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 - module_globals = {} - - dunder_name = module_globals.get('__name__', None) - - if isinstance(dunder_name, str) and dunder_name != '__main__': - # This is the usual case: an imported module. - return dunder_name - - loader = module_globals.get('__loader__', None) - for attrname in ('fullname', 'name'): # attribute renamed in py3.2 - if hasattr(loader, attrname): - fullname = getattr(loader, attrname) - else: - continue - - if isinstance(fullname, str) and fullname != '__main__': - # Module loaded via: runpy -m - return fullname - - # Script as first argument to Python command line. - inspectedname = inspect.getmodulename(filename) - if inspectedname is not None: - return inspectedname - else: - return dunder_name - - def _should_trace_internal(self, filename, frame=None): - """Decide whether to trace execution in `filename`, with a reason. - - This function is called from the trace function. As each new file name - is encountered, this function determines whether it is traced or not. - - Returns a FileDisposition object. - - """ - original_filename = filename - disp = _disposition_init(self.collector.file_disposition_class, filename) - - def nope(disp, reason): - """Simple helper to make it easy to return NO.""" - disp.trace = False - disp.reason = reason - return disp - - if frame is not None: - # Compiled Python files have two file names: frame.f_code.co_filename is - # the file name at the time the .pyc was compiled. The second name is - # __file__, which is where the .pyc was actually loaded from. Since - # .pyc files can be moved after compilation (for example, by being - # installed), we look for __file__ in the frame and prefer it to the - # co_filename value. - dunder_file = frame.f_globals and frame.f_globals.get('__file__') - if dunder_file: - filename = source_for_file(dunder_file) - if original_filename and not original_filename.startswith('<'): - orig = os.path.basename(original_filename) - if orig != os.path.basename(filename): - # Files shouldn't be renamed when moved. This happens when - # exec'ing code. If it seems like something is wrong with - # the frame's file name, then just use the original. - filename = original_filename - - if not filename: - # Empty string is pretty useless. - return nope(disp, "empty string isn't a file name") - - if filename.startswith('memory:'): - return nope(disp, "memory isn't traceable") - - if filename.startswith('<'): - # Lots of non-file execution is represented with artificial - # file names like "", "", or - # "". Don't ever trace these executions, since we - # can't do anything with the data later anyway. - return nope(disp, "not a real file name") - - # pyexpat does a dumb thing, calling the trace function explicitly from - # C code with a C file name. - if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename): - return nope(disp, "pyexpat lies about itself") - - # Jython reports the .class file to the tracer, use the source file. - if filename.endswith("$py.class"): - filename = filename[:-9] + ".py" - - canonical = canonical_filename(filename) - disp.canonical_filename = canonical - - # Try the plugins, see if they have an opinion about the file. - plugin = None - for plugin in self.plugins.file_tracers: - if not plugin._coverage_enabled: - continue - - try: - file_tracer = plugin.file_tracer(canonical) - if file_tracer is not None: - file_tracer._coverage_plugin = plugin - disp.trace = True - disp.file_tracer = file_tracer - if file_tracer.has_dynamic_source_filename(): - disp.has_dynamic_filename = True - else: - disp.source_filename = canonical_filename( - file_tracer.source_filename() - ) - break - except Exception: - self._warn( - "Disabling plug-in %r due to an exception:" % ( - plugin._coverage_plugin_name - ) - ) - traceback.print_exc() - plugin._coverage_enabled = False - continue - else: - # No plugin wanted it: it's Python. - disp.trace = True - disp.source_filename = canonical - - if not disp.has_dynamic_filename: - if not disp.source_filename: - raise CoverageException( - "Plugin %r didn't set source_filename for %r" % - (plugin, disp.original_filename) - ) - module_globals = frame.f_globals if frame is not None else {} - reason = self._check_include_omit_etc_internal(disp.source_filename, module_globals) - if reason: - nope(disp, reason) - - return disp - - def _check_include_omit_etc_internal(self, filename, module_globals): - """Check a file name against the include, omit, etc, rules. - - Returns a string or None. String means, don't trace, and is the reason - why. None means no reason found to not trace. - - """ - modulename = self._name_for_module(module_globals, filename) - - # If the user specified source or include, then that's authoritative - # about the outer bound of what to measure and we don't have to apply - # any canned exclusions. If they didn't, then we have to exclude the - # stdlib and coverage.py directories. - if self.source_match: - if self.source_pkgs_match.match(modulename): - if modulename in self.source_pkgs_unmatched: - self.source_pkgs_unmatched.remove(modulename) - elif not self.source_match.match(filename): - return "falls outside the --source trees" - elif self.include_match: - if not self.include_match.match(filename): - return "falls outside the --include trees" - else: - # If we aren't supposed to trace installed code, then check if this - # is near the Python standard library and skip it if so. - if self.pylib_match and self.pylib_match.match(filename): - return "is in the stdlib" - - # We exclude the coverage.py code itself, since a little of it - # will be measured otherwise. - if self.cover_match and self.cover_match.match(filename): - return "is part of coverage.py" - - # Check the file against the omit pattern. - if self.omit_match and self.omit_match.match(filename): - return "is inside an --omit pattern" - - # No reason found to skip this file. - return None - def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. Calls `_should_trace_internal`, and returns the FileDisposition. """ - disp = self._should_trace_internal(filename, frame) + disp = self.inorout.should_trace(filename, frame) if self.debug.should('trace'): - self.debug.write(_disposition_debug_msg(disp)) + self.debug.write(disposition_debug_msg(disp)) return disp def _check_include_omit_etc(self, filename, frame): @@ -605,7 +315,7 @@ def _check_include_omit_etc(self, filename, frame): """ module_globals = frame.f_globals if frame is not None else {} - reason = self._check_include_omit_etc_internal(filename, module_globals) + reason = self.inorout.check_include_omit_etc(filename, module_globals) if self.debug.should('trace'): if not reason: msg = "Including %r" % (filename,) @@ -694,9 +404,8 @@ def start(self): """ self._init() - if self.include: - if self.source or self.source_pkgs: - self._warn("--include is ignored because --source is set", slug="include-ignored") + self.inorout.warn_conflicting_settings() + if self.run_suffix: # Calling start() means we're running code, so use the run_suffix # as the data_suffix when we eventually save the data. @@ -706,29 +415,11 @@ def start(self): # See if we think some code that would eventually be measured has already been imported. if not Coverage._checked_preimported and self._warn_preimported_source: - if self.include or self.source or self.source_pkgs: - self._check_for_already_imported_files() - Coverage._checked_preimported = True + Coverage._checked_preimported = self.inorout.warn_already_imported_files() self.collector.start() self._started = True - def _check_for_already_imported_files(self): - """Examine sys.modules looking for files that will be measured.""" - warned = set() - for mod in list(sys.modules.values()): - filename = getattr(mod, "__file__", None) - if filename is None: - continue - if filename in warned: - continue - - disp = self._should_trace_internal(filename) - if disp.trace: - msg = "Already imported a file that will be measured: {0}".format(filename) - self._warn(msg, slug="already-imported") - warned.add(filename) - def stop(self): """Stop measuring code coverage.""" if self._started: @@ -873,83 +564,19 @@ def _post_save_work(self): # If there are still entries in the source_pkgs_unmatched list, # then we never encountered those packages. if self._warn_unimported_source: - for pkg in self.source_pkgs_unmatched: - self._warn_about_unmeasured_code(pkg) + self.inorout.warn_unimported_source() # Find out if we got any data. if not self.data and self._warn_no_data: self._warn("No data was collected.", slug="no-data-collected") # Find files that were never executed at all. - for pkg in self.source_pkgs: - if (not pkg in sys.modules or - not hasattr(sys.modules[pkg], '__file__') or - not os.path.exists(sys.modules[pkg].__file__)): - continue - pkg_file = source_for_file(sys.modules[pkg].__file__) - self._find_unexecuted_files(self._canonical_path(pkg_file)) - - for src in self.source: - self._find_unexecuted_files(src) + for file_path, plugin_name in self.inorout.find_unexecuted_files(): + self.data.touch_file(file_path, plugin_name) if self.config.note: self.data.add_run_info(note=self.config.note) - def _warn_about_unmeasured_code(self, pkg): - """Warn about a package or module that we never traced. - - `pkg` is a string, the name of the package or module. - - """ - mod = sys.modules.get(pkg) - if mod is None: - self._warn("Module %s was never imported." % pkg, slug="module-not-imported") - return - - is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__') - has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__) - - if is_namespace: - # A namespace package. It's OK for this not to have been traced, - # since there is no code directly in it. - return - - if not has_file: - self._warn("Module %s has no Python source." % pkg, slug="module-not-python") - return - - # The module was in sys.modules, and seems like a module with code, but - # we never measured it. I guess that means it was imported before - # coverage even started. - self._warn( - "Module %s was previously imported, but not measured" % pkg, - slug="module-not-measured", - ) - - def _find_plugin_files(self, src_dir): - """Get executable files from the plugins.""" - for plugin in self.plugins.file_tracers: - for x_file in plugin.find_executable_files(src_dir): - yield x_file, plugin._coverage_plugin_name - - def _find_unexecuted_files(self, src_dir): - """Find unexecuted files in `src_dir`. - - Search for files in `src_dir` that are probably importable, - and add them as unexecuted files in `self.data`. - - """ - py_files = ((py_file, None) for py_file in find_python_files(src_dir)) - plugin_files = self._find_plugin_files(src_dir) - - for file_path, plugin_name in itertools.chain(py_files, plugin_files): - file_path = canonical_filename(file_path) - if self.omit_match and self.omit_match.match(file_path): - # Turns out this file was omitted, so don't pull it back - # in as unexecuted. - continue - self.data.touch_file(file_path, plugin_name) - # Backward compatibility with version 1. def analysis(self, morf): """Like `analysis2` but doesn't return excluded line numbers.""" @@ -1193,8 +820,6 @@ def plugin_info(plugins): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('cover_paths', self.cover_paths), - ('pylib_paths', self.pylib_paths), ('tracer', self.collector.tracer_name()), ('plugins.file_tracers', plugin_info(self.plugins.file_tracers)), ('plugins.configurers', plugin_info(self.plugins.configurers)), @@ -1215,51 +840,11 @@ def plugin_info(plugins): ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] - matcher_names = [ - 'source_match', 'source_pkgs_match', - 'include_match', 'omit_match', - 'cover_match', 'pylib_match', - ] - - for matcher_name in matcher_names: - matcher = getattr(self, matcher_name) - if matcher: - matcher_info = matcher.info() - else: - matcher_info = '-none-' - info.append((matcher_name, matcher_info)) + info.extend(self.inorout.sys_info()) return info -# FileDisposition "methods": FileDisposition is a pure value object, so it can -# be implemented in either C or Python. Acting on them is done with these -# functions. - -def _disposition_init(cls, original_filename): - """Construct and initialize a new FileDisposition object.""" - disp = cls() - disp.original_filename = original_filename - disp.canonical_filename = original_filename - disp.source_filename = None - disp.trace = False - disp.reason = "" - disp.file_tracer = None - disp.has_dynamic_filename = False - return disp - - -def _disposition_debug_msg(disp): - """Make a nice debug message of what the FileDisposition is doing.""" - if disp.trace: - msg = "Tracing %r" % (disp.original_filename,) - if disp.file_tracer: - msg += ": will be traced by %r" % disp.file_tracer - else: - msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason) - return msg - - def process_startup(): """Call this at Python start-up to perhaps measure coverage. diff --git a/coverage/disposition.py b/coverage/disposition.py new file mode 100644 index 000000000..e9b8ba65c --- /dev/null +++ b/coverage/disposition.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Simple value objects for tracking what to do with files.""" + + +class FileDisposition(object): + """A simple value type for recording what to do with a file.""" + pass + + +# FileDisposition "methods": FileDisposition is a pure value object, so it can +# be implemented in either C or Python. Acting on them is done with these +# functions. + +def disposition_init(cls, original_filename): + """Construct and initialize a new FileDisposition object.""" + disp = cls() + disp.original_filename = original_filename + disp.canonical_filename = original_filename + disp.source_filename = None + disp.trace = False + disp.reason = "" + disp.file_tracer = None + disp.has_dynamic_filename = False + return disp + + +def disposition_debug_msg(disp): + """Make a nice debug message of what the FileDisposition is doing.""" + if disp.trace: + msg = "Tracing %r" % (disp.original_filename,) + if disp.file_tracer: + msg += ": will be traced by %r" % disp.file_tracer + else: + msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason) + return msg diff --git a/coverage/inorout.py b/coverage/inorout.py new file mode 100644 index 000000000..4fcec8e07 --- /dev/null +++ b/coverage/inorout.py @@ -0,0 +1,440 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Determining whether files are being measured/reported or not.""" + +# For finding the stdlib +import atexit +import inspect +import itertools +import os +import platform +import re +import sys +import traceback + +from coverage import env +from coverage.disposition import FileDisposition, disposition_init +from coverage.files import TreeMatcher, FnmatchMatcher, ModuleMatcher +from coverage.files import prep_patterns, find_python_files, canonical_filename +from coverage.misc import CoverageException +from coverage.python import source_for_file, source_for_morf + + +# Pypy has some unusual stuff in the "stdlib". Consider those locations +# when deciding where the stdlib is. These modules are not used for anything, +# they are modules importable from the pypy lib directories, so that we can +# find those directories. +_structseq = _pypy_irc_topic = None +if env.PYPY: + try: + import _structseq + except ImportError: + pass + + try: + import _pypy_irc_topic + except ImportError: + pass + + +def canonical_path(morf, directory=False): + """Return the canonical path of the module or file `morf`. + + If the module is a package, then return its directory. If it is a + module, then return its file, unless `directory` is True, in which + case return its enclosing directory. + + """ + morf_path = canonical_filename(source_for_morf(morf)) + if morf_path.endswith("__init__.py") or directory: + morf_path = os.path.split(morf_path)[0] + return morf_path + + +def name_for_module(module_globals, filename): + """Get the name of the module for a set of globals and file name. + + For configurability's sake, we allow __main__ modules to be matched by + their importable name. + + If loaded via runpy (aka -m), we can usually recover the "original" + full dotted module name, otherwise, we resort to interpreting the + file name to get the module's name. In the case that the module name + can't be determined, None is returned. + + """ + if module_globals is None: # pragma: only ironpython + # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 + module_globals = {} + + dunder_name = module_globals.get('__name__', None) + + if isinstance(dunder_name, str) and dunder_name != '__main__': + # This is the usual case: an imported module. + return dunder_name + + loader = module_globals.get('__loader__', None) + for attrname in ('fullname', 'name'): # attribute renamed in py3.2 + if hasattr(loader, attrname): + fullname = getattr(loader, attrname) + else: + continue + + if isinstance(fullname, str) and fullname != '__main__': + # Module loaded via: runpy -m + return fullname + + # Script as first argument to Python command line. + inspectedname = inspect.getmodulename(filename) + if inspectedname is not None: + return inspectedname + else: + return dunder_name + + +class InOrOut(object): + def __init__(self, warn): + self.warn = warn + + # The matchers for should_trace. + self.source_match = None + self.source_pkgs_match = None + self.pylib_paths = self.cover_paths = None + self.pylib_match = self.cover_match = None + self.include_match = self.omit_match = None + self.plugins = [] + self.disp_class = FileDisposition + + # The source argument can be directories or package names. + self.source = [] + self.source_pkgs = [] + self.omit = self.include = None + + def configure(self, config): + for src in config.source or []: + if os.path.isdir(src): + self.source.append(canonical_filename(src)) + else: + self.source_pkgs.append(src) + self.source_pkgs_unmatched = self.source_pkgs[:] + + self.omit = prep_patterns(config.run_omit) + self.include = prep_patterns(config.run_include) + + # The directories for files considered "installed with the interpreter". + self.pylib_paths = set() + if not config.cover_pylib: + # Look at where some standard modules are located. That's the + # indication for "installed with the interpreter". In some + # environments (virtualenv, for example), these modules may be + # spread across a few locations. Look at all the candidate modules + # we've imported, and take all the different ones. + for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback): + if m is not None and hasattr(m, "__file__"): + self.pylib_paths.add(canonical_path(m, directory=True)) + + if _structseq and not hasattr(_structseq, '__file__'): + # PyPy 2.4 has no __file__ in the builtin modules, but the code + # objects still have the file names. So dig into one to find + # the path to exclude. + structseq_new = _structseq.structseq_new + try: + structseq_file = structseq_new.func_code.co_filename + except AttributeError: + structseq_file = structseq_new.__code__.co_filename + self.pylib_paths.add(canonical_path(structseq_file)) + + # To avoid tracing the coverage.py code itself, we skip anything + # located where we are. + self.cover_paths = [canonical_path(__file__, directory=True)] + if env.TESTING: + # Don't include our own test code. + self.cover_paths.append(os.path.join(self.cover_paths[0], "tests")) + + # When testing, we use PyContracts, which should be considered + # part of coverage.py, and it uses six. Exclude those directories + # just as we exclude ourselves. + import contracts + import six + for mod in [contracts, six]: + self.cover_paths.append(canonical_path(mod)) + + # Create the matchers we need for should_trace + if self.source or self.source_pkgs: + self.source_match = TreeMatcher(self.source) + self.source_pkgs_match = ModuleMatcher(self.source_pkgs) + else: + if self.cover_paths: + self.cover_match = TreeMatcher(self.cover_paths) + if self.pylib_paths: + self.pylib_match = TreeMatcher(self.pylib_paths) + if self.include: + self.include_match = FnmatchMatcher(self.include) + if self.omit: + self.omit_match = FnmatchMatcher(self.omit) + + def should_trace(self, filename, frame=None): + """Decide whether to trace execution in `filename`, with a reason. + + This function is called from the trace function. As each new file name + is encountered, this function determines whether it is traced or not. + + Returns a FileDisposition object. + + """ + original_filename = filename + disp = disposition_init(self.disp_class, filename) + + def nope(disp, reason): + """Simple helper to make it easy to return NO.""" + disp.trace = False + disp.reason = reason + return disp + + if frame is not None: + # Compiled Python files have two file names: frame.f_code.co_filename is + # the file name at the time the .pyc was compiled. The second name is + # __file__, which is where the .pyc was actually loaded from. Since + # .pyc files can be moved after compilation (for example, by being + # installed), we look for __file__ in the frame and prefer it to the + # co_filename value. + dunder_file = frame.f_globals and frame.f_globals.get('__file__') + if dunder_file: + filename = source_for_file(dunder_file) + if original_filename and not original_filename.startswith('<'): + orig = os.path.basename(original_filename) + if orig != os.path.basename(filename): + # Files shouldn't be renamed when moved. This happens when + # exec'ing code. If it seems like something is wrong with + # the frame's file name, then just use the original. + filename = original_filename + + if not filename: + # Empty string is pretty useless. + return nope(disp, "empty string isn't a file name") + + if filename.startswith('memory:'): + return nope(disp, "memory isn't traceable") + + if filename.startswith('<'): + # Lots of non-file execution is represented with artificial + # file names like "", "", or + # "". Don't ever trace these executions, since we + # can't do anything with the data later anyway. + return nope(disp, "not a real file name") + + # pyexpat does a dumb thing, calling the trace function explicitly from + # C code with a C file name. + if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename): + return nope(disp, "pyexpat lies about itself") + + # Jython reports the .class file to the tracer, use the source file. + if filename.endswith("$py.class"): + filename = filename[:-9] + ".py" + + canonical = canonical_filename(filename) + disp.canonical_filename = canonical + + # Try the plugins, see if they have an opinion about the file. + plugin = None + for plugin in self.plugins.file_tracers: + if not plugin._coverage_enabled: + continue + + try: + file_tracer = plugin.file_tracer(canonical) + if file_tracer is not None: + file_tracer._coverage_plugin = plugin + disp.trace = True + disp.file_tracer = file_tracer + if file_tracer.has_dynamic_source_filename(): + disp.has_dynamic_filename = True + else: + disp.source_filename = canonical_filename( + file_tracer.source_filename() + ) + break + except Exception: + self.warn( + "Disabling plug-in %r due to an exception:" % (plugin._coverage_plugin_name) + ) + traceback.print_exc() + plugin._coverage_enabled = False + continue + else: + # No plugin wanted it: it's Python. + disp.trace = True + disp.source_filename = canonical + + if not disp.has_dynamic_filename: + if not disp.source_filename: + raise CoverageException( + "Plugin %r didn't set source_filename for %r" % + (plugin, disp.original_filename) + ) + module_globals = frame.f_globals if frame is not None else {} + reason = self.check_include_omit_etc(disp.source_filename, module_globals) + if reason: + nope(disp, reason) + + return disp + + def check_include_omit_etc(self, filename, module_globals): + """Check a file name against the include, omit, etc, rules. + + Returns a string or None. String means, don't trace, and is the reason + why. None means no reason found to not trace. + + """ + modulename = name_for_module(module_globals, filename) + + # If the user specified source or include, then that's authoritative + # about the outer bound of what to measure and we don't have to apply + # any canned exclusions. If they didn't, then we have to exclude the + # stdlib and coverage.py directories. + if self.source_match: + if self.source_pkgs_match.match(modulename): + if modulename in self.source_pkgs_unmatched: + self.source_pkgs_unmatched.remove(modulename) + elif not self.source_match.match(filename): + return "falls outside the --source trees" + elif self.include_match: + if not self.include_match.match(filename): + return "falls outside the --include trees" + else: + # If we aren't supposed to trace installed code, then check if this + # is near the Python standard library and skip it if so. + if self.pylib_match and self.pylib_match.match(filename): + return "is in the stdlib" + + # We exclude the coverage.py code itself, since a little of it + # will be measured otherwise. + if self.cover_match and self.cover_match.match(filename): + return "is part of coverage.py" + + # Check the file against the omit pattern. + if self.omit_match and self.omit_match.match(filename): + return "is inside an --omit pattern" + + # No reason found to skip this file. + return None + + def warn_conflicting_settings(self): + if self.include: + if self.source or self.source_pkgs: + self.warn("--include is ignored because --source is set", slug="include-ignored") + + def warn_already_imported_files(self): + if self.include or self.source or self.source_pkgs: + warned = set() + for mod in list(sys.modules.values()): + filename = getattr(mod, "__file__", None) + if filename is None: + continue + if filename in warned: + continue + + disp = self.should_trace(filename) + if disp.trace: + msg = "Already imported a file that will be measured: {0}".format(filename) + self.warn(msg, slug="already-imported") + warned.add(filename) + + return True + return False + + def warn_unimported_source(self): + for pkg in self.source_pkgs_unmatched: + self.warn_about_unmeasured_code(pkg) + + def warn_about_unmeasured_code(self, pkg): + """Warn about a package or module that we never traced. + + `pkg` is a string, the name of the package or module. + + """ + mod = sys.modules.get(pkg) + if mod is None: + self.warn("Module %s was never imported." % pkg, slug="module-not-imported") + return + + is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__') + has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__) + + if is_namespace: + # A namespace package. It's OK for this not to have been traced, + # since there is no code directly in it. + return + + if not has_file: + self.warn("Module %s has no Python source." % pkg, slug="module-not-python") + return + + # The module was in sys.modules, and seems like a module with code, but + # we never measured it. I guess that means it was imported before + # coverage even started. + self.warn( + "Module %s was previously imported, but not measured" % pkg, + slug="module-not-measured", + ) + + def find_unexecuted_files(self): + for pkg in self.source_pkgs: + if (not pkg in sys.modules or + not hasattr(sys.modules[pkg], '__file__') or + not os.path.exists(sys.modules[pkg].__file__)): + continue + pkg_file = source_for_file(sys.modules[pkg].__file__) + for ret in self._find_unexecuted_files(canonical_path(pkg_file)): + yield ret + + for src in self.source: + for ret in self._find_unexecuted_files(src): + yield ret + + def _find_plugin_files(self, src_dir): + """Get executable files from the plugins.""" + for plugin in self.plugins.file_tracers: + for x_file in plugin.find_executable_files(src_dir): + yield x_file, plugin._coverage_plugin_name + + def _find_unexecuted_files(self, src_dir): + """Find unexecuted files in `src_dir`. + + Search for files in `src_dir` that are probably importable, + and add them as unexecuted files in `self.data`. + + """ + py_files = ((py_file, None) for py_file in find_python_files(src_dir)) + plugin_files = self._find_plugin_files(src_dir) + + for file_path, plugin_name in itertools.chain(py_files, plugin_files): + file_path = canonical_filename(file_path) + if self.omit_match and self.omit_match.match(file_path): + # Turns out this file was omitted, so don't pull it back + # in as unexecuted. + continue + yield file_path, plugin_name + + def sys_info(self): + info = [ + ('cover_paths', self.cover_paths), + ('pylib_paths', self.pylib_paths), + ] + + matcher_names = [ + 'source_match', 'source_pkgs_match', + 'include_match', 'omit_match', + 'cover_match', 'pylib_match', + ] + + for matcher_name in matcher_names: + matcher = getattr(self, matcher_name) + if matcher: + matcher_info = matcher.info() + else: + matcher_info = '-none-' + info.append((matcher_name, matcher_info)) + + return info From 33cd875fc373ede7ca5b6ebde504fe7701532bf5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 26 Feb 2018 08:43:58 -0500 Subject: [PATCH 018/952] More uniformity --HG-- branch : inorout --- coverage/control.py | 3 +-- coverage/inorout.py | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 01feef2be..346ccf2bb 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -314,8 +314,7 @@ def _check_include_omit_etc(self, filename, frame): Returns a boolean: True if the file should be traced, False if not. """ - module_globals = frame.f_globals if frame is not None else {} - reason = self.inorout.check_include_omit_etc(filename, module_globals) + reason = self.inorout.check_include_omit_etc(filename, frame) if self.debug.should('trace'): if not reason: msg = "Including %r" % (filename,) diff --git a/coverage/inorout.py b/coverage/inorout.py index 4fcec8e07..7bb89f161 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -52,8 +52,8 @@ def canonical_path(morf, directory=False): return morf_path -def name_for_module(module_globals, filename): - """Get the name of the module for a set of globals and file name. +def name_for_module(filename, frame): + """Get the name of the module for a filename and frame. For configurability's sake, we allow __main__ modules to be matched by their importable name. @@ -64,6 +64,7 @@ def name_for_module(module_globals, filename): can't be determined, None is returned. """ + module_globals = frame.f_globals if frame is not None else {} if module_globals is None: # pragma: only ironpython # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 module_globals = {} @@ -273,21 +274,20 @@ def nope(disp, reason): "Plugin %r didn't set source_filename for %r" % (plugin, disp.original_filename) ) - module_globals = frame.f_globals if frame is not None else {} - reason = self.check_include_omit_etc(disp.source_filename, module_globals) + reason = self.check_include_omit_etc(disp.source_filename, frame) if reason: nope(disp, reason) return disp - def check_include_omit_etc(self, filename, module_globals): + def check_include_omit_etc(self, filename, frame): """Check a file name against the include, omit, etc, rules. Returns a string or None. String means, don't trace, and is the reason why. None means no reason found to not trace. """ - modulename = name_for_module(module_globals, filename) + modulename = name_for_module(filename, frame) # If the user specified source or include, then that's authoritative # about the outer bound of what to measure and we don't have to apply From e6a4d624ee52194e37c574e21f6b30915be75966 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 4 Mar 2018 19:54:49 -0500 Subject: [PATCH 019/952] A more disciplined way to manage inorout --HG-- branch : inorout --- coverage/control.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 346ccf2bb..77efe28b4 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -149,7 +149,8 @@ def __init__( # Other instance attributes, set later. self.data = self.data_files = self.collector = None self.plugins = None - self.inorout = None + self._inorout = None + self._inorout_class = InOrOut self.data_suffix = self.run_suffix = None self._exclude_re = None self.debug = None @@ -243,10 +244,10 @@ def _init(self): plugin._coverage_enabled = False # Create the file classifying substructure. - self.inorout = InOrOut(warn=self._warn) - self.inorout.configure(self.config) - self.inorout.plugins = self.plugins - self.inorout.disp_class = self.collector.file_disposition_class + self._inorout = self._inorout_class(warn=self._warn) + self._inorout.configure(self.config) + self._inorout.plugins = self.plugins + self._inorout.disp_class = self.collector.file_disposition_class # Suffixes are a bit tricky. We want to use the data suffix only when # collecting data, not when combining data. So we save it as @@ -303,7 +304,7 @@ def _should_trace(self, filename, frame): Calls `_should_trace_internal`, and returns the FileDisposition. """ - disp = self.inorout.should_trace(filename, frame) + disp = self._inorout.should_trace(filename, frame) if self.debug.should('trace'): self.debug.write(disposition_debug_msg(disp)) return disp @@ -314,7 +315,7 @@ def _check_include_omit_etc(self, filename, frame): Returns a boolean: True if the file should be traced, False if not. """ - reason = self.inorout.check_include_omit_etc(filename, frame) + reason = self._inorout.check_include_omit_etc(filename, frame) if self.debug.should('trace'): if not reason: msg = "Including %r" % (filename,) @@ -403,7 +404,7 @@ def start(self): """ self._init() - self.inorout.warn_conflicting_settings() + self._inorout.warn_conflicting_settings() if self.run_suffix: # Calling start() means we're running code, so use the run_suffix @@ -414,7 +415,7 @@ def start(self): # See if we think some code that would eventually be measured has already been imported. if not Coverage._checked_preimported and self._warn_preimported_source: - Coverage._checked_preimported = self.inorout.warn_already_imported_files() + Coverage._checked_preimported = self._inorout.warn_already_imported_files() self.collector.start() self._started = True @@ -563,14 +564,14 @@ def _post_save_work(self): # If there are still entries in the source_pkgs_unmatched list, # then we never encountered those packages. if self._warn_unimported_source: - self.inorout.warn_unimported_source() + self._inorout.warn_unimported_source() # Find out if we got any data. if not self.data and self._warn_no_data: self._warn("No data was collected.", slug="no-data-collected") # Find files that were never executed at all. - for file_path, plugin_name in self.inorout.find_unexecuted_files(): + for file_path, plugin_name in self._inorout.find_unexecuted_files(): self.data.touch_file(file_path, plugin_name) if self.config.note: @@ -839,7 +840,7 @@ def plugin_info(plugins): ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] - info.extend(self.inorout.sys_info()) + info.extend(self._inorout.sys_info()) return info From c9f4f661ccb7decb55055d80a1e9a1cbb825b27f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 5 Mar 2018 18:19:43 -0500 Subject: [PATCH 020/952] In 3.7, namespace modules can have: mod.__file__ is None --- coverage/inorout.py | 23 ++++++++++++++++------- coverage/python.py | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/coverage/inorout.py b/coverage/inorout.py index 7bb89f161..640b4e6f1 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -94,6 +94,19 @@ def name_for_module(filename, frame): return dunder_name +def module_is_namespace(mod): + """Is the module object `mod` a PEP420 namespace module?""" + return hasattr(mod, '__path__') and getattr(mod, '__file__', None) is None + + +def module_has_file(mod): + """Does the module object `mod` have an existing __file__ ?""" + mod__file__ = getattr(mod, '__file__', None) + if mod__file__ is None: + return False + return os.path.exists(mod__file__) + + class InOrOut(object): def __init__(self, warn): self.warn = warn @@ -359,15 +372,12 @@ def warn_about_unmeasured_code(self, pkg): self.warn("Module %s was never imported." % pkg, slug="module-not-imported") return - is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__') - has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__) - - if is_namespace: + if module_is_namespace(mod): # A namespace package. It's OK for this not to have been traced, # since there is no code directly in it. return - if not has_file: + if not module_has_file(mod): self.warn("Module %s has no Python source." % pkg, slug="module-not-python") return @@ -382,8 +392,7 @@ def warn_about_unmeasured_code(self, pkg): def find_unexecuted_files(self): for pkg in self.source_pkgs: if (not pkg in sys.modules or - not hasattr(sys.modules[pkg], '__file__') or - not os.path.exists(sys.modules[pkg].__file__)): + not module_has_file(sys.modules[pkg])): continue pkg_file = source_for_file(sys.modules[pkg].__file__) for ret in self._find_unexecuted_files(canonical_path(pkg_file)): diff --git a/coverage/python.py b/coverage/python.py index 5edfc54dd..834bc3321 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -131,7 +131,7 @@ def source_for_file(filename): def source_for_morf(morf): """Get the source filename for the module-or-file `morf`.""" - if hasattr(morf, '__file__'): + if hasattr(morf, '__file__') and morf.__file__: filename = morf.__file__ elif isinstance(morf, types.ModuleType): # A module should have had .__file__, otherwise we can't use it. From 232db5d57c53384162e5704ff405671acd9fe27b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 6 Mar 2018 07:15:17 -0500 Subject: [PATCH 021/952] In 3.7, testing libs are issuing warnings. We don't want them to count against us --- tests/test_process.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_process.py b/tests/test_process.py index 2cabe0b9d..9d4aa56eb 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -690,6 +690,11 @@ def test_deprecation_warnings(self): import coverage print("No warnings!") """) + + # Some of our testing infrastructure can issue warnings. + # Turn it all off for the sub-process. + self.del_environ("COVERAGE_TESTING") + out = self.run_command("python allok.py") self.assertEqual(out, "No warnings!\n") From 94ab016d224eedd46702005398ce53174d65273b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 6 Mar 2018 08:41:32 -0500 Subject: [PATCH 022/952] check_preimported=True controls whether coverage checks pre-imported files when starting --- coverage/cmdline.py | 1 + coverage/control.py | 19 ++++++++++++------- coverage/inorout.py | 3 --- tests/test_cmdline.py | 2 +- tests/test_process.py | 2 +- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 7b86054e3..04cf8e89b 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -475,6 +475,7 @@ def command_line(self, argv): include=include, debug=debug, concurrency=options.concurrency, + check_preimported=True, ) if options.action == "debug": diff --git a/coverage/control.py b/coverage/control.py index 77efe28b4..55c031ae7 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -53,15 +53,12 @@ class Coverage(object): cov.html_report(directory='covhtml') """ - # A global to know if we have ever checked for files imported before - # coverage has been started. - _checked_preimported = False def __init__( self, data_file=None, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, source=None, omit=None, include=None, debug=None, - concurrency=None, + concurrency=None, check_preimported=False, ): """ `data_file` is the base name of the data file to use, defaulting to @@ -115,12 +112,20 @@ def __init__( "eventlet", "gevent", "multiprocessing", or "thread" (the default). This can also be a list of these strings. + If `check_preimported` is true, then when coverage is started, the + aleady-imported files will be checked to see if they should be measured + by coverage. Importing measured files before coverage is started can + mean that code is missed. + .. versionadded:: 4.0 The `concurrency` parameter. .. versionadded:: 4.2 The `concurrency` parameter can now be a list of strings. + .. versionadded:: 4.6 + The `check_preimported` parameter. + """ # Build our configuration from a number of sources. self.config_file, self.config = read_coverage_config( @@ -141,7 +146,7 @@ def __init__( # Is it ok for no data to be collected? self._warn_no_data = True self._warn_unimported_source = True - self._warn_preimported_source = True + self._warn_preimported_source = check_preimported # A record of all the warnings that have been issued. self._warnings = [] @@ -414,8 +419,8 @@ def start(self): self.load() # See if we think some code that would eventually be measured has already been imported. - if not Coverage._checked_preimported and self._warn_preimported_source: - Coverage._checked_preimported = self._inorout.warn_already_imported_files() + if self._warn_preimported_source: + self._inorout.warn_already_imported_files() self.collector.start() self._started = True diff --git a/coverage/inorout.py b/coverage/inorout.py index 640b4e6f1..53ed8ea9b 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -354,9 +354,6 @@ def warn_already_imported_files(self): self.warn(msg, slug="already-imported") warned.add(filename) - return True - return False - def warn_unimported_source(self): for pkg in self.source_pkgs_unmatched: self.warn_about_unmeasured_code(pkg) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 1b7c66536..66fcec3a9 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -33,7 +33,7 @@ class BaseCmdLineTest(CoverageTest): 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, + concurrency=None, check_preimported=True, ) defaults.annotate( directory=None, ignore_errors=None, include=None, omit=None, morfs=[], diff --git a/tests/test_process.py b/tests/test_process.py index 9d4aa56eb..fb08562fa 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -589,7 +589,7 @@ def test_warn_preimported(self): self.make_file("hello.py", """\ import goodbye import coverage - cov = coverage.Coverage(include=["good*"]) + cov = coverage.Coverage(include=["good*"], check_preimported=True) cov.start() print(goodbye.f()) cov.stop() From 446d66e3adff0557241b3de6b2d7840ebd344565 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 13 Mar 2018 10:21:52 -0400 Subject: [PATCH 023/952] Finish up the inorout docstrings --- coverage/inorout.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/coverage/inorout.py b/coverage/inorout.py index 53ed8ea9b..c0f27d786 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -108,6 +108,8 @@ def module_has_file(mod): class InOrOut(object): + """Machinery for determining what files to measure.""" + def __init__(self, warn): self.warn = warn @@ -123,9 +125,11 @@ def __init__(self, warn): # The source argument can be directories or package names. self.source = [] self.source_pkgs = [] + self.source_pkgs_unmatched = [] self.omit = self.include = None def configure(self, config): + """Apply the configuration to get ready for decision-time.""" for src in config.source or []: if os.path.isdir(src): self.source.append(canonical_filename(src)) @@ -334,11 +338,13 @@ def check_include_omit_etc(self, filename, frame): return None def warn_conflicting_settings(self): + """Warn if there are settings that conflict.""" if self.include: if self.source or self.source_pkgs: self.warn("--include is ignored because --source is set", slug="include-ignored") def warn_already_imported_files(self): + """Warn if files have already been imported that we will be measuring.""" if self.include or self.source or self.source_pkgs: warned = set() for mod in list(sys.modules.values()): @@ -355,10 +361,11 @@ def warn_already_imported_files(self): warned.add(filename) def warn_unimported_source(self): + """Warn about source packages that were of interest, but never traced.""" for pkg in self.source_pkgs_unmatched: - self.warn_about_unmeasured_code(pkg) + self._warn_about_unmeasured_code(pkg) - def warn_about_unmeasured_code(self, pkg): + def _warn_about_unmeasured_code(self, pkg): """Warn about a package or module that we never traced. `pkg` is a string, the name of the package or module. @@ -387,6 +394,10 @@ def warn_about_unmeasured_code(self, pkg): ) def find_unexecuted_files(self): + """Find files in the areas of interest that weren't traced. + + Yields pairs: file path, and responsible plug-in name. + """ for pkg in self.source_pkgs: if (not pkg in sys.modules or not module_has_file(sys.modules[pkg])): @@ -424,6 +435,10 @@ def _find_unexecuted_files(self, src_dir): yield file_path, plugin_name def sys_info(self): + """Our information for Coverage.sys_info. + + Returns a list of (key, value) pairs. + """ info = [ ('cover_paths', self.cover_paths), ('pylib_paths', self.pylib_paths), From 46381f9c554260f56edef5b204c613a925f53618 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 13 Mar 2018 10:29:57 -0400 Subject: [PATCH 024/952] Private attributes should be indicated --- coverage/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 55c031ae7..ace9bf90f 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -128,7 +128,7 @@ def __init__( """ # Build our configuration from a number of sources. - self.config_file, self.config = read_coverage_config( + self._config_file, self.config = read_coverage_config( config_file=config_file, data_file=data_file, cover_pylib=cover_pylib, timid=timid, branch=branch, parallel=bool_or_none(data_suffix), @@ -220,7 +220,7 @@ def _init(self): raise CoverageException( # pragma: only jython "multiprocessing is not supported on this Python" ) - patch_multiprocessing(rcfile=self.config_file) + patch_multiprocessing(rcfile=self._config_file) # Multi-processing uses parallel for the subprocesses, so also use # it for the main process. self.config.parallel = True From b5b29ec13a62548645b3dd9ed527f1f6209d1230 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 13 Mar 2018 10:35:28 -0400 Subject: [PATCH 025/952] Private attributes should be indicated --- coverage/cmdline.py | 2 +- coverage/control.py | 34 +++++++++++++++++----------------- perf/perf_measure.py | 2 +- tests/test_oddball.py | 2 +- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 04cf8e89b..3f8af7d9b 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -657,7 +657,7 @@ def do_debug(self, args): self.coverage.load() data = self.coverage.data print(info_header("data")) - print("path: %s" % self.coverage.data_files.filename) + print("path: %s" % self.coverage._data_files.filename) if data: print("has_arcs: %r" % data.has_arcs()) summary = data.line_counts(fullpath=True) diff --git a/coverage/control.py b/coverage/control.py index ace9bf90f..a644a0deb 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -152,7 +152,7 @@ def __init__( self._warnings = [] # Other instance attributes, set later. - self.data = self.data_files = self.collector = None + self.data = self._data_files = self._collector = None self.plugins = None self._inorout = None self._inorout_class = InOrOut @@ -225,7 +225,7 @@ def _init(self): # it for the main process. self.config.parallel = True - self.collector = Collector( + self._collector = Collector( should_trace=self._should_trace, check_include=self._check_include_omit_etc, timid=self.config.timid, @@ -235,14 +235,14 @@ def _init(self): ) # Early warning if we aren't going to be able to support plugins. - if self.plugins.file_tracers and not self.collector.supports_plugins: + if self.plugins.file_tracers and not self._collector.supports_plugins: self._warn( "Plugin file tracers (%s) aren't supported with %s" % ( ", ".join( plugin._coverage_plugin_name for plugin in self.plugins.file_tracers ), - self.collector.tracer_name(), + self._collector.tracer_name(), ) ) for plugin in self.plugins.file_tracers: @@ -252,7 +252,7 @@ def _init(self): self._inorout = self._inorout_class(warn=self._warn) self._inorout.configure(self.config) self._inorout.plugins = self.plugins - self._inorout.disp_class = self.collector.file_disposition_class + self._inorout.disp_class = self._collector.file_disposition_class # Suffixes are a bit tricky. We want to use the data suffix only when # collecting data, not when combining data. So we save it as @@ -271,7 +271,7 @@ def _init(self): # data file will be written into the directory where the process # started rather than wherever the process eventually chdir'd to. self.data = CoverageData(debug=self.debug) - self.data_files = CoverageDataFiles( + self._data_files = CoverageDataFiles( basename=self.config.data_file, warn=self._warn, debug=self.debug, ) @@ -394,8 +394,8 @@ def use_cache(self, usecache): def load(self): """Load previously-collected coverage data from the data file.""" self._init() - self.collector.reset() - self.data_files.read(self.data) + self._collector.reset() + self._data_files.read(self.data) def start(self): """Start measuring code coverage. @@ -422,13 +422,13 @@ def start(self): if self._warn_preimported_source: self._inorout.warn_already_imported_files() - self.collector.start() + self._collector.start() self._started = True def stop(self): """Stop measuring code coverage.""" if self._started: - self.collector.stop() + self._collector.stop() self._started = False def _atexit(self): @@ -448,9 +448,9 @@ def erase(self): """ self._init() - self.collector.reset() + self._collector.reset() self.data.erase() - self.data_files.erase(parallel=self.config.parallel) + self._data_files.erase(parallel=self.config.parallel) def clear_exclude(self, which='exclude'): """Clear the exclude list.""" @@ -503,7 +503,7 @@ def save(self): """Save the collected coverage data to the data file.""" self._init() self.get_data() - self.data_files.write(self.data, suffix=self.data_suffix) + self._data_files.write(self.data, suffix=self.data_suffix) def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. @@ -538,7 +538,7 @@ def combine(self, data_paths=None, strict=False): for pattern in paths[1:]: aliases.add(pattern, result) - self.data_files.combine_parallel_data( + self._data_files.combine_parallel_data( self.data, aliases=aliases, data_paths=data_paths, strict=strict, ) @@ -554,7 +554,7 @@ def get_data(self): """ self._init() - if self.collector.save_data(self.data): + if self._collector.save_data(self.data): self._post_save_work() return self.data @@ -825,12 +825,12 @@ def plugin_info(plugins): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('tracer', self.collector.tracer_name()), + ('tracer', self._collector.tracer_name()), ('plugins.file_tracers', plugin_info(self.plugins.file_tracers)), ('plugins.configurers', plugin_info(self.plugins.configurers)), ('config_files', self.config.attempted_config_files), ('configs_read', self.config.config_files), - ('data_path', self.data_files.filename), + ('data_path', self._data_files.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), ('implementation', platform.python_implementation()), diff --git a/perf/perf_measure.py b/perf/perf_measure.py index 3b0ae52a5..2125251ad 100644 --- a/perf/perf_measure.py +++ b/perf/perf_measure.py @@ -78,7 +78,7 @@ def _run_scenario(self, file_count, call_count, line_count): finally: # pragma: nested # Stop coverage.py. covered = time.perf_counter() - start - stats = cov.collector.tracers[0].get_stats() + stats = cov._collector.tracers[0].get_stats() if stats: stats = stats.copy() cov.stop() diff --git a/tests/test_oddball.py b/tests/test_oddball.py index aa2f333ce..2a0b03c04 100644 --- a/tests/test_oddball.py +++ b/tests/test_oddball.py @@ -118,7 +118,7 @@ def recur(n): cov = coverage.Coverage() self.start_import_stop(cov, "recur") - pytrace = (cov.collector.tracer_name() == "PyTracer") + pytrace = (cov._collector.tracer_name() == "PyTracer") expected_missing = [3] if pytrace: # pragma: no metacov expected_missing += [9, 10, 11] From 8276c975c368199fc0e513bfc7bc77b29b9bfac1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 13 Mar 2018 11:01:24 -0400 Subject: [PATCH 026/952] Private attributes should be indicated, and same-named things should be separated --- coverage/control.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index a644a0deb..0819dff85 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -141,7 +141,7 @@ def __init__( self._debug_file = None self._auto_load = self._auto_save = auto_data - self._data_suffix = data_suffix + self._data_suffix_specified = data_suffix # Is it ok for no data to be collected? self._warn_no_data = True @@ -156,7 +156,7 @@ def __init__( self.plugins = None self._inorout = None self._inorout_class = InOrOut - self.data_suffix = self.run_suffix = None + self._data_suffix = self._run_suffix = None self._exclude_re = None self.debug = None @@ -256,16 +256,16 @@ def _init(self): # Suffixes are a bit tricky. We want to use the data suffix only when # collecting data, not when combining data. So we save it as - # `self.run_suffix` now, and promote it to `self.data_suffix` if we + # `self._run_suffix` now, and promote it to `self._data_suffix` if we # find that we are collecting data later. - if self._data_suffix or self.config.parallel: - if not isinstance(self._data_suffix, string_class): + if self._data_suffix_specified or self.config.parallel: + if not isinstance(self._data_suffix_specified, string_class): # if data_suffix=True, use .machinename.pid.random - self._data_suffix = True + self._data_suffix_specified = True else: - self._data_suffix = None - self.data_suffix = None - self.run_suffix = self._data_suffix + self._data_suffix_specified = None + self._data_suffix = None + self._run_suffix = self._data_suffix_specified # Create the data file. We do this at construction time so that the # data file will be written into the directory where the process @@ -411,10 +411,10 @@ def start(self): self._init() self._inorout.warn_conflicting_settings() - if self.run_suffix: + if self._run_suffix: # Calling start() means we're running code, so use the run_suffix # as the data_suffix when we eventually save the data. - self.data_suffix = self.run_suffix + self._data_suffix = self._run_suffix if self._auto_load: self.load() @@ -503,7 +503,7 @@ def save(self): """Save the collected coverage data to the data file.""" self._init() self.get_data() - self._data_files.write(self.data, suffix=self.data_suffix) + self._data_files.write(self.data, suffix=self._data_suffix) def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. From 12bb08506e72f11a7c3150af6223066628851041 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 13 Mar 2018 11:06:32 -0400 Subject: [PATCH 027/952] Private attributes should be indicated (plugins) --- coverage/control.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 0819dff85..b8ff69d55 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -153,7 +153,7 @@ def __init__( # Other instance attributes, set later. self.data = self._data_files = self._collector = None - self.plugins = None + self._plugins = None self._inorout = None self._inorout_class = InOrOut self._data_suffix = self._run_suffix = None @@ -204,10 +204,10 @@ def _init(self): set_relative_directory() # Load plugins - self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug) + self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug) # Run configuring plugins. - for plugin in self.plugins.configurers: + for plugin in self._plugins.configurers: # We need an object with set_option and get_option. Either self or # self.config will do. Choosing randomly stops people from doing # other things with those objects, against the public API. Yes, @@ -235,23 +235,23 @@ def _init(self): ) # Early warning if we aren't going to be able to support plugins. - if self.plugins.file_tracers and not self._collector.supports_plugins: + if self._plugins.file_tracers and not self._collector.supports_plugins: self._warn( "Plugin file tracers (%s) aren't supported with %s" % ( ", ".join( plugin._coverage_plugin_name - for plugin in self.plugins.file_tracers + for plugin in self._plugins.file_tracers ), self._collector.tracer_name(), ) ) - for plugin in self.plugins.file_tracers: + for plugin in self._plugins.file_tracers: plugin._coverage_enabled = False # Create the file classifying substructure. self._inorout = self._inorout_class(warn=self._warn) self._inorout.configure(self.config) - self._inorout.plugins = self.plugins + self._inorout.plugins = self._plugins self._inorout.disp_class = self._collector.file_disposition_class # Suffixes are a bit tricky. We want to use the data suffix only when @@ -294,7 +294,7 @@ def _write_startup_debug(self): if self.debug.should('sys'): write_formatted_info(self.debug, "sys", self.sys_info()) - for plugin in self.plugins: + for plugin in self._plugins: header = "sys: " + plugin._coverage_plugin_name info = plugin.sys_info() write_formatted_info(self.debug, header, info) @@ -636,7 +636,7 @@ def _get_file_reporter(self, morf): abs_morf = abs_file(morf) plugin_name = self.data.file_tracer(abs_morf) if plugin_name: - plugin = self.plugins.get(plugin_name) + plugin = self._plugins.get(plugin_name) if plugin: file_reporter = plugin.file_reporter(abs_morf) @@ -826,8 +826,8 @@ def plugin_info(plugins): ('version', covmod.__version__), ('coverage', covmod.__file__), ('tracer', self._collector.tracer_name()), - ('plugins.file_tracers', plugin_info(self.plugins.file_tracers)), - ('plugins.configurers', plugin_info(self.plugins.configurers)), + ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), + ('plugins.configurers', plugin_info(self._plugins.configurers)), ('config_files', self.config.attempted_config_files), ('configs_read', self.config.config_files), ('data_path', self._data_files.filename), From 3dde2f8be2ac964a0b938b5632a24fb972ca750d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 13 Mar 2018 11:26:24 -0400 Subject: [PATCH 028/952] Private attributes should be indicated (debug) --- coverage/control.py | 38 +++++++++++++++++++------------------- coverage/multiproc.py | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index b8ff69d55..b1d95eb3e 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -158,7 +158,7 @@ def __init__( self._inorout_class = InOrOut self._data_suffix = self._run_suffix = None self._exclude_re = None - self.debug = None + self._debug = None # State machine variables: # Have we initialized everything? @@ -196,7 +196,7 @@ def _init(self): self._debug_file = open(debug_file_name, "a") else: self._debug_file = sys.stderr - self.debug = DebugControl(self.config.debug, self._debug_file) + self._debug = DebugControl(self.config.debug, self._debug_file) # _exclude_re is a dict that maps exclusion list names to compiled regexes. self._exclude_re = {} @@ -204,7 +204,7 @@ def _init(self): set_relative_directory() # Load plugins - self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug) + self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug) # Run configuring plugins. for plugin in self._plugins.configurers: @@ -270,9 +270,9 @@ def _init(self): # Create the data file. We do this at construction time so that the # data file will be written into the directory where the process # started rather than wherever the process eventually chdir'd to. - self.data = CoverageData(debug=self.debug) + self.data = CoverageData(debug=self._debug) self._data_files = CoverageDataFiles( - basename=self.config.data_file, warn=self._warn, debug=self.debug, + basename=self.config.data_file, warn=self._warn, debug=self._debug, ) # Set the reporting precision. @@ -286,22 +286,22 @@ def _init(self): def _write_startup_debug(self): """Write out debug info at startup if needed.""" wrote_any = False - with self.debug.without_callers(): - if self.debug.should('config'): + with self._debug.without_callers(): + if self._debug.should('config'): config_info = sorted(self.config.__dict__.items()) - write_formatted_info(self.debug, "config", config_info) + write_formatted_info(self._debug, "config", config_info) wrote_any = True - if self.debug.should('sys'): - write_formatted_info(self.debug, "sys", self.sys_info()) + if self._debug.should('sys'): + write_formatted_info(self._debug, "sys", self.sys_info()) for plugin in self._plugins: header = "sys: " + plugin._coverage_plugin_name info = plugin.sys_info() - write_formatted_info(self.debug, header, info) + write_formatted_info(self._debug, header, info) wrote_any = True if wrote_any: - write_formatted_info(self.debug, "end", ()) + write_formatted_info(self._debug, "end", ()) def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. @@ -310,8 +310,8 @@ def _should_trace(self, filename, frame): """ disp = self._inorout.should_trace(filename, frame) - if self.debug.should('trace'): - self.debug.write(disposition_debug_msg(disp)) + if self._debug.should('trace'): + self._debug.write(disposition_debug_msg(disp)) return disp def _check_include_omit_etc(self, filename, frame): @@ -321,12 +321,12 @@ def _check_include_omit_etc(self, filename, frame): """ reason = self._inorout.check_include_omit_etc(filename, frame) - if self.debug.should('trace'): + if self._debug.should('trace'): if not reason: msg = "Including %r" % (filename,) else: msg = "Not including %r: %s" % (filename, reason) - self.debug.write(msg) + self._debug.write(msg) return not reason @@ -342,7 +342,7 @@ def _warn(self, msg, slug=None): self._warnings.append(msg) if slug: msg = "%s (%s)" % (msg, slug) - if self.debug.should('pid'): + if self._debug.should('pid'): msg = "[%d] %s" % (os.getpid(), msg) sys.stderr.write("Coverage.py warning: %s\n" % msg) @@ -433,8 +433,8 @@ def stop(self): def _atexit(self): """Clean up on process shutdown.""" - if self.debug.should("process"): - self.debug.write("atexit: {0!r}".format(self)) + if self._debug.should("process"): + self._debug.write("atexit: {0!r}".format(self)) if self._started: self.stop() if self._auto_save: diff --git a/coverage/multiproc.py b/coverage/multiproc.py index 986ee9d43..b04076517 100644 --- a/coverage/multiproc.py +++ b/coverage/multiproc.py @@ -35,7 +35,7 @@ def _bootstrap(self): cov = Coverage(data_suffix=True, config_file=rcfile) cov._warn_preimported_source = False cov.start() - debug = cov.debug + debug = cov._debug try: if debug.should("multiproc"): debug.write("Calling multiprocessing bootstrap") From 3e5baac2703bc8d4dbfdd0e2592d6eed7ec07d2b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 19 Mar 2018 06:39:34 -0400 Subject: [PATCH 029/952] Slight refactor of how we look for configuration files --- coverage/config.py | 67 ++++++++++++++++++++++++++++----------------- coverage/control.py | 9 +++--- tests/test_debug.py | 4 +-- 3 files changed, 49 insertions(+), 31 deletions(-) diff --git a/coverage/config.py b/coverage/config.py index 7b8f2bd00..260dafd49 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -175,8 +175,12 @@ class CoverageConfig(object): def __init__(self): """Initialize the configuration attributes to their defaults.""" # Metadata about the config. + # We tried to read these config files. self.attempted_config_files = [] - self.config_files = [] + # We did read these config files, but maybe didn't find any content for us. + self.config_files_read = [] + # The file that gave us our configuration. + self.config_file = None # Defaults for [run] and [report] self._include = None @@ -262,7 +266,7 @@ def from_file(self, filename, our_file): if not files_read: return False - self.config_files.extend(files_read) + self.config_files_read.extend(files_read) any_set = False try: @@ -305,9 +309,14 @@ def from_file(self, filename, our_file): # then it was used. If we're piggybacking on someone else's file, # then it was only used if we found some settings in it. if our_file: - return True + used = True else: - return any_set + used = any_set + + if used: + self.config_file = filename + + return used CONFIG_FILE_OPTIONS = [ # These are *args for _set_attr_from_config_option: @@ -425,6 +434,28 @@ def get_option(self, option_name): raise CoverageException("No such option: %r" % option_name) +def config_files_to_try(config_file): + """What config files should we try to read? + + Returns a list of tuples: + (filename, is_our_file, was_file_specified) + """ + + # Some API users were specifying ".coveragerc" to mean the same as + # True, so make it so. + if config_file == ".coveragerc": + config_file = True + specified_file = (config_file is not True) + if not specified_file: + config_file = ".coveragerc" + files_to_try = [ + (config_file, True, specified_file), + ("setup.cfg", False, False), + ("tox.ini", False, False), + ] + return files_to_try + + def read_coverage_config(config_file, **kwargs): """Read the coverage.py configuration. @@ -435,10 +466,7 @@ def read_coverage_config(config_file, **kwargs): setting values in the configuration. Returns: - config_file, config: - config_file is the value to use for config_file in other - invocations of coverage. - + config: config is a CoverageConfig object read from the appropriate configuration file. @@ -449,25 +477,14 @@ def read_coverage_config(config_file, **kwargs): # 2) from a file: if config_file: - # Some API users were specifying ".coveragerc" to mean the same as - # True, so make it so. - if config_file == ".coveragerc": - config_file = True - specified_file = (config_file is not True) - if not specified_file: - config_file = ".coveragerc" - - for fname, our_file in [(config_file, True), - ("setup.cfg", False), - ("tox.ini", False)]: - config_read = config.from_file(fname, our_file=our_file) - is_config_file = fname == config_file - - if not config_read and is_config_file and specified_file: - raise CoverageException("Couldn't read '%s' as a config file" % fname) + files_to_try = config_files_to_try(config_file) + for fname, our_file, specified_file in files_to_try: + config_read = config.from_file(fname, our_file=our_file) if config_read: break + if our_file and specified_file: + raise CoverageException("Couldn't read '%s' as a config file" % fname) # 3) from environment variables: env_data_file = os.environ.get('COVERAGE_FILE') @@ -486,4 +503,4 @@ def read_coverage_config(config_file, **kwargs): config.html_dir = os.path.expanduser(config.html_dir) config.xml_output = os.path.expanduser(config.xml_output) - return config_file, config + return config diff --git a/coverage/control.py b/coverage/control.py index b1d95eb3e..80012f578 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -128,7 +128,7 @@ def __init__( """ # Build our configuration from a number of sources. - self._config_file, self.config = read_coverage_config( + self.config = read_coverage_config( config_file=config_file, data_file=data_file, cover_pylib=cover_pylib, timid=timid, branch=branch, parallel=bool_or_none(data_suffix), @@ -220,7 +220,7 @@ def _init(self): raise CoverageException( # pragma: only jython "multiprocessing is not supported on this Python" ) - patch_multiprocessing(rcfile=self._config_file) + patch_multiprocessing(rcfile=self.config.config_file) # Multi-processing uses parallel for the subprocesses, so also use # it for the main process. self.config.parallel = True @@ -828,8 +828,9 @@ def plugin_info(plugins): ('tracer', self._collector.tracer_name()), ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), ('plugins.configurers', plugin_info(self._plugins.configurers)), - ('config_files', self.config.attempted_config_files), - ('configs_read', self.config.config_files), + ('configs_attempted', self.config.attempted_config_files), + ('configs_read', self.config.config_files_read), + ('config_file', self.config.config_file), ('data_path', self._data_files.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), diff --git a/tests/test_debug.py b/tests/test_debug.py index 38f31f58c..c81ca24db 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -145,7 +145,7 @@ def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) labels = """ - attempted_config_files branch config_files cover_pylib data_file + attempted_config_files branch config_files_read config_file cover_pylib data_file debug exclude_list extra_css html_dir html_title ignore_errors run_include run_omit parallel partial_always_list partial_list paths precision show_missing source timid xml_output @@ -162,7 +162,7 @@ def test_debug_sys(self): out_lines = self.f1_debug_output(["sys"]) labels = """ - version coverage cover_paths pylib_paths tracer config_files + version coverage cover_paths pylib_paths tracer configs_attempted config_file configs_read data_path python platform implementation executable cwd path environment command_line cover_match pylib_match """.split() From 078ee5fd755caa81128d1115b28292d8a8c68008 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 19 Mar 2018 06:39:47 -0400 Subject: [PATCH 030/952] Pip 9.0.2 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c5194a779..b22713395 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ usedevelop = True deps = # https://requires.io/github/nedbat/coveragepy/requirements/ -rrequirements/pytest.pip - pip==9.0.1 + pip==9.0.2 # setuptools>=36 vendors packages which pollute the coverage output in tests setuptools==35.0.2 mock==2.0.0 From 7bef3d9b7b1914915b3dd5903188efd5bb506882 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 24 Mar 2018 12:22:31 -0400 Subject: [PATCH 031/952] Pip 9.0.3 restores py26 support --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b22713395..abd96029b 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ usedevelop = True deps = # https://requires.io/github/nedbat/coveragepy/requirements/ -rrequirements/pytest.pip - pip==9.0.2 + pip==9.0.3 # setuptools>=36 vendors packages which pollute the coverage output in tests setuptools==35.0.2 mock==2.0.0 From 18bf9c08f85f0b260eb9091b23bed731fc5965b9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 24 Mar 2018 14:05:26 -0400 Subject: [PATCH 032/952] New pylint, now we're pylint-clean --- requirements/dev.pip | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.pip b/requirements/dev.pip index 183d051f4..91a3f46b0 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -13,7 +13,7 @@ greenlet==0.4.13 mock==2.0.0 PyContracts==1.8.2 pyenchant==2.0.0 -pylint==1.8.2 +pylint==1.8.3 unittest-mixins==1.4 # for kitting. From fec75c18a87e8acf8a0d4f978b2ef4e1cea755de Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 Mar 2018 06:45:27 -0400 Subject: [PATCH 033/952] Adapt to the 3.7.0b3 change in -m sys.path[0] behavior --- coverage/execfile.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/coverage/execfile.py b/coverage/execfile.py index 42e0d96ab..a72cb71dd 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -111,7 +111,15 @@ def run_python_module(modulename, args): pathname = os.path.abspath(pathname) args[0] = pathname - run_python_file(pathname, args, package=packagename, modulename=modulename, path0="") + # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It + # used to be an empty string (meaning the current directory). It changed + # to be the actual path to the current directory, so that os.chdir wouldn't + # affect the outcome. + if sys.version_info >= (3, 7, 0, 'beta', 3): + path0 = os.getcwd() + else: + path0 = "" + run_python_file(pathname, args, package=packagename, modulename=modulename, path0=path0) def run_python_file(filename, args, package=None, modulename=None, path0=None): From e4dd841be689c46f6104780d7619bdfe4213a8a8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 Mar 2018 07:22:55 -0400 Subject: [PATCH 034/952] A different way to group builtins, because it's fun, why not? --- tests/modules/process_test/try_execfile.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/modules/process_test/try_execfile.py b/tests/modules/process_test/try_execfile.py index ec7dcbe5f..3068327e0 100644 --- a/tests/modules/process_test/try_execfile.py +++ b/tests/modules/process_test/try_execfile.py @@ -68,10 +68,15 @@ def my_function(a): loader = globals().get('__loader__') fullname = getattr(loader, 'fullname', None) or getattr(loader, 'name', None) -# A more compact grouped-by-first-letter list of builtins. +# A more compact ad-hoc grouped-by-first-letter list of builtins. +CLUMPS = "ABC,DEF,GHI,JKLMN,OPQR,ST,U,VWXYZ_,ab,cd,efg,hij,lmno,pqr,stuvwxyz".split(",") + def word_group(w): - """Clump AB, CD, EF, etc.""" - return chr((ord(w[0]) + 1) & 0xFE) + """Figure out which CLUMP the first letter of w is in.""" + for i, clump in enumerate(CLUMPS): + if w[0] in clump: + return i + return 99 builtin_dir = [" ".join(s) for _, s in itertools.groupby(dir(__builtins__), key=word_group)] From 83d9cf29773b77e397b11c4e47d6c447a4dd9ef1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 3 Apr 2018 15:11:22 -0400 Subject: [PATCH 035/952] Use tox 3.0.0 --- requirements/tox.pip | 2 +- tox.ini | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/requirements/tox.pip b/requirements/tox.pip index b57aa3883..3fb7faf84 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -1,4 +1,4 @@ # The version of tox used by coverage.py -tox==2.9.1 +tox==3.0.0 # Adds env recreation on requirements file changes. tox-battery==0.5 diff --git a/tox.ini b/tox.ini index abd96029b..397cbecb2 100644 --- a/tox.ini +++ b/tox.ini @@ -58,8 +58,12 @@ commands = python igor.py test_with_tracer c {posargs} [testenv:py26] +basepython = python2.6 install_command = python -m pip.__main__ install -U {opts} {packages} +[testenv:py33] +basepython = python3.3 + [testenv:pypy] # The "pypy" environment is for Travis. Probably can make Travis use one of # the other environments... From ef5e1e4ba8200a9ff0656ece40e11ed3be67ac92 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 11 Apr 2018 11:20:10 -0400 Subject: [PATCH 036/952] Update/fix help for --rcfile --- coverage/cmdline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 3f8af7d9b..5baf07a0e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -115,7 +115,10 @@ class Opts(object): ) rcfile = optparse.make_option( '', '--rcfile', action='store', - help="Specify configuration file. Defaults to '.coveragerc'", + help=( + "Specify configuration file. " + "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried." + ), ) source = optparse.make_option( '', '--source', action='store', metavar="SRC1,SRC2,...", From 0089eb35b79b62c14e78a153a3519affcaa70c76 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Wed, 18 Apr 2018 19:21:45 -0700 Subject: [PATCH 037/952] Update all pypi.python.org URLs to pypi.org For details on the new PyPI, see the blog post: https://pythoninsider.blogspot.ca/2018/04/new-pypi-launched-legacy-pypi-shutting.html --- CHANGES.rst | 4 ++-- README.rst | 12 ++++++------ doc/faq.rst | 6 +++--- doc/index.rst | 2 +- doc/install.rst | 4 ++-- doc/plugins.rst | 2 +- doc/trouble.rst | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 126ced944..768b1be6a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -492,7 +492,7 @@ Work from the PyCon 2016 Sprints! .. _issue 478: https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running .. _issue 484: https://bitbucket.org/ned/coveragepy/issues/484/multiprocessing-greenlet-concurrency .. _issue 492: https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of -.. _unittest-mixins: https://pypi.python.org/pypi/unittest-mixins +.. _unittest-mixins: https://pypi.org/project/unittest-mixins/ .. _changes_41: @@ -1811,7 +1811,7 @@ Version 3.2b4 --- 2009-12-01 - On Python 3.x, setuptools has been replaced by `Distribute`_. -.. _Distribute: https://pypi.python.org/pypi/distribute +.. _Distribute: https://pypi.org/project/distribute/ Version 3.2b3 --- 2009-11-23 diff --git a/README.rst b/README.rst index d1c1698d6..a799e9dd0 100644 --- a/README.rst +++ b/README.rst @@ -85,22 +85,22 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. :target: https://requires.io/github/nedbat/coveragepy/requirements/?branch=master :alt: Requirements status .. |kit| image:: https://badge.fury.io/py/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: PyPI status .. |format| image:: https://img.shields.io/pypi/format/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: Kit format .. |downloads| image:: https://img.shields.io/pypi/dw/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: Weekly PyPI downloads .. |versions| image:: https://img.shields.io/pypi/pyversions/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: Python versions supported .. |status| image:: https://img.shields.io/pypi/status/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: Package stability .. |license| image:: https://img.shields.io/pypi/l/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: License .. |codecov| image:: https://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2 :target: https://codecov.io/github/nedbat/coveragepy?branch=master diff --git a/doc/faq.rst b/doc/faq.rst index f6085b610..fb6dc91f3 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -109,15 +109,15 @@ __ https://nedbatchelder.com/blog/200710/flaws_in_coverage_measurement.html - `trialcoverage`_ is a plug-in for Twisted trial. - .. _trialcoverage: https://pypi.python.org/pypi/trialcoverage + .. _trialcoverage: https://pypi.org/project/trialcoverage/ - `pytest-coverage`_ - .. _pytest-coverage: https://pypi.python.org/pypi/pytest-coverage + .. _pytest-coverage: https://pypi.org/project/pytest-coverage/ - `django-coverage`_ for use with Django. - .. _django-coverage: https://pypi.python.org/pypi/django-coverage + .. _django-coverage: https://pypi.org/project/django-coverage/ **Q: Where can I get more help with coverage.py?** diff --git a/doc/index.rst b/doc/index.rst index 20658edd0..2df9554b3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -145,7 +145,7 @@ Getting started is easy: Then visit htmlcov/index.html in your browser, to see a `report like this one`_. -.. _coverage.py page on the Python Package Index: https://pypi.python.org/pypi/coverage +.. _coverage.py page on the Python Package Index: https://pypi.org/project/coverage/ .. _report like this: https://nedbatchelder.com/files/sample_coverage_html/index.html .. _report like this one: https://nedbatchelder.com/files/sample_coverage_html_beta/index.html diff --git a/doc/install.rst b/doc/install.rst index 29bc833e0..4f3717b42 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -29,8 +29,8 @@ Installation .. highlight:: console -.. _coverage_pypi: https://pypi.python.org/pypi/coverage -.. _setuptools: https://pypi.python.org/pypi/setuptools +.. _coverage_pypi: https://pypi.org/project/coverage/ +.. _setuptools: https://pypi.org/project/setuptools/ You can install coverage.py in the usual ways. The simplest way is with pip:: diff --git a/doc/plugins.rst b/doc/plugins.rst index f2bad6d4d..e4967b4ba 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -66,7 +66,7 @@ Some coverage.py plug-ins you might find useful: * `Django template coverage.py plug-in`__: for measuring coverage in Django templates. - .. __: https://pypi.python.org/pypi/django_coverage_plugin + .. __: https://pypi.org/project/django_coverage_plugin/ * `Mako template coverage plug-in`__: for measuring coverage in Mako templates. Doesn't work yet, probably needs some changes in Mako itself. diff --git a/doc/trouble.rst b/doc/trouble.rst index 7534c5cb0..c0003aaf6 100644 --- a/doc/trouble.rst +++ b/doc/trouble.rst @@ -64,7 +64,7 @@ timid=True`` configuration option. DecoratorTools fiddles with the trace function. You will need to use ``--timid``. -.. _DecoratorTools: https://pypi.python.org/pypi/DecoratorTools +.. _DecoratorTools: https://pypi.org/project/DecoratorTools/ .. _TurboGears: http://turbogears.org/ From ee02b2054674f583349c7c136d81690062e05e6f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 19 Apr 2018 06:13:57 -0400 Subject: [PATCH 038/952] It helps me to remember what's going on if I put the next version number here --- CHANGES.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 768b1be6a..ced70268d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -11,12 +11,11 @@ Change history for Coverage.py .. .. .. _changes_781: .. - .. .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- -Unreleased ----------- +Unreleased (might become 4.6) +----------------------------- - A new warning (already-imported) is issued if measurable files have already been imported before coverage.py started measurement. See From 3d3dfc871f102f64737883ea78777a89b6126794 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 19 Apr 2018 07:01:14 -0400 Subject: [PATCH 039/952] COVERAGE_RCFILE can specify the config file location. #650 --- CHANGES.rst | 5 +++++ coverage/config.py | 8 +++++++- tests/test_config.py | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index ced70268d..85db3b2ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,10 +17,15 @@ Change history for Coverage.py Unreleased (might become 4.6) ----------------------------- +- The location of the configuration file can now be specified with a + `COVERAGE_RCFILE` environment variable, as requested in `issue 650`_. + - A new warning (already-imported) is issued if measurable files have already been imported before coverage.py started measurement. See :ref:`cmd_warnings` for more information. +.. _issue 650: https://bitbucket.org/ned/coveragepy/issues/650/allow-setting-configuration-file-location + .. _changes_451: diff --git a/coverage/config.py b/coverage/config.py index 260dafd49..285cb219e 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -447,6 +447,12 @@ def config_files_to_try(config_file): config_file = True specified_file = (config_file is not True) if not specified_file: + # No file was specified. Check COVERAGE_RCFILE. + config_file = os.environ.get('COVERAGE_RCFILE') + if config_file: + specified_file = True + if not specified_file: + # Still no file specified. Default to .coveragerc config_file = ".coveragerc" files_to_try = [ (config_file, True, specified_file), @@ -483,7 +489,7 @@ def read_coverage_config(config_file, **kwargs): config_read = config.from_file(fname, our_file=our_file) if config_read: break - if our_file and specified_file: + if specified_file: raise CoverageException("Couldn't read '%s' as a config file" % fname) # 3) from environment variables: diff --git a/tests/test_config.py b/tests/test_config.py index 0b4d40b64..bbfa46776 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -103,6 +103,21 @@ def test_debug_from_environment(self): cov = coverage.Coverage() self.assertEqual(cov.config.debug, ["dataio", "pids", "callers", "fooey"]) + def test_rcfile_from_environment(self): + self.make_file("here.ini", """\ + [run] + data_file = overthere.dat + """) + self.set_environ("COVERAGE_RCFILE", "here.ini") + cov = coverage.Coverage() + self.assertEqual(cov.config.data_file, "overthere.dat") + + def test_missing_rcfile_from_environment(self): + self.set_environ("COVERAGE_RCFILE", "nowhere.ini") + msg = "Couldn't read 'nowhere.ini' as a config file" + with self.assertRaisesRegex(CoverageException, msg): + coverage.Coverage() + def test_parse_errors(self): # Im-parsable values raise CoverageException, with details. bad_configs_and_msgs = [ From c5e3f3751d1ea54b8e75b0d589f8912c4aaa63c6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 19 Apr 2018 07:08:17 -0400 Subject: [PATCH 040/952] Document COVERAGE_RCFILE --- CHANGES.rst | 2 +- coverage/cmdline.py | 9 +++++---- doc/config.rst | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 85db3b2ed..8028f1fd2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,7 +18,7 @@ Unreleased (might become 4.6) ----------------------------- - The location of the configuration file can now be specified with a - `COVERAGE_RCFILE` environment variable, as requested in `issue 650`_. + ``COVERAGE_RCFILE`` environment variable, as requested in `issue 650`_. - A new warning (already-imported) is issued if measurable files have already been imported before coverage.py started measurement. See diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 5baf07a0e..ea86b4458 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -44,7 +44,7 @@ class Opts(object): ) debug = optparse.make_option( '', '--debug', action='store', metavar="OPTS", - help="Debug options, separated by commas", + help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", ) directory = optparse.make_option( '-d', '--directory', action='store', metavar="DIR", @@ -116,8 +116,9 @@ class Opts(object): rcfile = optparse.make_option( '', '--rcfile', action='store', help=( - "Specify configuration file. " - "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried." + "Specify configuration file. " + "By default '.coveragerc', 'setup.cfg' and 'tox.ini' are tried. " + "[env: COVERAGE_RCFILE]" ), ) source = optparse.make_option( @@ -127,7 +128,7 @@ class Opts(object): timid = optparse.make_option( '', '--timid', action='store_true', help=( - "Use a simpler but slower trace method. Try this if you get " + "Use a simpler but slower trace method. Try this if you get " "seemingly impossible results!" ), ) diff --git a/doc/config.rst b/doc/config.rst index c1fb4b1b7..062aa740a 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -33,8 +33,9 @@ configuration file are tied to your source code and how it should be measured, so it should be stored with your source, and checked into source control, rather than put in your home directory. -A different name for the configuration file can be specified with the -``--rcfile=FILE`` command line option. +A different location for the configuration file can be specified with the +``--rcfile=FILE`` command line option or with the ``COVERAGE_RCFILE`` +environment variable. Coverage.py will read settings from other usual configuration files if no other configuration file is used. It will automatically read from "setup.cfg" or From 37b10a651fb8bbcee2a6a461ed6d4e4ba6b9ee4b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 19 Apr 2018 07:28:48 -0400 Subject: [PATCH 041/952] Now with COVERAGE_RCFILE, no need for multiprocessing to do its own env thing --- coverage/multiproc.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/coverage/multiproc.py b/coverage/multiproc.py index b04076517..93b315523 100644 --- a/coverage/multiproc.py +++ b/coverage/multiproc.py @@ -14,9 +14,6 @@ # monkey-patched. PATCHED_MARKER = "_coverage$patched" -# The environment variable that specifies the rcfile for subprocesses. -COVERAGE_RCFILE_ENV = "_COVERAGE_RCFILE" - if sys.version_info >= (3, 4): OriginalProcess = multiprocessing.process.BaseProcess @@ -31,8 +28,7 @@ class ProcessWithCoverage(OriginalProcess): def _bootstrap(self): """Wrapper around _bootstrap to start coverage.""" from coverage import Coverage # avoid circular import - rcfile = os.environ[COVERAGE_RCFILE_ENV] - cov = Coverage(data_suffix=True, config_file=rcfile) + cov = Coverage(data_suffix=True) cov._warn_preimported_source = False cov.start() debug = cov._debug @@ -81,7 +77,7 @@ def patch_multiprocessing(rcfile): # Set the value in ProcessWithCoverage that will be pickled into the child # process. - os.environ[COVERAGE_RCFILE_ENV] = rcfile + os.environ["COVERAGE_RCFILE"] = rcfile # When spawning processes rather than forking them, we have no state in the # new process. We sneak in there with a Stowaway: we stuff one of our own From 0f06d4633ee68ca1bece0de6f0ff520aa186c590 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 22 Apr 2018 15:02:22 -0400 Subject: [PATCH 042/952] Update some deps --- requirements/dev.pip | 4 ++-- requirements/tox.pip | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/dev.pip b/requirements/dev.pip index 91a3f46b0..36e962d13 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -11,9 +11,9 @@ # for linting. greenlet==0.4.13 mock==2.0.0 -PyContracts==1.8.2 +PyContracts==1.8.3 pyenchant==2.0.0 -pylint==1.8.3 +pylint==1.8.4 unittest-mixins==1.4 # for kitting. diff --git a/requirements/tox.pip b/requirements/tox.pip index 3fb7faf84..a209ac7fd 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -1,4 +1,4 @@ # The version of tox used by coverage.py tox==3.0.0 # Adds env recreation on requirements file changes. -tox-battery==0.5 +tox-battery==0.5.1 diff --git a/tox.ini b/tox.ini index 397cbecb2..c703eb5a8 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ deps = # setuptools>=36 vendors packages which pollute the coverage output in tests setuptools==35.0.2 mock==2.0.0 - PyContracts==1.8.0 + PyContracts==1.8.3 unittest-mixins==1.4 #-e/Users/ned/unittest_mixins py26: unittest2==1.1.0 From 2a35bb182114a4ea47780cd0dc6fa08735e5fbaa Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 5 May 2018 07:26:03 -0400 Subject: [PATCH 043/952] Going to drop support for 2.6 and 3.3, so bump the version --- CHANGES.rst | 2 +- coverage/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8028f1fd2..a2ab0a853 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,7 +14,7 @@ Change history for Coverage.py .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- -Unreleased (might become 4.6) +Unreleased (might become 5.0) ----------------------------- - The location of the configuration file can now be specified with a diff --git a/coverage/version.py b/coverage/version.py index 8a1b39e1f..0eb9210cf 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (4, 6, 0, 'alpha', 0) +version_info = (5, 0, 0, 'alpha', 0) def _make_version(major, minor, micro, releaselevel, serial): From 692987a02adfaa7e05086c1d4c89228d0525bc2e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 5 May 2018 08:49:01 -0400 Subject: [PATCH 044/952] No more support for 2.6 or 3.3 --- .travis.yml | 2 -- Makefile | 4 ++-- README.rst | 2 +- coverage/backunittest.py | 7 +------ setup.py | 4 +--- tests/test_arcs.py | 4 ---- tests/test_parser.py | 4 ---- tests/test_process.py | 8 -------- tox.ini | 24 ++++-------------------- tox_wheels.ini | 4 ++-- 10 files changed, 11 insertions(+), 52 deletions(-) diff --git a/.travis.yml b/.travis.yml index bb5a4b158..8b318f50a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,7 @@ language: python cache: pip sudo: false python: - - '2.6' - '2.7' - - '3.3' - '3.4' - '3.5' - '3.6' diff --git a/Makefile b/Makefile index 6174d7775..a3c375e08 100644 --- a/Makefile +++ b/Makefile @@ -50,10 +50,10 @@ test: TOX_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) smoke: - COVERAGE_NO_PYTRACER=1 tox -e py26,py33 -- $(TOX_SMOKE_ARGS) + COVERAGE_NO_PYTRACER=1 tox -e py27,py34 -- $(TOX_SMOKE_ARGS) pysmoke: - COVERAGE_NO_CTRACER=1 tox -e py26,py33 -- $(TOX_SMOKE_ARGS) + COVERAGE_NO_CTRACER=1 tox -e py27,py34 -- $(TOX_SMOKE_ARGS) metacov: COVERAGE_COVERAGE=yes tox $(ARGS) diff --git a/README.rst b/README.rst index a799e9dd0..9336b9716 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: -* CPython 2.6, 2.7 and 3.3 through 3.7. +* CPython 2.7 and 3.4 through 3.7. * PyPy2 5.10 and PyPy3 5.10. * Jython 2.7.1, though not for reporting. * IronPython 2.7.7, though not for reporting. diff --git a/coverage/backunittest.py b/coverage/backunittest.py index 09574ccb6..1b0848355 100644 --- a/coverage/backunittest.py +++ b/coverage/backunittest.py @@ -3,12 +3,7 @@ """Implementations of unittest features from the future.""" -# Use unittest2 if it's available, otherwise unittest. This gives us -# back-ported features for 2.6. -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest def unittest_has(method): diff --git a/setup.py b/setup.py index 77bc903c0..99874fd49 100644 --- a/setup.py +++ b/setup.py @@ -24,10 +24,8 @@ Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: 2 -Programming Language :: Python :: 2.6 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 -Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 @@ -107,7 +105,7 @@ classifiers=classifier_list, url="https://bitbucket.org/ned/coveragepy", - python_requires=">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4", + python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", ) # A replacement for the build_ext command which raises a single exception diff --git a/tests/test_arcs.py b/tests/test_arcs.py index ef71ea167..4bd804ba3 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -407,8 +407,6 @@ def test_generator_expression(self): ) def test_other_comprehensions(self): - if env.PYVERSION < (2, 7): - self.skipTest("No set or dict comprehensions before 2.7") # Set comprehension: self.check_coverage("""\ o = ((1,2), (3,4)) @@ -431,8 +429,6 @@ def test_other_comprehensions(self): ) def test_multiline_dict_comp(self): - if env.PYVERSION < (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: diff --git a/tests/test_parser.py b/tests/test_parser.py index afb877160..89ef6dc46 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -260,10 +260,6 @@ def func10(): ) def test_missing_arc_descriptions_for_small_callables(self): - # We use 2.7 features here, so just skip this test on 2.6 - if env.PYVERSION < (2, 7): - self.skipTest("No dict or set comps in 2.6") - parser = self.parse_text(u"""\ callables = [ lambda: 2, diff --git a/tests/test_process.py b/tests/test_process.py index fb08562fa..f0b1cde1c 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -727,10 +727,6 @@ def foo(): ) def test_module_name(self): - if sys.version_info < (2, 7): - # Python 2.6 thinks that coverage is a package that can't be - # executed - self.skipTest("-m doesn't work the same < Python 2.7") # https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running out = self.run_command("python -m coverage") self.assertIn("Use 'coverage help' for help", out) @@ -852,10 +848,6 @@ def test_coverage_run_dashm_is_like_python_dashm_off_path(self): 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')") diff --git a/tox.ini b/tox.ini index c703eb5a8..deab5394c 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt [tox] -envlist = py{26,27,33,34,35,36,37}, pypy{2,3}, jython, doc, lint +envlist = py{27,34,35,36,37}, pypy{2,3}, jython, doc, lint skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True} toxworkdir = {env:TOXWORKDIR:.tox} @@ -19,11 +19,9 @@ deps = PyContracts==1.8.3 unittest-mixins==1.4 #-e/Users/ned/unittest_mixins - py26: unittest2==1.1.0 - py{27,33,34,35,36}: gevent==1.2.2 - py26: eventlet==0.21.0 - py{27,33,34,35,36,37}: eventlet==0.22.0 - py{26,27,33,34,35,36,37}: greenlet==0.4.13 + py{27,34,35,36}: gevent==1.2.2 + py{27,34,35,36,37}: eventlet==0.22.0 + py{27,34,35,36,37}: greenlet==0.4.13 # Windows can't update the pip version with pip running, so use Python # to install things. @@ -43,13 +41,6 @@ commands = # Remove the C extension so that we can test the PyTracer python igor.py zip_mods install_egg remove_extension - # When running parallel tests, many processes might all try to import the - # same modules at once. This should be safe, but especially on Python 3.3, - # this caused a number of test failures trying to import usepkgs. To - # prevent the race condition, pre-compile the tests/modules directory. - py33: python -m compileall -q -f tests/modules - py33: python -c "import time; time.sleep(1.1)" - # Test with the PyTracer python igor.py test_with_tracer py {posargs} @@ -57,13 +48,6 @@ commands = python setup.py --quiet build_ext --inplace python igor.py test_with_tracer c {posargs} -[testenv:py26] -basepython = python2.6 -install_command = python -m pip.__main__ install -U {opts} {packages} - -[testenv:py33] -basepython = python3.3 - [testenv:pypy] # The "pypy" environment is for Travis. Probably can make Travis use one of # the other environments... diff --git a/tox_wheels.ini b/tox_wheels.ini index 187159450..adf48bf72 100644 --- a/tox_wheels.ini +++ b/tox_wheels.ini @@ -2,14 +2,14 @@ # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt [tox] -envlist = py{26,27,33,34,35,36,sys} +envlist = py{27,34,35,36,sys} toxworkdir = {toxinidir}/.tox_kits [testenv] deps = -rrequirements/wheel.pip -commands = +commands = python -c "import sys; print(sys.real_prefix)" python setup.py bdist_wheel {posargs} From 41dd416811754f008b992235801fcd6010ca3979 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 5 May 2018 09:51:16 -0400 Subject: [PATCH 045/952] Upgrade most dependencies --- doc/requirements.pip | 6 +++--- requirements/dev.pip | 2 +- requirements/pytest.pip | 5 ++--- requirements/wheel.pip | 5 ++--- tests/test_oddball.py | 9 --------- tox.ini | 4 ++-- 6 files changed, 10 insertions(+), 21 deletions(-) diff --git a/doc/requirements.pip b/doc/requirements.pip index 73467c940..299ca0eb9 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -3,9 +3,9 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ pyenchant==2.0.0 -sphinx==1.6.6 -sphinxcontrib-spelling==4.0.1 -sphinx_rtd_theme==0.2.4 +sphinx==1.7.4 +sphinxcontrib-spelling==4.1.0 +sphinx_rtd_theme==0.3.1 # A version of doc8 with a -q flag. git+https://github.com/nedbat/doc8.git#egg=doc8==0.0 diff --git a/requirements/dev.pip b/requirements/dev.pip index 36e962d13..cf6c40eac 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -18,4 +18,4 @@ unittest-mixins==1.4 # for kitting. requests==2.18.4 -twine==1.9.1 +twine==1.11.0 diff --git a/requirements/pytest.pip b/requirements/pytest.pip index 17ccc0d8d..dfa447fb1 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -3,7 +3,6 @@ # The pytest specifics used by coverage.py -# Keep pytest at 3.2.x until we are done with Python 2.6 and 3.3 -pytest==3.2.5 -pytest-xdist==1.20.1 +pytest==3.5.1 +pytest-xdist==1.22.2 flaky==3.4.0 diff --git a/requirements/wheel.pip b/requirements/wheel.pip index 6dfe70b2d..957416439 100644 --- a/requirements/wheel.pip +++ b/requirements/wheel.pip @@ -1,4 +1,3 @@ # Things needed to make wheels for coverage.py -setuptools==35.0.2 -# We need to stick with 0.29.0 until we drop 2.6 and 3.3 -wheel==0.29.0 +setuptools==39.1.0 +wheel==0.31.0 diff --git a/tests/test_oddball.py b/tests/test_oddball.py index 2a0b03c04..5bd204d91 100644 --- a/tests/test_oddball.py +++ b/tests/test_oddball.py @@ -398,15 +398,6 @@ def doit(calls): class DoctestTest(CoverageTest): """Tests invoked with doctest should measure properly.""" - def setUp(self): - super(DoctestTest, self).setUp() - - # 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('''\ def return_arg_or_void(arg): diff --git a/tox.ini b/tox.ini index deab5394c..4cec188b7 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ usedevelop = True deps = # https://requires.io/github/nedbat/coveragepy/requirements/ -rrequirements/pytest.pip - pip==9.0.3 + pip==10.0.1 # setuptools>=36 vendors packages which pollute the coverage output in tests setuptools==35.0.2 mock==2.0.0 @@ -20,7 +20,7 @@ deps = unittest-mixins==1.4 #-e/Users/ned/unittest_mixins py{27,34,35,36}: gevent==1.2.2 - py{27,34,35,36,37}: eventlet==0.22.0 + py{27,34,35,36,37}: eventlet==0.22.1 py{27,34,35,36,37}: greenlet==0.4.13 # Windows can't update the pip version with pip running, so use Python From 67a0dc73a282dd40bb33874d9ed972f88a31dd67 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 5 May 2018 09:53:17 -0400 Subject: [PATCH 046/952] Remove CI things we aren't using --- circle.yml | 18 ------------------ tox-new.ini | 53 ----------------------------------------------------- 2 files changed, 71 deletions(-) delete mode 100644 circle.yml delete mode 100644 tox-new.ini diff --git a/circle.yml b/circle.yml deleted file mode 100644 index a52959eff..000000000 --- a/circle.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Circle CI configuration for coverage.py. -# https://circleci.com/gh/nedbat/coveragepy - -machine: - python: - version: 2.7.6 - post: - - pyenv global pypy-2.4.0 2.6.8 2.7.9 3.3.3 3.4.2 - -dependencies: - pre: - - pip install -U pip - override: - - pip install -r requirements/tox.pip - -test: - override: - - tox diff --git a/tox-new.ini b/tox-new.ini deleted file mode 100644 index bc5f041a5..000000000 --- a/tox-new.ini +++ /dev/null @@ -1,53 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# An experiment in using tox to install the sdist, and do the pytracer/ctracer -# split. Doesn't yet work because the working tree is in the import path, so -# "import coverage" finds the working tree instead of the sdist-installed code. -# This can be fixed one of two ways: -# -# 1. By changing to a "src" layout, so that "import coverage" won't work in the -# working tree, or -# -# 2. By removing the "__init__.py" from the tests directory, so that nose won't -# add the working tree to the path. This will also mean changing a number of -# import statements in the tests directory. - -[tox] -envlist = py{26,27,33,34,35}-{c,py}tracer, pypy{24,26,3_24}-pytracer -skip_missing_interpreters = True - -[testenv] -commands = - # Create tests/zipmods.zip, install the egg1 egg - python igor.py zip_mods install_egg - - # Remove the C extension so that we can test the PyTracer - pytracer: python igor.py remove_extension - pytracer: python igor.py test_with_tracer py {posargs} - - ctracer: python igor.py test_with_tracer c {posargs} - -deps = - # https://requires.io/github/nedbat/coveragepy/requirements/ - nose==1.3.7 - mock==1.3.0 - PyContracts==1.7.6 - py26: unittest2==1.1.0 - py{26,27}: gevent==1.0.2 - py{26,27}: eventlet==0.17.4 - py{26,27,33,34,35}: greenlet==0.4.9 - -passenv = COVERAGE_* - -[testenv:pypy] -basepython = pypy - -[testenv:pypy24] -basepython = pypy2.4 - -[testenv:pypy26] -basepython = pypy2.6 - -[testenv:pypy3_24] -basepython = pypy3-2.4 From 19845437442e0124437725ed7dd328bcd46e3d67 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 5 May 2018 09:54:17 -0400 Subject: [PATCH 047/952] Newer pytest changed the name of its cache --- .gitignore | 1 + .hgignore | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 74520d511..b952771a1 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ setuptools-*.egg .tox* .noseids .cache +.pytest_cache .hypothesis # Stuff in the test directory. diff --git a/.hgignore b/.hgignore index ce114b130..53576e149 100644 --- a/.hgignore +++ b/.hgignore @@ -28,6 +28,7 @@ setuptools-*.egg .tox* .noseids .cache +.pytest_cache .hypothesis # Stuff in the test directory. From 23b0a977d88934d8fb241911c9b56e88987bdd21 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 5 May 2018 09:56:25 -0400 Subject: [PATCH 048/952] Clean the pytest cache --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index a3c375e08..d9bc1775c 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ clean: -rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz -rm -rf doc/_build doc/_spell doc/sample_html_beta -rm -rf .tox_kits + -rm -rf .cache .pytest_cache sterile: clean -rm -rf .tox* From 01235f7d5aa3a83a40dd1c719d9fe7425c2145bb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 6 May 2018 08:03:58 -0400 Subject: [PATCH 049/952] Big changes should be mentioned in the changelog --- CHANGES.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a2ab0a853..3b9dcbc2f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,8 @@ Change history for Coverage.py Unreleased (might become 5.0) ----------------------------- +- Coverage.py no longer supports Python 2.6 or 3.3. + - The location of the configuration file can now be specified with a ``COVERAGE_RCFILE`` environment variable, as requested in `issue 650`_. From db8b6053fd7004d8ef041f6e6ac40c96ae7d17e3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 6 May 2018 19:32:14 +0200 Subject: [PATCH 050/952] tests: some minor improvements for the farm tests --- tests/test_farm.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_farm.py b/tests/test_farm.py index 1b52bc295..8927cf860 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -104,7 +104,7 @@ def runTest(self): # pragma: not covered raise Exception("runTest isn't used in this class!") def __call__(self): - """Execute the test from the run.py file.""" + """Execute the test from the runpy file.""" if _TEST_NAME_FILE: # pragma: debugging with open(_TEST_NAME_FILE, "w") as f: f.write(self.description.replace("/", "_")) @@ -147,6 +147,8 @@ def noop(*args_unused, **kwargs_unused): def copy(src, dst): """Copy a directory.""" + if os.path.exists(dst): + pytest.fail('%s already exists.' % os.path.join(os.getcwd(), dst)) shutil.copytree(src, dst) @@ -234,19 +236,21 @@ def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scru # ourselves. text_diff = [] for f in diff_files: - with open(os.path.join(dir1, f), READ_MODE) as fobj: + left_file = os.path.join(dir1, f) + right_file = os.path.join(dir2, f) + with open(left_file, READ_MODE) as fobj: left = fobj.read() - with open(os.path.join(dir2, f), READ_MODE) as fobj: + with open(right_file, READ_MODE) as fobj: right = fobj.read() if scrubs: left = scrub(left, scrubs) right = scrub(right, scrubs) if left != right: # pragma: only failure - text_diff.append(f) + text_diff.append('%s != %s' % (left_file, right_file)) left = left.splitlines() right = right.splitlines() print("\n".join(difflib.Differ().compare(left, right))) - assert not text_diff, "Files differ: %s" % text_diff + assert not text_diff, "Files differ: %s" % '\n'.join(text_diff) if not left_extra: assert not left_only, "Files in %s only: %s" % (dir1, left_only) From 69e172c4fd98100ee67802207c232092200520e7 Mon Sep 17 00:00:00 2001 From: "\"David R. MacIver\"" Date: Mon, 14 May 2018 11:32:54 -0400 Subject: [PATCH 051/952] Add collector local cache of abs_file --- coverage/collector.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/coverage/collector.py b/coverage/collector.py index 0c3ca9c2f..d64683787 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -103,6 +103,7 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency self.origin = short_stack() self.concur_id_func = None + self.abs_file_cache = {} # We can handle a few concurrency options here, but only one at a time. these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) @@ -369,6 +370,13 @@ def switch_context(self, new_context): for tracer in self.tracers: tracer.data = data + def cached_abs_file(self, filename): + key = (type(filename), filename) + try: + return self.abs_file_cache[key] + except KeyError: + return self.abs_file_cache.setdefault(key, abs_file(filename)) + def save_data(self, covdata): """Save the collected data to a `CoverageData`. @@ -394,7 +402,7 @@ def abs_file_dict(d): else: raise runtime_err # pylint: disable=raising-bad-type - return dict((abs_file(k), v) for k, v in items) + return dict((self.cached_abs_file(k), v) for k, v in items) if self.branch: covdata.add_arcs(abs_file_dict(self.data)) From 3a564a0704a075e7b63897354fa630c57e3a354c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 14 May 2018 14:27:35 -0400 Subject: [PATCH 052/952] Add a note about the lstat cache --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3b9dcbc2f..99abfd429 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,6 +26,10 @@ Unreleased (might become 5.0) been imported before coverage.py started measurement. See :ref:`cmd_warnings` for more information. +- Running coverage many times for small runs in a single process should be + faster, closing `issue 625`_. Thanks, David MacIver. + +.. _issue 625: https://bitbucket.org/ned/coveragepy/issues/625/lstat-dominates-in-the-case-of-small .. _issue 650: https://bitbucket.org/ned/coveragepy/issues/650/allow-setting-configuration-file-location From 1042a8202952948e1c8bbc59266f88d67c99db14 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 14 May 2018 16:29:40 -0400 Subject: [PATCH 053/952] Use check-manifest to check the manifest --- MANIFEST.in | 1 + requirements/dev.pip | 1 + tox.ini | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 462f24ff7..447f8ea93 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -22,6 +22,7 @@ include pylintrc include setup.py include tox.ini include tox_wheels.ini +include .editorconfig recursive-include ci *.* exclude ci/appveyor.token diff --git a/requirements/dev.pip b/requirements/dev.pip index cf6c40eac..4083ca54a 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -15,6 +15,7 @@ PyContracts==1.8.3 pyenchant==2.0.0 pylint==1.8.4 unittest-mixins==1.4 +check-manifest==0.37 # for kitting. requests==2.18.4 diff --git a/tox.ini b/tox.ini index 4cec188b7..c471f71d2 100644 --- a/tox.ini +++ b/tox.ini @@ -81,6 +81,7 @@ setenv = LINTABLE = coverage tests igor.py setup.py __main__.py commands = - python -m pylint --notes= {env:LINTABLE} python -m tabnanny {env:LINTABLE} python igor.py check_eol + check-manifest --ignore 'lab*,perf*,doc/sample_html*,.treerc' + python -m pylint --notes= {env:LINTABLE} From 012f6ae37683daf6808b24d781fcd96630a86b50 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 24 May 2018 07:39:59 -0400 Subject: [PATCH 054/952] readme_renderer checks that the README.rst is valid --- requirements/dev.pip | 1 + tox.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements/dev.pip b/requirements/dev.pip index 4083ca54a..fa774d043 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -16,6 +16,7 @@ pyenchant==2.0.0 pylint==1.8.4 unittest-mixins==1.4 check-manifest==0.37 +readme_renderer==20.0 # for kitting. requests==2.18.4 diff --git a/tox.ini b/tox.ini index c471f71d2..bca3d585b 100644 --- a/tox.ini +++ b/tox.ini @@ -84,4 +84,5 @@ commands = python -m tabnanny {env:LINTABLE} python igor.py check_eol check-manifest --ignore 'lab*,perf*,doc/sample_html*,.treerc' + python setup.py check -r -s python -m pylint --notes= {env:LINTABLE} From 644391c631e357711a8583db49b2243625f0ef00 Mon Sep 17 00:00:00 2001 From: pankajp Date: Thu, 24 May 2018 12:40:37 +0000 Subject: [PATCH 055/952] Created new branch pankajp/faster-html-report-ui --HG-- branch : pankajp/faster-html-report-ui From b27ab43a7e00277e919d2aee8df55c8c2bceb0ab Mon Sep 17 00:00:00 2001 From: pankajp Date: Thu, 24 May 2018 12:43:32 +0000 Subject: [PATCH 056/952] Prevent expensive relayout in loop in html report js Faster html report loading in browser by avoiding unneeded expensive layouts in the loop to generate markers. --HG-- branch : pankajp/faster-html-report-ui --- coverage/htmlfiles/coverage_html.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index f6f5de207..b7fbd9c99 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -555,11 +555,14 @@ coverage.resize_scroll_markers = function () { var previous_line = -99, last_mark, - last_top; + last_top, + offsets = {}; + // Calculate line offsets outside loop to prevent relayouts + c.missed_lines.each(function(){offsets[this.id] = $(this).offset().top}); c.missed_lines.each(function () { - var line_top = Math.round($(this).offset().top * marker_scale), - id_name = $(this).attr('id'), + var id_name = $(this).attr('id'), + line_top = Math.round(offsets[id_name] * marker_scale), line_number = parseInt(id_name.substring(1, id_name.length)); if (line_number === previous_line + 1) { From fe5997ec194948afde3bd211d2ee6db329725abc Mon Sep 17 00:00:00 2001 From: pankajp Date: Thu, 24 May 2018 12:45:36 +0000 Subject: [PATCH 057/952] Html report marker div style in its own css layer. This gives faster scrolling in Chrome, comparable to firefox --HG-- branch : pankajp/faster-html-report-ui --- coverage/htmlfiles/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index 86b820914..12e90645e 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -365,6 +365,7 @@ td.text { height: 100%; background: white; border-left: 1px solid #eee; + will-change: transform; /* for faster scrolling of fixed element in Chrome */ } #scroll_marker .marker { From 96bcb2d1c48f8717e6a0290b0b5e60e0bc2a1d97 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 27 May 2018 08:43:05 -0400 Subject: [PATCH 058/952] PyPy on travis? --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8b318f50a..06e7d3ba5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,8 @@ python: - '3.4' - '3.5' - '3.6' - - 'pypy' + - 'pypy2.7' + - 'pypy3.5' env: matrix: From 8a6ddab29ea01d50e48b2d3dbcc91de9960d77d2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 27 May 2018 09:12:55 -0400 Subject: [PATCH 059/952] Maybe pypy2 works? --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 06e7d3ba5..de8488f7b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: - '3.4' - '3.5' - '3.6' - - 'pypy2.7' + - 'pypy2' - 'pypy3.5' env: From c1b54c2433e4f23c61354303d9e3698704f5a3f7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 27 May 2018 09:44:08 -0400 Subject: [PATCH 060/952] Back to pypy --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index de8488f7b..df238e57f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: - '3.4' - '3.5' - '3.6' - - 'pypy2' + - 'pypy' - 'pypy3.5' env: From 863d7602bf3ffff089dc9e9ee8b89ad404f0fea4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 28 May 2018 07:15:03 -0400 Subject: [PATCH 061/952] Clean up --- CHANGES.rst | 2 ++ CONTRIBUTORS.txt | 1 + coverage/htmlfiles/coverage_html.js | 4 +++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 99abfd429..d75322f6c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,6 +29,8 @@ Unreleased (might become 5.0) - Running coverage many times for small runs in a single process should be faster, closing `issue 625`_. Thanks, David MacIver. +- Large HTML report pages load faster. Thanks, pankajp. + .. _issue 625: https://bitbucket.org/ned/coveragepy/issues/625/lstat-dominates-in-the-case-of-small .. _issue 650: https://bitbucket.org/ned/coveragepy/issues/650/allow-setting-configuration-file-location diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index d3112a144..de4ea7d95 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -81,6 +81,7 @@ Nathan Land Noel O'Boyle Olivier Grisel Ori Avtalion +pankajp Pablo Carballo Patrick Mezard Peter Baughman diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index b7fbd9c99..c1a41192f 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -559,7 +559,9 @@ coverage.resize_scroll_markers = function () { offsets = {}; // Calculate line offsets outside loop to prevent relayouts - c.missed_lines.each(function(){offsets[this.id] = $(this).offset().top}); + c.missed_lines.each(function() { + offsets[this.id] = $(this).offset().top; + }); c.missed_lines.each(function () { var id_name = $(this).attr('id'), line_top = Math.round(offsets[id_name] * marker_scale), From 947071b4a9fc3ae90039e3e7b2a1e2ca483f7f50 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 28 May 2018 17:15:53 -0400 Subject: [PATCH 062/952] Updates for 5.0a1 --- CHANGES.rst | 6 ++++-- README.rst | 6 ++++-- coverage/version.py | 2 +- doc/conf.py | 4 ++-- doc/index.rst | 10 +++++----- doc/python-coverage.1.txt | 6 +++++- howto.txt | 1 + 7 files changed, 22 insertions(+), 13 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index d75322f6c..6ceba6090 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,8 +14,10 @@ Change history for Coverage.py .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- -Unreleased (might become 5.0) ------------------------------ +.. _changes_50a1: + +Version 5.0a1 --- 2018-05-28 +---------------------------- - Coverage.py no longer supports Python 2.6 or 3.3. diff --git a/README.rst b/README.rst index 9336b9716..007155925 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: * CPython 2.7 and 3.4 through 3.7. -* PyPy2 5.10 and PyPy3 5.10. +* PyPy2 6.0 and PyPy3 6.0. * Jython 2.7.1, though not for reporting. * IronPython 2.7.7, though not for reporting. @@ -32,7 +32,9 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on .. _GitHub: https://github.com/nedbat/coveragepy -**New in 4.5:** Configurator plug-ins. +**New in 5.0:** Dropped support for Python 2.6 and 3.3. + +New in 4.5: Configurator plug-ins. New in 4.4: Suppressable warnings, continuous coverage measurement. diff --git a/coverage/version.py b/coverage/version.py index 0eb9210cf..db8f9ade3 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 0, 0, 'alpha', 0) +version_info = (5, 0, 0, 'alpha', 1) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/doc/conf.py b/doc/conf.py index 08c88537f..503387b52 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -56,9 +56,9 @@ # built documents. # # The short X.Y version. -version = '4.5' # CHANGEME +version = '5.0' # CHANGEME # The full version, including alpha/beta/rc tags. -release = '4.5.1' # CHANGEME +release = '5.0a1' # CHANGEME # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/index.rst b/doc/index.rst index 2df9554b3..2f99d5934 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -77,21 +77,21 @@ not. .. ifconfig:: prerelease - The latest version is coverage.py 4.4b1, released April 4th 2017. It is + The latest version is coverage.py 5.0a1, released May 28th 2018. It is supported on: - * Python versions 2.6, 2.7, 3.3, 3.4, 3.5, and 3.6. + * Python versions 2.7, 3.4, 3.5, 3.6, and 3.7. - * PyPy2 5.6 and PyPy3 5.5. + * PyPy2 6.0 and PyPy3 6.0. * Jython 2.7.1, though only for running code, not reporting. * IronPython 2.7.7, though only for running code, not reporting. **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 4.3.4, `described here`_. + apply.** The latest stable version is coverage.py 4.5.1, `described here`_. -.. _described here: https://nedbatchelder.com/code/coverage +.. _described here: http://coverage.readthedocs.io/ Quick start diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt index 94402b840..a415f0805 100644 --- a/doc/python-coverage.1.txt +++ b/doc/python-coverage.1.txt @@ -8,7 +8,7 @@ measure code coverage of Python program execution :Author: Ned Batchelder :Author: |author| -:Date: 2015-09-20 +:Date: 2018-05-28 :Copyright: Apache 2.0 license, attribution and disclaimer required. :Manual section: 1 :Manual group: Coverage.py @@ -219,6 +219,10 @@ COVERAGE_FILE Path to the file where coverage measurements are collected to and reported from. Default: ``.coverage`` in the current working directory. +COVERAGE_RCFILE + + Path to the configuration file, often named ``.coveragerc``. + HISTORY ======= diff --git a/howto.txt b/howto.txt index 2707be51e..25425ec27 100644 --- a/howto.txt +++ b/howto.txt @@ -10,6 +10,7 @@ - Update CHANGES.rst, including release date. - Update README.rst - "New in x.y:" + - Python versions supported - Update docs - Version, date and python versions in doc/index.rst - Version and copyright date in doc/conf.py From c57ff835530d72751590518473f4e5dd9ab7c880 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 29 May 2018 15:56:47 -0400 Subject: [PATCH 063/952] Not sure why pip is failing on appveyor. Try this. --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 6ff592afd..fe99f6300 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,9 +99,9 @@ install: # Upgrade to the latest version of pip to avoid it displaying warnings # about it being out of date. - - "pip install --disable-pip-version-check --user --upgrade pip" + - "python -m pip install --disable-pip-version-check --user --upgrade pip" # And upgrade virtualenv to get the latest pip inside .tox virtualenvs. - - "pip install --disable-pip-version-check --user --upgrade virtualenv" + - "python -m pip install --disable-pip-version-check --user --upgrade virtualenv" # Install requirements. - "%CMD_IN_ENV% pip install -r requirements/ci.pip" From cdbd5e68ac9718d38709e6b6a388fb35edab532f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 May 2018 10:50:38 -0400 Subject: [PATCH 064/952] A few cleanups --- MANIFEST.in | 1 - coverage/collector.py | 1 + tests/test_farm.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 447f8ea93..275f7526d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -14,7 +14,6 @@ include TODO.txt include __main__.py include .travis.yml include appveyor.yml -include circle.yml include howto.txt include igor.py include metacov.ini diff --git a/coverage/collector.py b/coverage/collector.py index d64683787..bc385fc27 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -371,6 +371,7 @@ def switch_context(self, new_context): tracer.data = data def cached_abs_file(self, filename): + """A locally cached version of `abs_file`.""" key = (type(filename), filename) try: return self.abs_file_cache[key] diff --git a/tests/test_farm.py b/tests/test_farm.py index 1b52bc295..836812601 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -103,7 +103,7 @@ 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): + def __call__(self): # pylint: disable=arguments-differ """Execute the test from the run.py file.""" if _TEST_NAME_FILE: # pragma: debugging with open(_TEST_NAME_FILE, "w") as f: From b4bbc37f3748a447c2f85218f8092a94fc8e13fe Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 1 Jun 2018 07:13:55 -0400 Subject: [PATCH 065/952] Update requirements --- doc/requirements.pip | 2 +- requirements/dev.pip | 4 ++-- requirements/pytest.pip | 2 +- requirements/wheel.pip | 4 ++-- tox.ini | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/requirements.pip b/doc/requirements.pip index 299ca0eb9..dbd6c8fab 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -3,7 +3,7 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ pyenchant==2.0.0 -sphinx==1.7.4 +sphinx==1.7.5 sphinxcontrib-spelling==4.1.0 sphinx_rtd_theme==0.3.1 diff --git a/requirements/dev.pip b/requirements/dev.pip index fa774d043..98cac62eb 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -13,10 +13,10 @@ greenlet==0.4.13 mock==2.0.0 PyContracts==1.8.3 pyenchant==2.0.0 -pylint==1.8.4 +pylint==1.9.1 unittest-mixins==1.4 check-manifest==0.37 -readme_renderer==20.0 +readme_renderer==21.0 # for kitting. requests==2.18.4 diff --git a/requirements/pytest.pip b/requirements/pytest.pip index dfa447fb1..d90c16b41 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -3,6 +3,6 @@ # The pytest specifics used by coverage.py -pytest==3.5.1 +pytest==3.6.0 pytest-xdist==1.22.2 flaky==3.4.0 diff --git a/requirements/wheel.pip b/requirements/wheel.pip index 957416439..9c6bf0cae 100644 --- a/requirements/wheel.pip +++ b/requirements/wheel.pip @@ -1,3 +1,3 @@ # Things needed to make wheels for coverage.py -setuptools==39.1.0 -wheel==0.31.0 +setuptools==39.2.0 +wheel==0.31.1 diff --git a/tox.ini b/tox.ini index bca3d585b..bb33d65c3 100644 --- a/tox.ini +++ b/tox.ini @@ -19,8 +19,8 @@ deps = PyContracts==1.8.3 unittest-mixins==1.4 #-e/Users/ned/unittest_mixins - py{27,34,35,36}: gevent==1.2.2 - py{27,34,35,36,37}: eventlet==0.22.1 + py{27,34,35,36}: gevent==1.3.2 + py{27,34,35,36,37}: eventlet==0.23.0 py{27,34,35,36,37}: greenlet==0.4.13 # Windows can't update the pip version with pip running, so use Python From 3be75d375c82003ea7ca9d3bc9502a46ff3a5822 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jun 2018 11:46:37 -0400 Subject: [PATCH 066/952] Undo some 3.7b4 ast tweaks --- tests/test_coverage.py | 13 ++----------- tests/test_parser.py | 6 +----- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 45abb2be0..c8ac55dfc 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -582,12 +582,7 @@ def test_module_docstring(self): """, [2, 3] ) - if env.PYVERSION < (3, 7): - # Before 3.7, module docstrings were included in the lnotab table, - # unless they were the first line in the file? - lines = [2, 3, 4] - else: - lines = [3, 4] + lines = [2, 3, 4] self.check_coverage("""\ # Start with a comment, because it changes the behavior(!?) '''I am a module docstring.''' @@ -1147,11 +1142,7 @@ def foo( [1,10,12,13], "") def test_class_def(self): - if env.PYVERSION < (3, 7): - arcz="-22 2D DE E-2 23 36 6A A-2 -68 8-6 -AB B-A" - else: - # Python 3.7 no longer includes class docstrings in the lnotab table. - arcz="-22 2D DE E-2 26 6A A-2 -68 8-6 -AB B-A" + arcz="-22 2D DE E-2 23 36 6A A-2 -68 8-6 -AB B-A" self.check_coverage("""\ # A comment. class theClass: diff --git a/tests/test_parser.py b/tests/test_parser.py index 89ef6dc46..184825bfe 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -165,11 +165,7 @@ def meth(self): def func(x=25): return 26 """) - if env.PYVERSION < (3, 7): - raw_statements = set([3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26]) - else: - # Python 3.7 no longer includes class docstrings in the lnotab table. - raw_statements = set([3, 4, 5, 6, 8, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26]) + raw_statements = set([3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26]) self.assertEqual(parser.raw_statements, raw_statements) self.assertEqual(parser.statements, set([8])) From ed7ae4982184c3cf33c5b655e74cbd4bcbe07e63 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jun 2018 05:52:52 -0400 Subject: [PATCH 067/952] Pankaj Pandey did the recent HTML speed-ups --- CONTRIBUTORS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index de4ea7d95..28f2fee79 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -81,7 +81,7 @@ Nathan Land Noel O'Boyle Olivier Grisel Ori Avtalion -pankajp +Pankaj Pandey Pablo Carballo Patrick Mezard Peter Baughman From 3151c58555a068f9752c7fb3e0891ff18368d5f9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jun 2018 06:11:39 -0400 Subject: [PATCH 068/952] Keep gevent at a version that works for now --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index bb33d65c3..8c35391f7 100644 --- a/tox.ini +++ b/tox.ini @@ -19,7 +19,8 @@ deps = PyContracts==1.8.3 unittest-mixins==1.4 #-e/Users/ned/unittest_mixins - py{27,34,35,36}: gevent==1.3.2 + # gevent 1.3 causes a failure: https://bitbucket.org/ned/coveragepy/issues/663/gevent-132-on-windows-fails + py{27,34,35,36}: gevent==1.2.2 py{27,34,35,36,37}: eventlet==0.23.0 py{27,34,35,36,37}: greenlet==0.4.13 From a11a5505e082e00fb3896a25b8420289e7efe05b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jun 2018 06:12:59 -0400 Subject: [PATCH 069/952] Didn't ship it in May --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6ceba6090..e5ccbdef7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -16,7 +16,7 @@ Change history for Coverage.py .. _changes_50a1: -Version 5.0a1 --- 2018-05-28 +Version 5.0a1 --- 2018-06-05 ---------------------------- - Coverage.py no longer supports Python 2.6 or 3.3. From 5ad0592e32c7b4f94bbca54e7e5906b95ef89736 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jun 2018 09:47:47 -0400 Subject: [PATCH 070/952] One other 5.0a1 date to fix --- doc/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/index.rst b/doc/index.rst index 2f99d5934..071befb82 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -77,7 +77,7 @@ not. .. ifconfig:: prerelease - The latest version is coverage.py 5.0a1, released May 28th 2018. It is + The latest version is coverage.py 5.0a1, released June 5th 2018. It is supported on: * Python versions 2.7, 3.4, 3.5, 3.6, and 3.7. From 8a3158657ce9677ebf1547d5a0b42ae51a8c2baa Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jun 2018 13:28:21 -0400 Subject: [PATCH 071/952] Coverage 5.0a1 --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index 411bf391a..48574a0fe 100644 --- a/.hgtags +++ b/.hgtags @@ -66,3 +66,4 @@ ed196840b79136f17ab493699ec83dcf7dbfe973 coverage-4.4.1 b65ae46a6504b8d577e967bd3fdcfcaceec95528 coverage-4.4.2 102b2250a123537e640cd014f5df281822e79cec coverage-4.5 dda8b38e71d0bd2bde79d644f7265e1c02ce02f9 coverage-4.5.1 +865c64d99227b40e9f92586f63f2b61ebbe12d48 coverage-5.0a1 From d0f6422902026b9cac6d3ae6c3c397dfb906fee2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jun 2018 13:29:54 -0400 Subject: [PATCH 072/952] Bump to 5.0a2 --- CHANGES.rst | 4 ++++ coverage/version.py | 2 +- howto.txt | 3 ++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e5ccbdef7..f0fccc764 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,10 @@ Change history for Coverage.py .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- +Unreleased +---------- + + .. _changes_50a1: Version 5.0a1 --- 2018-06-05 diff --git a/coverage/version.py b/coverage/version.py index db8f9ade3..0e6b0f9cd 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 0, 0, 'alpha', 1) +version_info = (5, 0, 0, 'alpha', 2) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/howto.txt b/howto.txt index 25425ec27..b23757dcb 100644 --- a/howto.txt +++ b/howto.txt @@ -58,7 +58,8 @@ - Update PyPi: - upload kits: - $ make kit_upload - - Visit https://pypi.python.org/pypi?:action=pkg_edit&name=coverage : + - DON'T NEED TO DO THIS ANY MORE? + - Visit https://pypi.python.org/pypi?:action=pkg_edit&name=coverage : - show/hide the proper versions. - Tag the tree - hg tag -m "Coverage 3.0.1" coverage-3.0.1 From 1b94cffbbcf809bae8ecc338b498622be8d3a163 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 11 Jun 2018 07:30:46 -0400 Subject: [PATCH 073/952] Fix a name --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index f0fccc764..5c8b4eecf 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,7 +35,7 @@ Version 5.0a1 --- 2018-06-05 - Running coverage many times for small runs in a single process should be faster, closing `issue 625`_. Thanks, David MacIver. -- Large HTML report pages load faster. Thanks, pankajp. +- Large HTML report pages load faster. Thanks, Pankaj Pandey. .. _issue 625: https://bitbucket.org/ned/coveragepy/issues/625/lstat-dominates-in-the-case-of-small .. _issue 650: https://bitbucket.org/ned/coveragepy/issues/650/allow-setting-configuration-file-location From 637851fcc659b92ad493c3edc87f9f2fc9b1c313 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 13 Jun 2018 07:22:44 -0400 Subject: [PATCH 074/952] Adapt to a recent 3.7 change in how functions with only docstrings get line-numbered --- tests/test_parser.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 184825bfe..034e9aa7d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -197,8 +197,14 @@ def bar(self): pass """) self.assertEqual(parser.statements, set([1, 2, 4, 8, 10])) - self.assertEqual(parser.arcs(), set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8"))) - self.assertEqual(parser.exit_counts(), {1: 1, 2: 1, 4: 1, 8: 1, 10: 1}) + expected_arcs = set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8")) + expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1} + if env.PYVERSION >= (3, 7, 0, 'beta', 5): + # 3.7 changed how functions with only docstrings were numbered. + expected_arcs.update(set(self.arcz_to_arcs("-46 6-4"))) + expected_exits.update({6: 1}) + self.assertEqual(parser.arcs(), expected_arcs) + self.assertEqual(parser.exit_counts(), expected_exits) class ParserMissingArcDescriptionTest(CoverageTest): From 1bbfe7e938d0250a1bb777492f617e95349abc5d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 13 Jun 2018 07:57:18 -0400 Subject: [PATCH 075/952] Make version checking more uniform --- coverage/config.py | 4 ++-- coverage/execfile.py | 9 +++++---- coverage/multiproc.py | 6 +++--- tests/test_farm.py | 2 +- tests/test_parser.py | 2 +- tests/test_process.py | 4 ++-- 6 files changed, 14 insertions(+), 13 deletions(-) diff --git a/coverage/config.py b/coverage/config.py index 285cb219e..effa382f6 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -6,8 +6,8 @@ import collections import os import re -import sys +from coverage import env from coverage.backward import configparser, iitems, string_class from coverage.misc import contract, CoverageException, isolate_module @@ -33,7 +33,7 @@ def __init__(self, our_file): def read(self, filenames): """Read a file name as UTF-8 configuration data.""" kwargs = {} - if sys.version_info >= (3, 2): + if env.PYVERSION >= (3, 2): kwargs['encoding'] = "utf-8" return configparser.RawConfigParser.read(self, filenames, **kwargs) diff --git a/coverage/execfile.py b/coverage/execfile.py index a72cb71dd..68417f8ac 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -9,6 +9,7 @@ import sys import types +from coverage import env from coverage.backward import BUILTINS from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module @@ -115,7 +116,7 @@ def run_python_module(modulename, args): # used to be an empty string (meaning the current directory). It changed # to be the actual path to the current directory, so that os.chdir wouldn't # affect the outcome. - if sys.version_info >= (3, 7, 0, 'beta', 3): + if env.PYVERSION >= (3, 7, 0, 'beta', 3): path0 = os.getcwd() else: path0 = "" @@ -136,7 +137,7 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None): function will decide on a value. """ - if modulename is None and sys.version_info >= (3, 3): + if modulename is None and env.PYVERSION >= (3, 3): modulename = '__main__' # Create a module to serve as __main__ @@ -263,7 +264,7 @@ def make_code_from_pyc(filename): raise NoCode("Bad magic number in .pyc file") date_based = True - if sys.version_info >= (3, 7, 0, 'alpha', 4): + if env.PYVERSION >= (3, 7, 0, 'alpha', 4): flags = struct.unpack('= (3, 3): + if env.PYVERSION >= (3, 3): # 3.3 added another long to the header (size), skip it. fpyc.read(4) diff --git a/coverage/multiproc.py b/coverage/multiproc.py index 93b315523..bbc88fbe9 100644 --- a/coverage/multiproc.py +++ b/coverage/multiproc.py @@ -6,8 +6,8 @@ import multiprocessing import multiprocessing.process import os -import sys +from coverage import env from coverage.misc import contract # An attribute that will be set on the module to indicate that it has been @@ -15,7 +15,7 @@ PATCHED_MARKER = "_coverage$patched" -if sys.version_info >= (3, 4): +if env.PYVERSION >= (3, 4): OriginalProcess = multiprocessing.process.BaseProcess else: OriginalProcess = multiprocessing.Process @@ -70,7 +70,7 @@ def patch_multiprocessing(rcfile): if hasattr(multiprocessing, PATCHED_MARKER): return - if sys.version_info >= (3, 4): + if env.PYVERSION >= (3, 4): OriginalProcess._bootstrap = ProcessWithCoverage._bootstrap else: multiprocessing.Process = ProcessWithCoverage diff --git a/tests/test_farm.py b/tests/test_farm.py index 836812601..4fc0ea5a3 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -36,7 +36,7 @@ def test_farm(filename): # "rU" was deprecated in 3.4 -READ_MODE = "rU" if sys.version_info < (3, 4) else "r" +READ_MODE = "rU" if env.PYVERSION < (3, 4) else "r" class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): diff --git a/tests/test_parser.py b/tests/test_parser.py index 034e9aa7d..169319f54 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -200,7 +200,7 @@ def bar(self): expected_arcs = set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8")) expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1} if env.PYVERSION >= (3, 7, 0, 'beta', 5): - # 3.7 changed how functions with only docstrings were numbered. + # 3.7 changed how functions with only docstrings are numbered. expected_arcs.update(set(self.arcz_to_arcs("-46 6-4"))) expected_exits.update({6: 1}) self.assertEqual(parser.arcs(), expected_arcs) diff --git a/tests/test_process.py b/tests/test_process.py index f0b1cde1c..7e55c77c2 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -659,7 +659,7 @@ def test_fullcoverage(self): # pragma: no metacov self.assertGreater(data.line_counts()['os.py'], 50) def test_lang_c(self): - if env.PY3 and sys.version_info < (3, 4): + if env.PY3 and env.PYVERSION < (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") if env.JYTHON: @@ -768,7 +768,7 @@ def test_coverage_run_dashm_is_like_python_dashm(self): self.assert_tryexecfile_output(out_cov, out_py) def test_coverage_run_dir_is_like_python_dir(self): - if sys.version_info == (3, 5, 4, 'final', 0): # pragma: obscure + if env.PYVERSION == (3, 5, 4, 'final', 0): # pragma: obscure self.skipTest("3.5.4 broke this: https://bugs.python.org/issue32551") with open(TRY_EXECFILE) as f: self.make_file("with_main/__main__.py", f.read()) From a355099628a28b21f2e61600a84011cb1be32756 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 19 Jun 2018 07:27:00 -0400 Subject: [PATCH 076/952] Drop support for EOL 2.5, 2.6 and 3.3 --- __main__.py | 11 ++--------- doc/contributing.rst | 2 +- tests/test_process.py | 3 --- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/__main__.py b/__main__.py index c998e1da7..f1f2b4f67 100644 --- a/__main__.py +++ b/__main__.py @@ -8,12 +8,5 @@ PKG = 'coverage' -try: - run_globals = runpy.run_module(PKG, run_name='__main__', alter_sys=True) - executed = os.path.splitext(os.path.basename(run_globals['__file__']))[0] - if executed != '__main__': # For Python 2.5 compatibility - raise ImportError( - 'Incorrectly executed %s instead of __main__' % executed - ) -except ImportError: # For Python 2.6 compatibility - runpy.run_module('%s.__main__' % PKG, run_name='__main__', alter_sys=True) +run_globals = runpy.run_module(PKG, run_name='__main__', alter_sys=True) +executed = os.path.splitext(os.path.basename(run_globals['__file__']))[0] diff --git a/doc/contributing.rst b/doc/contributing.rst index 656d25196..896b39c77 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -117,7 +117,7 @@ the second uses the C implementation. To limit tox to just a few versions of Python, use the ``-e`` switch:: - $ tox -e py27,py33 + $ tox -e py27,py37 To run just a few tests, you can use `pytest test selectors`_:: diff --git a/tests/test_process.py b/tests/test_process.py index 7e55c77c2..68262a57d 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -659,9 +659,6 @@ def test_fullcoverage(self): # pragma: no metacov self.assertGreater(data.line_counts()['os.py'], 50) def test_lang_c(self): - if env.PY3 and env.PYVERSION < (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") if env.JYTHON: # Jython as of 2.7.1rc3 won't compile a filename that isn't utf8. self.skipTest("Jython can't handle this test") From 0df8e82c77b677a1cda984aa56e3666948f72e9b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 19 Jun 2018 08:14:03 -0400 Subject: [PATCH 077/952] I like to give credit where credit is due --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 28f2fee79..549a83dcf 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -52,6 +52,7 @@ George Paci George Song Greg Rogers Guillaume Chazarain +Hugo van Kemenade Ilia Meerovich Imri Goldberg Ionel Cristian Mărieș From 11998ee5ee43b334744123f820b488091728ae52 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jun 2018 11:43:01 -0400 Subject: [PATCH 078/952] Update NOTICE link to GitHub. --- CHANGES.rst | 2 +- MANIFEST.in | 2 +- Makefile | 2 +- README.rst | 2 +- __main__.py | 2 +- ci/download_appveyor.py | 2 +- coverage/__init__.py | 2 +- coverage/__main__.py | 2 +- coverage/annotate.py | 2 +- coverage/backunittest.py | 2 +- coverage/backward.py | 2 +- coverage/bytecode.py | 2 +- coverage/cmdline.py | 2 +- coverage/collector.py | 2 +- coverage/config.py | 2 +- coverage/control.py | 2 +- coverage/ctracer/datastack.c | 2 +- coverage/ctracer/datastack.h | 2 +- coverage/ctracer/filedisp.c | 2 +- coverage/ctracer/filedisp.h | 2 +- coverage/ctracer/module.c | 2 +- coverage/ctracer/stats.h | 2 +- coverage/ctracer/tracer.c | 2 +- coverage/ctracer/tracer.h | 2 +- coverage/ctracer/util.h | 2 +- coverage/data.py | 2 +- coverage/debug.py | 2 +- coverage/disposition.py | 2 +- coverage/env.py | 2 +- coverage/execfile.py | 2 +- coverage/files.py | 2 +- coverage/fullcoverage/encodings.py | 2 +- coverage/html.py | 2 +- coverage/htmlfiles/coverage_html.js | 2 +- coverage/htmlfiles/index.html | 2 +- coverage/htmlfiles/pyfile.html | 2 +- coverage/htmlfiles/style.css | 2 +- coverage/inorout.py | 2 +- coverage/misc.py | 2 +- coverage/multiproc.py | 2 +- coverage/parser.py | 2 +- coverage/phystokens.py | 2 +- coverage/pickle2json.py | 2 +- coverage/plugin.py | 2 +- coverage/plugin_support.py | 2 +- coverage/python.py | 2 +- coverage/pytracer.py | 2 +- coverage/report.py | 2 +- coverage/results.py | 2 +- coverage/summary.py | 2 +- coverage/templite.py | 2 +- coverage/version.py | 2 +- coverage/xmlreport.py | 2 +- doc/api.rst | 2 +- doc/api_coverage.rst | 2 +- doc/api_coveragedata.rst | 2 +- doc/api_plugin.rst | 2 +- doc/branch.rst | 2 +- doc/changes.rst | 2 +- doc/cmd.rst | 2 +- doc/conf.py | 2 +- doc/config.rst | 2 +- doc/contributing.rst | 2 +- doc/excluding.rst | 2 +- doc/faq.rst | 2 +- doc/howitworks.rst | 2 +- doc/index.rst | 2 +- doc/install.rst | 2 +- doc/plugins.rst | 2 +- doc/source.rst | 2 +- doc/subprocess.rst | 2 +- doc/trouble.rst | 2 +- igor.py | 2 +- lab/branches.py | 2 +- lab/hack_pyc.py | 2 +- lab/parser.py | 2 +- lab/platform_info.py | 2 +- lab/run_trace.py | 2 +- lab/show_pyc.py | 2 +- metacov.ini | 2 +- perf/perf_measure.py | 2 +- perf/solve_poly.py | 2 +- pylintrc | 2 +- requirements/ci.pip | 2 +- requirements/dev.pip | 2 +- requirements/pytest.pip | 2 +- setup.py | 2 +- tests/__init__.py | 2 +- tests/backtest.py | 2 +- tests/conftest.py | 2 +- tests/coveragetest.py | 2 +- tests/covmodzip1.py | 2 +- tests/eggsrc/egg1/egg1.py | 2 +- tests/eggsrc/setup.py | 2 +- tests/farm/annotate/annotate_dir.py | 2 +- tests/farm/annotate/gold/white.py,cover | 2 +- tests/farm/annotate/gold_anno_dir/a_a.py,cover | 2 +- tests/farm/annotate/gold_anno_dir/b_b.py,cover | 2 +- tests/farm/annotate/gold_anno_dir/multi.py,cover | 2 +- tests/farm/annotate/gold_encodings/utf8.py,cover | 2 +- tests/farm/annotate/gold_multi/a/a.py,cover | 2 +- tests/farm/annotate/gold_multi/b/b.py,cover | 2 +- tests/farm/annotate/gold_multi/multi.py,cover | 2 +- tests/farm/annotate/run.py | 2 +- tests/farm/annotate/run_encodings.py | 2 +- tests/farm/annotate/run_multi.py | 2 +- tests/farm/annotate/src/a/a.py | 2 +- tests/farm/annotate/src/b/b.py | 2 +- tests/farm/annotate/src/multi.py | 2 +- tests/farm/annotate/src/utf8.py | 2 +- tests/farm/annotate/src/white.py | 2 +- tests/farm/html/gold_a/a_py.html | 2 +- tests/farm/html/gold_b_branch/b_py.html | 2 +- tests/farm/html/gold_isolatin1/isolatin1_py.html | 2 +- tests/farm/html/gold_omit_1/m1_py.html | 2 +- tests/farm/html/gold_omit_1/m2_py.html | 2 +- tests/farm/html/gold_omit_1/m3_py.html | 2 +- tests/farm/html/gold_omit_1/main_py.html | 2 +- tests/farm/html/gold_omit_2/m2_py.html | 2 +- tests/farm/html/gold_omit_2/m3_py.html | 2 +- tests/farm/html/gold_omit_2/main_py.html | 2 +- tests/farm/html/gold_omit_3/m3_py.html | 2 +- tests/farm/html/gold_omit_3/main_py.html | 2 +- tests/farm/html/gold_omit_4/m1_py.html | 2 +- tests/farm/html/gold_omit_4/m3_py.html | 2 +- tests/farm/html/gold_omit_4/main_py.html | 2 +- tests/farm/html/gold_omit_5/m1_py.html | 2 +- tests/farm/html/gold_omit_5/main_py.html | 2 +- tests/farm/html/gold_other/blah_blah_other_py.html | 2 +- tests/farm/html/gold_other/here_py.html | 2 +- tests/farm/html/gold_partial/partial_py.html | 2 +- tests/farm/html/gold_styled/a_py.html | 2 +- tests/farm/html/gold_styled/style.css | 2 +- tests/farm/html/gold_unicode/unicode_py.html | 2 +- tests/farm/html/othersrc/other.py | 2 +- tests/farm/html/src/a.py | 2 +- tests/farm/html/src/b.py | 2 +- tests/farm/html/src/bom.py | 2 +- tests/farm/html/src/here.py | 2 +- tests/farm/html/src/isolatin1.py | 2 +- tests/farm/html/src/m1.py | 2 +- tests/farm/html/src/m2.py | 2 +- tests/farm/html/src/m3.py | 2 +- tests/farm/html/src/main.py | 2 +- tests/farm/html/src/omit4.ini | 2 +- tests/farm/html/src/omit5.ini | 2 +- tests/farm/html/src/partial.ini | 2 +- tests/farm/html/src/partial.py | 2 +- tests/farm/html/src/run_a_xml_2.ini | 2 +- tests/farm/html/src/tabbed.py | 2 +- tests/farm/html/src/unicode.py | 2 +- tests/farm/html/src/y.py | 2 +- tests/farm/run/run_chdir.py | 2 +- tests/farm/run/run_timid.py | 2 +- tests/farm/run/run_xxx.py | 2 +- tests/farm/run/src/chdir.py | 2 +- tests/farm/run/src/showtrace.py | 2 +- tests/farm/run/src/xxx | 2 +- tests/goldtest.py | 2 +- tests/helpers.py | 2 +- tests/js/tests.js | 2 +- tests/modules/covmod1.py | 2 +- tests/modules/namespace_420/sub1/__init__.py | 2 +- tests/modules/pkg1/p1a.py | 2 +- tests/modules/pkg1/p1b.py | 2 +- tests/modules/pkg1/p1c.py | 2 +- tests/modules/pkg1/runmod2.py | 2 +- tests/modules/pkg1/sub/ps1a.py | 2 +- tests/modules/pkg1/sub/runmod3.py | 2 +- tests/modules/pkg2/p2a.py | 2 +- tests/modules/pkg2/p2b.py | 2 +- tests/modules/plugins/another.py | 2 +- tests/modules/process_test/try_execfile.py | 2 +- tests/modules/runmod1.py | 2 +- tests/modules/usepkgs.py | 2 +- tests/moremodules/namespace_420/sub2/__init__.py | 2 +- tests/moremodules/othermods/othera.py | 2 +- tests/moremodules/othermods/otherb.py | 2 +- tests/moremodules/othermods/sub/osa.py | 2 +- tests/moremodules/othermods/sub/osb.py | 2 +- tests/osinfo.py | 2 +- tests/plugin1.py | 2 +- tests/plugin2.py | 2 +- tests/plugin_config.py | 2 +- tests/stress_phystoken.tok | 2 +- tests/stress_phystoken_dos.tok | 2 +- tests/test_api.py | 2 +- tests/test_arcs.py | 2 +- tests/test_backward.py | 2 +- tests/test_cmdline.py | 2 +- tests/test_collector.py | 2 +- tests/test_concurrency.py | 2 +- tests/test_config.py | 2 +- tests/test_coverage.py | 2 +- tests/test_data.py | 2 +- tests/test_debug.py | 2 +- tests/test_execfile.py | 2 +- tests/test_farm.py | 2 +- tests/test_filereporter.py | 2 +- tests/test_files.py | 2 +- tests/test_html.py | 2 +- tests/test_misc.py | 2 +- tests/test_oddball.py | 2 +- tests/test_parser.py | 2 +- tests/test_phystokens.py | 2 +- tests/test_pickle2json.py | 2 +- tests/test_plugins.py | 2 +- tests/test_process.py | 2 +- tests/test_python.py | 2 +- tests/test_results.py | 2 +- tests/test_setup.py | 2 +- tests/test_summary.py | 2 +- tests/test_templite.py | 2 +- tests/test_testing.py | 2 +- tests/test_version.py | 2 +- tests/test_xml.py | 2 +- tox.ini | 2 +- tox_wheels.ini | 2 +- 218 files changed, 218 insertions(+), 218 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5c8b4eecf..883cd973f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt ============================== Change history for Coverage.py diff --git a/MANIFEST.in b/MANIFEST.in index 275f7526d..536aa118e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # MANIFEST.in file for coverage.py diff --git a/Makefile b/Makefile index d9bc1775c..5fe79f902 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Makefile for utility work on coverage.py. diff --git a/README.rst b/README.rst index 007155925..7e37618be 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt =========== Coverage.py diff --git a/__main__.py b/__main__.py index f1f2b4f67..28ad7d2da 100644 --- a/__main__.py +++ b/__main__.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Be able to execute coverage.py by pointing Python at a working tree.""" diff --git a/ci/download_appveyor.py b/ci/download_appveyor.py index daf6f06eb..7cec413ca 100644 --- a/ci/download_appveyor.py +++ b/ci/download_appveyor.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Use the Appveyor API to download Windows artifacts.""" diff --git a/coverage/__init__.py b/coverage/__init__.py index 63f488f2f..0f17c0a1f 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Code coverage measurement for Python. diff --git a/coverage/__main__.py b/coverage/__main__.py index 35ab87a56..79aa4e2b3 100644 --- a/coverage/__main__.py +++ b/coverage/__main__.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Coverage.py's main entry point.""" diff --git a/coverage/annotate.py b/coverage/annotate.py index 4060450ff..48e2b91ce 100644 --- a/coverage/annotate.py +++ b/coverage/annotate.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Source file annotation for coverage.py.""" diff --git a/coverage/backunittest.py b/coverage/backunittest.py index 1b0848355..21d7bcb23 100644 --- a/coverage/backunittest.py +++ b/coverage/backunittest.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Implementations of unittest features from the future.""" diff --git a/coverage/backward.py b/coverage/backward.py index 5aff6406c..5f59b23f0 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Add things to old Pythons so I can pretend they are newer.""" diff --git a/coverage/bytecode.py b/coverage/bytecode.py index d823c67c9..943f29e17 100644 --- a/coverage/bytecode.py +++ b/coverage/bytecode.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Bytecode manipulation for coverage.py""" diff --git a/coverage/cmdline.py b/coverage/cmdline.py index ea86b4458..2af301411 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Command-line support for coverage.py.""" diff --git a/coverage/collector.py b/coverage/collector.py index bc385fc27..fa3eaaa49 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Raw data collector for coverage.py.""" diff --git a/coverage/config.py b/coverage/config.py index effa382f6..a0d7d06bc 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Config file for coverage.py""" diff --git a/coverage/control.py b/coverage/control.py index 80012f578..aa93671c4 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Core control stuff for coverage.py.""" diff --git a/coverage/ctracer/datastack.c b/coverage/ctracer/datastack.c index 515ba9249..a9cfcc2cf 100644 --- a/coverage/ctracer/datastack.c +++ b/coverage/ctracer/datastack.c @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #include "util.h" #include "datastack.h" diff --git a/coverage/ctracer/datastack.h b/coverage/ctracer/datastack.h index b2dbeb95e..3b3078ba2 100644 --- a/coverage/ctracer/datastack.h +++ b/coverage/ctracer/datastack.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_DATASTACK_H #define _COVERAGE_DATASTACK_H diff --git a/coverage/ctracer/filedisp.c b/coverage/ctracer/filedisp.c index 479a2c9f7..47782ae09 100644 --- a/coverage/ctracer/filedisp.c +++ b/coverage/ctracer/filedisp.c @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #include "util.h" #include "filedisp.h" diff --git a/coverage/ctracer/filedisp.h b/coverage/ctracer/filedisp.h index ada68eafe..860f9a50b 100644 --- a/coverage/ctracer/filedisp.h +++ b/coverage/ctracer/filedisp.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_FILEDISP_H #define _COVERAGE_FILEDISP_H diff --git a/coverage/ctracer/module.c b/coverage/ctracer/module.c index 762318593..f308902b6 100644 --- a/coverage/ctracer/module.c +++ b/coverage/ctracer/module.c @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #include "util.h" #include "tracer.h" diff --git a/coverage/ctracer/stats.h b/coverage/ctracer/stats.h index c5ffdf5f2..05173369f 100644 --- a/coverage/ctracer/stats.h +++ b/coverage/ctracer/stats.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_STATS_H #define _COVERAGE_STATS_H diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 6dcdc576d..01f8b19ba 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ /* C-based Tracer for coverage.py. */ diff --git a/coverage/ctracer/tracer.h b/coverage/ctracer/tracer.h index d5d630fb4..61c01b41a 100644 --- a/coverage/ctracer/tracer.h +++ b/coverage/ctracer/tracer.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_TRACER_H #define _COVERAGE_TRACER_H diff --git a/coverage/ctracer/util.h b/coverage/ctracer/util.h index f0c302cfd..96d2e51c0 100644 --- a/coverage/ctracer/util.h +++ b/coverage/ctracer/util.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_UTIL_H #define _COVERAGE_UTIL_H diff --git a/coverage/data.py b/coverage/data.py index 6f76a727b..9f2d13086 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Coverage data for coverage.py.""" diff --git a/coverage/debug.py b/coverage/debug.py index 6e6e80130..d63a90705 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Control of and utilities for debugging.""" diff --git a/coverage/disposition.py b/coverage/disposition.py index e9b8ba65c..9b9a997d8 100644 --- a/coverage/disposition.py +++ b/coverage/disposition.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Simple value objects for tracking what to do with files.""" diff --git a/coverage/env.py b/coverage/env.py index 4699a1e52..e35d026b9 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Determine facts about the environment.""" diff --git a/coverage/execfile.py b/coverage/execfile.py index 68417f8ac..b2b78444e 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Execute files of Python code.""" diff --git a/coverage/files.py b/coverage/files.py index 759ec2c97..70fde9dba 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """File wrangling.""" diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py index 699f38634..aeb416e40 100644 --- a/coverage/fullcoverage/encodings.py +++ b/coverage/fullcoverage/encodings.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Imposter encodings module that installs a coverage-style tracer. diff --git a/coverage/html.py b/coverage/html.py index b0c616499..1bef93a18 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """HTML reporting for coverage.py.""" diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index c1a41192f..7fc2963c6 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -1,5 +1,5 @@ // Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -// For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt // Coverage.py HTML report browser code. /*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html index 1e3999f9e..4129bc31b 100644 --- a/coverage/htmlfiles/index.html +++ b/coverage/htmlfiles/index.html @@ -1,5 +1,5 @@ {# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 #} -{# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt #} +{# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt #} diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html index 8542a4678..245ecf411 100644 --- a/coverage/htmlfiles/pyfile.html +++ b/coverage/htmlfiles/pyfile.html @@ -1,5 +1,5 @@ {# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 #} -{# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt #} +{# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt #} diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index 12e90645e..145928652 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ /* CSS styles for coverage.py. */ diff --git a/coverage/inorout.py b/coverage/inorout.py index c0f27d786..15e496afe 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Determining whether files are being measured/reported or not.""" diff --git a/coverage/misc.py b/coverage/misc.py index 28aa3b060..fff2a1875 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Miscellaneous stuff for coverage.py.""" diff --git a/coverage/multiproc.py b/coverage/multiproc.py index bbc88fbe9..62f6beb70 100644 --- a/coverage/multiproc.py +++ b/coverage/multiproc.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Monkey-patching to add multiprocessing support for coverage.py""" diff --git a/coverage/parser.py b/coverage/parser.py index 6e6cccd51..c9eb793f6 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Code parsing for coverage.py.""" diff --git a/coverage/phystokens.py b/coverage/phystokens.py index a2b23cfc3..ccfe63b3c 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Better tokenizing for coverage.py.""" diff --git a/coverage/pickle2json.py b/coverage/pickle2json.py index 95b42ef37..006558f15 100644 --- a/coverage/pickle2json.py +++ b/coverage/pickle2json.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Convert pickle to JSON for coverage.py.""" diff --git a/coverage/plugin.py b/coverage/plugin.py index 415246ab6..f65d419c4 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """ .. versionadded:: 4.0 diff --git a/coverage/plugin_support.py b/coverage/plugin_support.py index c737a42c5..0727a3b0b 100644 --- a/coverage/plugin_support.py +++ b/coverage/plugin_support.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Support for plugins.""" diff --git a/coverage/python.py b/coverage/python.py index 834bc3321..31db1a272 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Python source expertise for coverage.py""" diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 7e70bab61..d0549f72a 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Raw data collector for coverage.py.""" diff --git a/coverage/report.py b/coverage/report.py index b46086339..e4378f6db 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Reporter foundation for coverage.py.""" diff --git a/coverage/results.py b/coverage/results.py index 5f84a689f..7e3bd268d 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Results of coverage measurement.""" diff --git a/coverage/summary.py b/coverage/summary.py index 271b648a8..9fc60676d 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Summary reporting""" diff --git a/coverage/templite.py b/coverage/templite.py index 9944695a0..b546ef7c7 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """A simple Python template renderer, for a nano-subset of Django syntax. diff --git a/coverage/version.py b/coverage/version.py index 0e6b0f9cd..2639941a1 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """The version and URL for coverage.py""" # This file is exec'ed in setup.py, don't import anything! diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 3b651d468..511270f13 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """XML reporting for coverage.py""" diff --git a/doc/api.rst b/doc/api.rst index 26db06ef7..d485d90a6 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _api: diff --git a/doc/api_coverage.rst b/doc/api_coverage.rst index 70a57eef5..9ee959671 100644 --- a/doc/api_coverage.rst +++ b/doc/api_coverage.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _api_coverage: diff --git a/doc/api_coveragedata.rst b/doc/api_coveragedata.rst index 75215a3d4..b3b643283 100644 --- a/doc/api_coveragedata.rst +++ b/doc/api_coveragedata.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _api_coveragedata: diff --git a/doc/api_plugin.rst b/doc/api_plugin.rst index e400a2981..d070e2bf1 100644 --- a/doc/api_plugin.rst +++ b/doc/api_plugin.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _api_plugin: diff --git a/doc/branch.rst b/doc/branch.rst index f95f1d47e..92cab27b1 100644 --- a/doc/branch.rst +++ b/doc/branch.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _branch: diff --git a/doc/changes.rst b/doc/changes.rst index 0243b5c1b..1dd7b911b 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _changes: diff --git a/doc/cmd.rst b/doc/cmd.rst index baf1ca083..d198178f6 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _cmd: diff --git a/doc/conf.py b/doc/conf.py index 503387b52..9936d1b97 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,6 +1,6 @@ # -*- 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # # coverage.py documentation build configuration file, created by diff --git a/doc/config.rst b/doc/config.rst index 062aa740a..3e76e3d00 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _config: diff --git a/doc/contributing.rst b/doc/contributing.rst index 1b06bed74..d83175918 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _contributing: diff --git a/doc/excluding.rst b/doc/excluding.rst index 465afe167..e56cf0a76 100644 --- a/doc/excluding.rst +++ b/doc/excluding.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _excluding: diff --git a/doc/faq.rst b/doc/faq.rst index fb9dbeb24..293b34b08 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _faq: diff --git a/doc/howitworks.rst b/doc/howitworks.rst index 764472beb..62af42e35 100644 --- a/doc/howitworks.rst +++ b/doc/howitworks.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _howitworks: diff --git a/doc/index.rst b/doc/index.rst index c6ddf5de7..78496cdb3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt =========== Coverage.py diff --git a/doc/install.rst b/doc/install.rst index 4f3717b42..037fd62aa 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _install: diff --git a/doc/plugins.rst b/doc/plugins.rst index e4967b4ba..c77c1e33b 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _plugins: diff --git a/doc/source.rst b/doc/source.rst index 640fc431b..e1bc80385 100644 --- a/doc/source.rst +++ b/doc/source.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _source: diff --git a/doc/subprocess.rst b/doc/subprocess.rst index 7236b7ef1..060689592 100644 --- a/doc/subprocess.rst +++ b/doc/subprocess.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _subprocess: diff --git a/doc/trouble.rst b/doc/trouble.rst index d152599a5..8f260604e 100644 --- a/doc/trouble.rst +++ b/doc/trouble.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _trouble: diff --git a/igor.py b/igor.py index 3f5ce12b5..f7ae6e66b 100644 --- a/igor.py +++ b/igor.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Helper for building, testing, and linting coverage.py. diff --git a/lab/branches.py b/lab/branches.py index d1908d0fa..5a35f1bd1 100644 --- a/lab/branches.py +++ b/lab/branches.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Demonstrate some issues with coverage.py branch testing. diff --git a/lab/hack_pyc.py b/lab/hack_pyc.py index 0ebd99486..d63da8fa3 100644 --- a/lab/hack_pyc.py +++ b/lab/hack_pyc.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """ Wicked hack to get .pyc files to do bytecode tracing instead of line tracing. diff --git a/lab/parser.py b/lab/parser.py index 0393d209f..b3560506f 100644 --- a/lab/parser.py +++ b/lab/parser.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Parser.py: a main for invoking code in coverage/parser.py""" diff --git a/lab/platform_info.py b/lab/platform_info.py index 8caea50f9..61e02dd26 100644 --- a/lab/platform_info.py +++ b/lab/platform_info.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Dump information so we can get a quick look at what's available.""" diff --git a/lab/run_trace.py b/lab/run_trace.py index ea0a6cb78..ea9d9bb49 100644 --- a/lab/run_trace.py +++ b/lab/run_trace.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Run a simple trace function on a file of Python code.""" diff --git a/lab/show_pyc.py b/lab/show_pyc.py index 525797a8f..7573c1c31 100644 --- a/lab/show_pyc.py +++ b/lab/show_pyc.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt import binascii import dis diff --git a/metacov.ini b/metacov.ini index eebfc0fd4..50ea524df 100644 --- a/metacov.ini +++ b/metacov.ini @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Settings to use when using coverage.py to measure itself. [run] diff --git a/perf/perf_measure.py b/perf/perf_measure.py index 2125251ad..a8f2ffaa0 100644 --- a/perf/perf_measure.py +++ b/perf/perf_measure.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Run like this: # .tox/py36/bin/python perf/perf_measure.py diff --git a/perf/solve_poly.py b/perf/solve_poly.py index 41365f487..662317253 100644 --- a/perf/solve_poly.py +++ b/perf/solve_poly.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Given empirical data from perf_measure.py, calculate the coefficients of the # polynomials for file, call, and line operation counts. diff --git a/pylintrc b/pylintrc index dd94f5639..368d9184c 100644 --- a/pylintrc +++ b/pylintrc @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # lint Python modules using external checkers. # diff --git a/requirements/ci.pip b/requirements/ci.pip index 12e9c6989..cb94d7379 100644 --- a/requirements/ci.pip +++ b/requirements/ci.pip @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Things CI servers need to succeeed. -r tox.pip diff --git a/requirements/dev.pip b/requirements/dev.pip index 98cac62eb..7616d6b43 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Requirements for doing local development work on coverage.py. # https://requires.io/github/nedbat/coveragepy/requirements/ diff --git a/requirements/pytest.pip b/requirements/pytest.pip index d90c16b41..19a5ccd46 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # The pytest specifics used by coverage.py diff --git a/setup.py b/setup.py index 99874fd49..10e509a62 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Code coverage measurement for Python""" diff --git a/tests/__init__.py b/tests/__init__.py index 1ff1e1be6..38f2ff8b0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Automated tests. Run with pytest.""" diff --git a/tests/backtest.py b/tests/backtest.py index 827e891f9..979f6755e 100644 --- a/tests/backtest.py +++ b/tests/backtest.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Add things to old Pythons so I can pretend they are newer, for tests.""" diff --git a/tests/conftest.py b/tests/conftest.py index a0c21a84a..4470b7514 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """ Pytest auto configuration. diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 29d634c41..8e3fa05db 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Base test case class for coverage.py testing.""" diff --git a/tests/covmodzip1.py b/tests/covmodzip1.py index cab642814..0133ed6af 100644 --- a/tests/covmodzip1.py +++ b/tests/covmodzip1.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Module-level docstrings are counted differently in different versions of Python, # so don't add one here. diff --git a/tests/eggsrc/egg1/egg1.py b/tests/eggsrc/egg1/egg1.py index 72600808d..939386e3f 100644 --- a/tests/eggsrc/egg1/egg1.py +++ b/tests/eggsrc/egg1/egg1.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # My egg file! diff --git a/tests/eggsrc/setup.py b/tests/eggsrc/setup.py index c935798db..26a0b650f 100644 --- a/tests/eggsrc/setup.py +++ b/tests/eggsrc/setup.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt from setuptools import setup diff --git a/tests/farm/annotate/annotate_dir.py b/tests/farm/annotate/annotate_dir.py index 3bb2dbe74..9bf1b7685 100644 --- a/tests/farm/annotate/annotate_dir.py +++ b/tests/farm/annotate/annotate_dir.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt copy("src", "run") run(""" diff --git a/tests/farm/annotate/gold/white.py,cover b/tests/farm/annotate/gold/white.py,cover index fc1632267..b190ffdb9 100644 --- a/tests/farm/annotate/gold/white.py,cover +++ b/tests/farm/annotate/gold/white.py,cover @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A test case sent to me by Steve White diff --git a/tests/farm/annotate/gold_anno_dir/a_a.py,cover b/tests/farm/annotate/gold_anno_dir/a_a.py,cover index 4729cfbb4..0bd0f4a54 100644 --- a/tests/farm/annotate/gold_anno_dir/a_a.py,cover +++ b/tests/farm/annotate/gold_anno_dir/a_a.py,cover @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt > def a(x): > if x == 1: diff --git a/tests/farm/annotate/gold_anno_dir/b_b.py,cover b/tests/farm/annotate/gold_anno_dir/b_b.py,cover index 228715f05..49f723ac6 100644 --- a/tests/farm/annotate/gold_anno_dir/b_b.py,cover +++ b/tests/farm/annotate/gold_anno_dir/b_b.py,cover @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt > def b(x): > msg = "x is %s" % x diff --git a/tests/farm/annotate/gold_anno_dir/multi.py,cover b/tests/farm/annotate/gold_anno_dir/multi.py,cover index 90a13c915..980f4793b 100644 --- a/tests/farm/annotate/gold_anno_dir/multi.py,cover +++ b/tests/farm/annotate/gold_anno_dir/multi.py,cover @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt > import a.a > import b.b diff --git a/tests/farm/annotate/gold_encodings/utf8.py,cover b/tests/farm/annotate/gold_encodings/utf8.py,cover index 3ef31e0fa..0bd26415b 100644 --- a/tests/farm/annotate/gold_encodings/utf8.py,cover +++ b/tests/farm/annotate/gold_encodings/utf8.py,cover @@ -1,6 +1,6 @@ # -*- 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 + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # This comment has an accent: é diff --git a/tests/farm/annotate/gold_multi/a/a.py,cover b/tests/farm/annotate/gold_multi/a/a.py,cover index e5e972264..95f0d8c56 100644 --- a/tests/farm/annotate/gold_multi/a/a.py,cover +++ b/tests/farm/annotate/gold_multi/a/a.py,cover @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt > def a(x): > if x == 1: diff --git a/tests/farm/annotate/gold_multi/b/b.py,cover b/tests/farm/annotate/gold_multi/b/b.py,cover index 26b25548a..18240bf0a 100644 --- a/tests/farm/annotate/gold_multi/b/b.py,cover +++ b/tests/farm/annotate/gold_multi/b/b.py,cover @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt > def b(x): > print "x is %s" % x diff --git a/tests/farm/annotate/gold_multi/multi.py,cover b/tests/farm/annotate/gold_multi/multi.py,cover index 90a13c915..980f4793b 100644 --- a/tests/farm/annotate/gold_multi/multi.py,cover +++ b/tests/farm/annotate/gold_multi/multi.py,cover @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt > import a.a > import b.b diff --git a/tests/farm/annotate/run.py b/tests/farm/annotate/run.py index 33e5f6711..6c98a770b 100644 --- a/tests/farm/annotate/run.py +++ b/tests/farm/annotate/run.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt copy("src", "out") run(""" diff --git a/tests/farm/annotate/run_encodings.py b/tests/farm/annotate/run_encodings.py index 46d8c643e..8fd98fead 100644 --- a/tests/farm/annotate/run_encodings.py +++ b/tests/farm/annotate/run_encodings.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt copy("src", "out_encodings") run(""" diff --git a/tests/farm/annotate/run_multi.py b/tests/farm/annotate/run_multi.py index 4646293e9..d3c4cf7c8 100644 --- a/tests/farm/annotate/run_multi.py +++ b/tests/farm/annotate/run_multi.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt copy("src", "out_multi") run(""" diff --git a/tests/farm/annotate/src/a/a.py b/tests/farm/annotate/src/a/a.py index e3e6631d7..411d3d8ef 100644 --- a/tests/farm/annotate/src/a/a.py +++ b/tests/farm/annotate/src/a/a.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt def a(x): if x == 1: diff --git a/tests/farm/annotate/src/b/b.py b/tests/farm/annotate/src/b/b.py index b31d8c956..f0055b3b0 100644 --- a/tests/farm/annotate/src/b/b.py +++ b/tests/farm/annotate/src/b/b.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt def b(x): msg = "x is %s" % x diff --git a/tests/farm/annotate/src/multi.py b/tests/farm/annotate/src/multi.py index bf8cfd5fb..31155b407 100644 --- a/tests/farm/annotate/src/multi.py +++ b/tests/farm/annotate/src/multi.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt import a.a import b.b diff --git a/tests/farm/annotate/src/utf8.py b/tests/farm/annotate/src/utf8.py index fd43b2ab0..f3988379f 100644 --- a/tests/farm/annotate/src/utf8.py +++ b/tests/farm/annotate/src/utf8.py @@ -1,6 +1,6 @@ # -*- 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # This comment has an accent: é diff --git a/tests/farm/annotate/src/white.py b/tests/farm/annotate/src/white.py index 21e8a6275..566083244 100644 --- a/tests/farm/annotate/src/white.py +++ b/tests/farm/annotate/src/white.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A test case sent to me by Steve White diff --git a/tests/farm/html/gold_a/a_py.html b/tests/farm/html/gold_a/a_py.html index 00ab529d6..da7006154 100644 --- a/tests/farm/html/gold_a/a_py.html +++ b/tests/farm/html/gold_a/a_py.html @@ -81,7 +81,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

# A test file for HTML reporting by coverage.py. 

 

diff --git a/tests/farm/html/gold_b_branch/b_py.html b/tests/farm/html/gold_b_branch/b_py.html index 4a8765005..839e4c98f 100644 --- a/tests/farm/html/gold_b_branch/b_py.html +++ b/tests/farm/html/gold_b_branch/b_py.html @@ -105,7 +105,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

# A test file for HTML reporting by coverage.py. 

 

diff --git a/tests/farm/html/gold_isolatin1/isolatin1_py.html b/tests/farm/html/gold_isolatin1/isolatin1_py.html index bad0d78d0..1c0f79d9c 100644 --- a/tests/farm/html/gold_isolatin1/isolatin1_py.html +++ b/tests/farm/html/gold_isolatin1/isolatin1_py.html @@ -80,7 +80,7 @@

# -*- coding: iso8859-1 -*- 

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

# A Python source file in another encoding. 

 

diff --git a/tests/farm/html/gold_omit_1/m1_py.html b/tests/farm/html/gold_omit_1/m1_py.html index 44d73e495..5d9541453 100644 --- a/tests/farm/html/gold_omit_1/m1_py.html +++ b/tests/farm/html/gold_omit_1/m1_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m1a = 1 

m1b = 2 

diff --git a/tests/farm/html/gold_omit_1/m2_py.html b/tests/farm/html/gold_omit_1/m2_py.html index ff75cbe97..12fc68486 100644 --- a/tests/farm/html/gold_omit_1/m2_py.html +++ b/tests/farm/html/gold_omit_1/m2_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m2a = 1 

m2b = 2 

diff --git a/tests/farm/html/gold_omit_1/m3_py.html b/tests/farm/html/gold_omit_1/m3_py.html index 4cdcc0885..e5b40653d 100644 --- a/tests/farm/html/gold_omit_1/m3_py.html +++ b/tests/farm/html/gold_omit_1/m3_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m3a = 1 

m3b = 2 

diff --git a/tests/farm/html/gold_omit_1/main_py.html b/tests/farm/html/gold_omit_1/main_py.html index d33dfedc8..7ae0d5f25 100644 --- a/tests/farm/html/gold_omit_1/main_py.html +++ b/tests/farm/html/gold_omit_1/main_py.html @@ -84,7 +84,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

import m1 

import m2 

diff --git a/tests/farm/html/gold_omit_2/m2_py.html b/tests/farm/html/gold_omit_2/m2_py.html index ff75cbe97..12fc68486 100644 --- a/tests/farm/html/gold_omit_2/m2_py.html +++ b/tests/farm/html/gold_omit_2/m2_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m2a = 1 

m2b = 2 

diff --git a/tests/farm/html/gold_omit_2/m3_py.html b/tests/farm/html/gold_omit_2/m3_py.html index 4cdcc0885..e5b40653d 100644 --- a/tests/farm/html/gold_omit_2/m3_py.html +++ b/tests/farm/html/gold_omit_2/m3_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m3a = 1 

m3b = 2 

diff --git a/tests/farm/html/gold_omit_2/main_py.html b/tests/farm/html/gold_omit_2/main_py.html index d33dfedc8..7ae0d5f25 100644 --- a/tests/farm/html/gold_omit_2/main_py.html +++ b/tests/farm/html/gold_omit_2/main_py.html @@ -84,7 +84,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

import m1 

import m2 

diff --git a/tests/farm/html/gold_omit_3/m3_py.html b/tests/farm/html/gold_omit_3/m3_py.html index 4cdcc0885..e5b40653d 100644 --- a/tests/farm/html/gold_omit_3/m3_py.html +++ b/tests/farm/html/gold_omit_3/m3_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m3a = 1 

m3b = 2 

diff --git a/tests/farm/html/gold_omit_3/main_py.html b/tests/farm/html/gold_omit_3/main_py.html index d33dfedc8..7ae0d5f25 100644 --- a/tests/farm/html/gold_omit_3/main_py.html +++ b/tests/farm/html/gold_omit_3/main_py.html @@ -84,7 +84,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

import m1 

import m2 

diff --git a/tests/farm/html/gold_omit_4/m1_py.html b/tests/farm/html/gold_omit_4/m1_py.html index 44d73e495..5d9541453 100644 --- a/tests/farm/html/gold_omit_4/m1_py.html +++ b/tests/farm/html/gold_omit_4/m1_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m1a = 1 

m1b = 2 

diff --git a/tests/farm/html/gold_omit_4/m3_py.html b/tests/farm/html/gold_omit_4/m3_py.html index 4cdcc0885..e5b40653d 100644 --- a/tests/farm/html/gold_omit_4/m3_py.html +++ b/tests/farm/html/gold_omit_4/m3_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m3a = 1 

m3b = 2 

diff --git a/tests/farm/html/gold_omit_4/main_py.html b/tests/farm/html/gold_omit_4/main_py.html index d33dfedc8..7ae0d5f25 100644 --- a/tests/farm/html/gold_omit_4/main_py.html +++ b/tests/farm/html/gold_omit_4/main_py.html @@ -84,7 +84,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

import m1 

import m2 

diff --git a/tests/farm/html/gold_omit_5/m1_py.html b/tests/farm/html/gold_omit_5/m1_py.html index 44d73e495..5d9541453 100644 --- a/tests/farm/html/gold_omit_5/m1_py.html +++ b/tests/farm/html/gold_omit_5/m1_py.html @@ -76,7 +76,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

m1a = 1 

m1b = 2 

diff --git a/tests/farm/html/gold_omit_5/main_py.html b/tests/farm/html/gold_omit_5/main_py.html index d33dfedc8..7ae0d5f25 100644 --- a/tests/farm/html/gold_omit_5/main_py.html +++ b/tests/farm/html/gold_omit_5/main_py.html @@ -84,7 +84,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

import m1 

import m2 

diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index 54861c23c..4eda1194a 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -78,7 +78,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

# A file in another directory.  We're checking that it ends up in the 

# HTML report. 

diff --git a/tests/farm/html/gold_other/here_py.html b/tests/farm/html/gold_other/here_py.html index 82aa70a2f..82fc7cbae 100644 --- a/tests/farm/html/gold_other/here_py.html +++ b/tests/farm/html/gold_other/here_py.html @@ -82,7 +82,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

# A test file for HTML reporting by coverage.py. 

 

diff --git a/tests/farm/html/gold_partial/partial_py.html b/tests/farm/html/gold_partial/partial_py.html index 796153f1f..2d763fa3a 100644 --- a/tests/farm/html/gold_partial/partial_py.html +++ b/tests/farm/html/gold_partial/partial_py.html @@ -94,7 +94,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

# partial branches 

 

diff --git a/tests/farm/html/gold_styled/a_py.html b/tests/farm/html/gold_styled/a_py.html index c8221753b..af1d9121a 100644 --- a/tests/farm/html/gold_styled/a_py.html +++ b/tests/farm/html/gold_styled/a_py.html @@ -83,7 +83,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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

# A test file for HTML reporting by coverage.py. 

 

diff --git a/tests/farm/html/gold_styled/style.css b/tests/farm/html/gold_styled/style.css index c3767eef6..0cd0cce19 100644 --- a/tests/farm/html/gold_styled/style.css +++ b/tests/farm/html/gold_styled/style.css @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ /* CSS styles for coverage.py. */ /* Page-wide styles */ diff --git a/tests/farm/html/gold_unicode/unicode_py.html b/tests/farm/html/gold_unicode/unicode_py.html index 83b0f382d..5a6f3ff9f 100644 --- a/tests/farm/html/gold_unicode/unicode_py.html +++ b/tests/farm/html/gold_unicode/unicode_py.html @@ -80,7 +80,7 @@

# -*- 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 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

# A Python source file with exotic characters. 

 

diff --git a/tests/farm/html/othersrc/other.py b/tests/farm/html/othersrc/other.py index bf0304d29..54b4fb7c7 100644 --- a/tests/farm/html/othersrc/other.py +++ b/tests/farm/html/othersrc/other.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A file in another directory. We're checking that it ends up in the # HTML report. diff --git a/tests/farm/html/src/a.py b/tests/farm/html/src/a.py index 85764e211..f31bdedb6 100644 --- a/tests/farm/html/src/a.py +++ b/tests/farm/html/src/a.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A test file for HTML reporting by coverage.py. diff --git a/tests/farm/html/src/b.py b/tests/farm/html/src/b.py index cb673c229..720cb5f15 100644 --- a/tests/farm/html/src/b.py +++ b/tests/farm/html/src/b.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A test file for HTML reporting by coverage.py. diff --git a/tests/farm/html/src/bom.py b/tests/farm/html/src/bom.py index 21d26ca27..098ad84fe 100644 --- a/tests/farm/html/src/bom.py +++ b/tests/farm/html/src/bom.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A Python source file in utf-8, with BOM. math = "3×4 = 12, ÷2 = 6±0" diff --git a/tests/farm/html/src/here.py b/tests/farm/html/src/here.py index fee9960db..ca85c75dd 100644 --- a/tests/farm/html/src/here.py +++ b/tests/farm/html/src/here.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A test file for HTML reporting by coverage.py. diff --git a/tests/farm/html/src/isolatin1.py b/tests/farm/html/src/isolatin1.py index 55a6f7def..69b4a529c 100644 --- a/tests/farm/html/src/isolatin1.py +++ b/tests/farm/html/src/isolatin1.py @@ -1,6 +1,6 @@ # -*- coding: iso8859-1 -*- # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A Python source file in another encoding. diff --git a/tests/farm/html/src/m1.py b/tests/farm/html/src/m1.py index 524fb0aac..bef6c9ae3 100644 --- a/tests/farm/html/src/m1.py +++ b/tests/farm/html/src/m1.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt m1a = 1 m1b = 2 diff --git a/tests/farm/html/src/m2.py b/tests/farm/html/src/m2.py index 2d13bfe23..ac75070af 100644 --- a/tests/farm/html/src/m2.py +++ b/tests/farm/html/src/m2.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt m2a = 1 m2b = 2 diff --git a/tests/farm/html/src/m3.py b/tests/farm/html/src/m3.py index 96e8b992a..a6f871cd9 100644 --- a/tests/farm/html/src/m3.py +++ b/tests/farm/html/src/m3.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt m3a = 1 m3b = 2 diff --git a/tests/farm/html/src/main.py b/tests/farm/html/src/main.py index 238d0b58b..3d0eba65f 100644 --- a/tests/farm/html/src/main.py +++ b/tests/farm/html/src/main.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt import m1 import m2 diff --git a/tests/farm/html/src/omit4.ini b/tests/farm/html/src/omit4.ini index b792e7037..844d3fd0a 100644 --- a/tests/farm/html/src/omit4.ini +++ b/tests/farm/html/src/omit4.ini @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [report] omit = m2.py diff --git a/tests/farm/html/src/omit5.ini b/tests/farm/html/src/omit5.ini index 3b6add295..2615c056b 100644 --- a/tests/farm/html/src/omit5.ini +++ b/tests/farm/html/src/omit5.ini @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [report] omit = diff --git a/tests/farm/html/src/partial.ini b/tests/farm/html/src/partial.ini index cdb241b5c..86a1b9bd3 100644 --- a/tests/farm/html/src/partial.ini +++ b/tests/farm/html/src/partial.ini @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [run] branch = True diff --git a/tests/farm/html/src/partial.py b/tests/farm/html/src/partial.py index 0f8fbe3cc..ea97ec4fd 100644 --- a/tests/farm/html/src/partial.py +++ b/tests/farm/html/src/partial.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # partial branches and excluded lines diff --git a/tests/farm/html/src/run_a_xml_2.ini b/tests/farm/html/src/run_a_xml_2.ini index f632bd097..85ba5e8b4 100644 --- a/tests/farm/html/src/run_a_xml_2.ini +++ b/tests/farm/html/src/run_a_xml_2.ini @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Put all the XML output in xml_2 [xml] diff --git a/tests/farm/html/src/tabbed.py b/tests/farm/html/src/tabbed.py index e897e9fa3..573ab1269 100644 --- a/tests/farm/html/src/tabbed.py +++ b/tests/farm/html/src/tabbed.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # This file should have tabs. x = 1 diff --git a/tests/farm/html/src/unicode.py b/tests/farm/html/src/unicode.py index 37c5533af..c592935bb 100644 --- a/tests/farm/html/src/unicode.py +++ b/tests/farm/html/src/unicode.py @@ -1,6 +1,6 @@ # -*- 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A Python source file with exotic characters. diff --git a/tests/farm/html/src/y.py b/tests/farm/html/src/y.py index a50bb629d..3ed0ba49b 100644 --- a/tests/farm/html/src/y.py +++ b/tests/farm/html/src/y.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # A test file for XML reporting by coverage.py. diff --git a/tests/farm/run/run_chdir.py b/tests/farm/run/run_chdir.py index 1da4e9a32..5ec0b4ea0 100644 --- a/tests/farm/run/run_chdir.py +++ b/tests/farm/run/run_chdir.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt copy("src", "out_chdir") run(""" diff --git a/tests/farm/run/run_timid.py b/tests/farm/run/run_timid.py index 0370cf848..564566189 100644 --- a/tests/farm/run/run_timid.py +++ b/tests/farm/run/run_timid.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Test that the --timid command line argument properly swaps the tracer # function for a simpler one. diff --git a/tests/farm/run/run_xxx.py b/tests/farm/run/run_xxx.py index 1db5b0d0c..3caba0ec7 100644 --- a/tests/farm/run/run_xxx.py +++ b/tests/farm/run/run_xxx.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt copy("src", "out_xxx") run(""" diff --git a/tests/farm/run/src/chdir.py b/tests/farm/run/src/chdir.py index 35cfcc811..250f7132f 100644 --- a/tests/farm/run/src/chdir.py +++ b/tests/farm/run/src/chdir.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt import os print("Line One") diff --git a/tests/farm/run/src/showtrace.py b/tests/farm/run/src/showtrace.py index 3a2750a6f..a36925218 100644 --- a/tests/farm/run/src/showtrace.py +++ b/tests/farm/run/src/showtrace.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Show the current frame's trace function, so that we can test what the # command-line options do to the trace function used. diff --git a/tests/farm/run/src/xxx b/tests/farm/run/src/xxx index 864da4576..818c7f938 100644 --- a/tests/farm/run/src/xxx +++ b/tests/farm/run/src/xxx @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # This is a python file though it doesn't look like it, like a main script. a = b = c = d = 0 diff --git a/tests/goldtest.py b/tests/goldtest.py index baaa8f01d..8f24f239d 100644 --- a/tests/goldtest.py +++ b/tests/goldtest.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """A test base class for tests based on gold file comparison.""" diff --git a/tests/helpers.py b/tests/helpers.py index f10169a95..bc9c39829 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Helpers for coverage.py tests.""" diff --git a/tests/js/tests.js b/tests/js/tests.js index bf2ca7344..7bd3b9ca9 100644 --- a/tests/js/tests.js +++ b/tests/js/tests.js @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ // Tests of coverage.py HTML report chunk navigation. /*global coverage, jQuery, $ */ diff --git a/tests/modules/covmod1.py b/tests/modules/covmod1.py index 0f9638b87..618f1d048 100644 --- a/tests/modules/covmod1.py +++ b/tests/modules/covmod1.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # covmod1.py: Simplest module for testing. i = 1 diff --git a/tests/modules/namespace_420/sub1/__init__.py b/tests/modules/namespace_420/sub1/__init__.py index 94bb2959c..4a8721abb 100644 --- a/tests/modules/namespace_420/sub1/__init__.py +++ b/tests/modules/namespace_420/sub1/__init__.py @@ -1,4 +1,4 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt sub1 = "namespace_420 sub1" diff --git a/tests/modules/pkg1/p1a.py b/tests/modules/pkg1/p1a.py index 5d81b1fad..984bf7483 100644 --- a/tests/modules/pkg1/p1a.py +++ b/tests/modules/pkg1/p1a.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt import os, sys diff --git a/tests/modules/pkg1/p1b.py b/tests/modules/pkg1/p1b.py index 53505cef4..dc6d0b17a 100644 --- a/tests/modules/pkg1/p1b.py +++ b/tests/modules/pkg1/p1b.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt x = 1 y = 2 diff --git a/tests/modules/pkg1/p1c.py b/tests/modules/pkg1/p1c.py index 98f319e8a..24e2932c1 100644 --- a/tests/modules/pkg1/p1c.py +++ b/tests/modules/pkg1/p1c.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt a = 1 b = 2 diff --git a/tests/modules/pkg1/runmod2.py b/tests/modules/pkg1/runmod2.py index 5911db7bd..b0f43c2e1 100644 --- a/tests/modules/pkg1/runmod2.py +++ b/tests/modules/pkg1/runmod2.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Used in the tests for run_python_module import sys diff --git a/tests/modules/pkg1/sub/ps1a.py b/tests/modules/pkg1/sub/ps1a.py index 44d3b2741..b895fc608 100644 --- a/tests/modules/pkg1/sub/ps1a.py +++ b/tests/modules/pkg1/sub/ps1a.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt d = 1 e = 2 diff --git a/tests/modules/pkg1/sub/runmod3.py b/tests/modules/pkg1/sub/runmod3.py index 1f5ce27e3..b3b40327b 100644 --- a/tests/modules/pkg1/sub/runmod3.py +++ b/tests/modules/pkg1/sub/runmod3.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Used in the tests for run_python_module import sys diff --git a/tests/modules/pkg2/p2a.py b/tests/modules/pkg2/p2a.py index 62caae227..f3f86f992 100644 --- a/tests/modules/pkg2/p2a.py +++ b/tests/modules/pkg2/p2a.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt q = 1 r = 1 diff --git a/tests/modules/pkg2/p2b.py b/tests/modules/pkg2/p2b.py index 73716eb40..38d7a840f 100644 --- a/tests/modules/pkg2/p2b.py +++ b/tests/modules/pkg2/p2b.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt t = 1 u = 1 diff --git a/tests/modules/plugins/another.py b/tests/modules/plugins/another.py index 80902d342..dfe03c973 100644 --- a/tests/modules/plugins/another.py +++ b/tests/modules/plugins/another.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """A plugin for tests to reference.""" diff --git a/tests/modules/process_test/try_execfile.py b/tests/modules/process_test/try_execfile.py index 3068327e0..706fe39f0 100644 --- a/tests/modules/process_test/try_execfile.py +++ b/tests/modules/process_test/try_execfile.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Test file for run_python_file. diff --git a/tests/modules/runmod1.py b/tests/modules/runmod1.py index b43b299ab..cb1f7e999 100644 --- a/tests/modules/runmod1.py +++ b/tests/modules/runmod1.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Used in the tests for run_python_module import sys diff --git a/tests/modules/usepkgs.py b/tests/modules/usepkgs.py index 222e68ceb..63ce7c186 100644 --- a/tests/modules/usepkgs.py +++ b/tests/modules/usepkgs.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt import pkg1.p1a, pkg1.p1b, pkg1.sub import pkg2.p2a, pkg2.p2b diff --git a/tests/moremodules/namespace_420/sub2/__init__.py b/tests/moremodules/namespace_420/sub2/__init__.py index 0839688c2..b5e413cc8 100644 --- a/tests/moremodules/namespace_420/sub2/__init__.py +++ b/tests/moremodules/namespace_420/sub2/__init__.py @@ -1,4 +1,4 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt sub2 = "namespace_420 sub2" diff --git a/tests/moremodules/othermods/othera.py b/tests/moremodules/othermods/othera.py index b3ee9c046..56cdfe801 100644 --- a/tests/moremodules/othermods/othera.py +++ b/tests/moremodules/othermods/othera.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt o = 1 p = 2 diff --git a/tests/moremodules/othermods/otherb.py b/tests/moremodules/othermods/otherb.py index 334fdc4aa..50ba96a35 100644 --- a/tests/moremodules/othermods/otherb.py +++ b/tests/moremodules/othermods/otherb.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt q = 3 r = 4 diff --git a/tests/moremodules/othermods/sub/osa.py b/tests/moremodules/othermods/sub/osa.py index 4005640e8..5e5a5ff5f 100644 --- a/tests/moremodules/othermods/sub/osa.py +++ b/tests/moremodules/othermods/sub/osa.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt s = 5 t = 6 diff --git a/tests/moremodules/othermods/sub/osb.py b/tests/moremodules/othermods/sub/osb.py index 7d96fb79d..c150087d2 100644 --- a/tests/moremodules/othermods/sub/osb.py +++ b/tests/moremodules/othermods/sub/osb.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt u = 7 v = 8 diff --git a/tests/osinfo.py b/tests/osinfo.py index 094fb0979..f9562debe 100644 --- a/tests/osinfo.py +++ b/tests/osinfo.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """OS information for testing.""" diff --git a/tests/plugin1.py b/tests/plugin1.py index e03dadf1e..a070af367 100644 --- a/tests/plugin1.py +++ b/tests/plugin1.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """A file tracer plugin for test_plugins.py to import.""" diff --git a/tests/plugin2.py b/tests/plugin2.py index c1ab1c23f..c334628ad 100644 --- a/tests/plugin2.py +++ b/tests/plugin2.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """A file tracer plugin for test_plugins.py to import.""" diff --git a/tests/plugin_config.py b/tests/plugin_config.py index 67a790a2e..c4d3cf65b 100644 --- a/tests/plugin_config.py +++ b/tests/plugin_config.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """A configuring plugin for test_plugins.py to import.""" diff --git a/tests/stress_phystoken.tok b/tests/stress_phystoken.tok index eb2fb6699..f2b190c36 100644 --- a/tests/stress_phystoken.tok +++ b/tests/stress_phystoken.tok @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Here's some random Python so that test_tokenize_myself will have some # stressful stuff to try. This file is .tok instead of .py so pylint won't diff --git a/tests/stress_phystoken_dos.tok b/tests/stress_phystoken_dos.tok index 5b016a776..eb937d95c 100644 --- a/tests/stress_phystoken_dos.tok +++ b/tests/stress_phystoken_dos.tok @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Here's some random Python so that test_tokenize_myself will have some # stressful stuff to try. This file is .tok instead of .py so pylint won't diff --git a/tests/test_api.py b/tests/test_api.py index feb8b2e67..248784ffb 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.py's API.""" diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 4bd804ba3..c86147b76 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.py's arc measurement.""" diff --git a/tests/test_backward.py b/tests/test_backward.py index bbecb7801..8acb8707c 100644 --- a/tests/test_backward.py +++ b/tests/test_backward.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests that our version shims in backward.py are working.""" diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 66fcec3a9..b6fad76df 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Test cmdline.py for coverage.py.""" diff --git a/tests/test_collector.py b/tests/test_collector.py index bd9634158..9989b2292 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests of coverage/collector.py and other collectors.""" diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 88f2b50d2..7d54a97f9 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for concurrency libraries.""" diff --git a/tests/test_config.py b/tests/test_config.py index bbfa46776..513522ee3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Test the config file handling for coverage.py""" diff --git a/tests/test_coverage.py b/tests/test_coverage.py index c8ac55dfc..676fc831f 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.py.""" diff --git a/tests/test_data.py b/tests/test_data.py index 46999f636..0d3172d42 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.data""" diff --git a/tests/test_debug.py b/tests/test_debug.py index c81ca24db..2699ca61f 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests of coverage/debug.py""" diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 693df71a3..cb835c2a4 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.execfile""" diff --git a/tests/test_farm.py b/tests/test_farm.py index 4fc0ea5a3..892d23938 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Run tests in the farm sub-directory. Designed for pytest.""" diff --git a/tests/test_filereporter.py b/tests/test_filereporter.py index 91e477620..e50a74933 100644 --- a/tests/test_filereporter.py +++ b/tests/test_filereporter.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for FileReporters""" diff --git a/tests/test_files.py b/tests/test_files.py index dd88b6ebb..2e705a1be 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for files.py""" diff --git a/tests/test_html.py b/tests/test_html.py index 9bb8f3924..b6f6f9eb8 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1,6 +1,6 @@ # -*- 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests that HTML generation is awesome.""" diff --git a/tests/test_misc.py b/tests/test_misc.py index 939b1c980..f3d485cc2 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests of miscellaneous stuff.""" diff --git a/tests/test_oddball.py b/tests/test_oddball.py index 5bd204d91..5d615c350 100644 --- a/tests/test_oddball.py +++ b/tests/test_oddball.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Oddball cases for testing coverage.py""" diff --git a/tests/test_parser.py b/tests/test_parser.py index 169319f54..c2d70ee58 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.py's code parsing.""" diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py index 15bc6af08..1045225e4 100644 --- a/tests/test_phystokens.py +++ b/tests/test_phystokens.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.py's improved tokenizer.""" diff --git a/tests/test_pickle2json.py b/tests/test_pickle2json.py index 433dade62..37886bac0 100644 --- a/tests/test_pickle2json.py +++ b/tests/test_pickle2json.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.pickle2json""" diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 27227c33c..c9a8feaef 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for plugins.""" diff --git a/tests/test_process.py b/tests/test_process.py index 68262a57d..b4a881786 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for process behavior of coverage.py.""" diff --git a/tests/test_python.py b/tests/test_python.py index 9027aa6c2..441ef499a 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests of coverage/python.py""" diff --git a/tests/test_results.py b/tests/test_results.py index deaf8113c..307ef7623 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.py's results analysis.""" diff --git a/tests/test_setup.py b/tests/test_setup.py index 78fcefc92..4467d2092 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests of miscellaneous stuff.""" diff --git a/tests/test_summary.py b/tests/test_summary.py index 44defa5b0..b404f1fff 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Test text-based summary reporting for coverage.py""" diff --git a/tests/test_templite.py b/tests/test_templite.py index bcc65f946..16942db83 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for coverage.templite.""" diff --git a/tests/test_testing.py b/tests/test_testing.py index 05bf0c92c..d8cd0ef05 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,6 +1,6 @@ # -*- 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests that our test infrastructure is really working!""" diff --git a/tests/test_version.py b/tests/test_version.py index eb8de8752..11b180d52 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests of version.py.""" diff --git a/tests/test_xml.py b/tests/test_xml.py index b49debc9a..acb82a483 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for XML reports from coverage.py.""" diff --git a/tox.ini b/tox.ini index 8c35391f7..6a2fcb561 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [tox] envlist = py{27,34,35,36,37}, pypy{2,3}, jython, doc, lint diff --git a/tox_wheels.ini b/tox_wheels.ini index adf48bf72..121b0331a 100644 --- a/tox_wheels.ini +++ b/tox_wheels.ini @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [tox] envlist = py{27,34,35,36,sys} From 69bb3e4a4eca4e13ff3d3996d1fa1776082b0fe8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 23 Jun 2018 20:36:31 -0400 Subject: [PATCH 079/952] Update README for GitHub --- README.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 7e37618be..5072e84da 100644 --- a/README.rst +++ b/README.rst @@ -25,10 +25,9 @@ Coverage.py runs on many versions of Python: * IronPython 2.7.7, though not for reporting. Documentation is on `Read the Docs`_. Code repository and issue tracker are on -`Bitbucket`_, with a mirrored repository on `GitHub`_. +`GitHub`_. .. _Read the Docs: https://coverage.readthedocs.io/ -.. _Bitbucket: https://bitbucket.org/ned/coveragepy .. _GitHub: https://github.com/nedbat/coveragepy @@ -71,7 +70,7 @@ License Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. .. _Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0 -.. _NOTICE.txt: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. _NOTICE.txt: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. |ci-status| image:: https://travis-ci.org/nedbat/coveragepy.svg?branch=master From 2256c1ba0cd6a27bf8da160d7413ada50912b608 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jun 2018 08:02:16 -0400 Subject: [PATCH 080/952] Don't need .hg* files --- .hgignore | 43 ---------------------------------- .hgtags | 69 ------------------------------------------------------- 2 files changed, 112 deletions(-) delete mode 100644 .hgignore delete mode 100644 .hgtags diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 53576e149..000000000 --- a/.hgignore +++ /dev/null @@ -1,43 +0,0 @@ -syntax: glob - -# Files that can appear anywhere in the tree. -*.pyc -*.pyo -*$py.class -*.pyd -*.so -*.bak -.coverage -.coverage.* -coverage.xml -.metacov -.metacov.* -*.swp - -# Stuff generated by editors. -.idea/ -.vimtags - -# Stuff in the root. -build -*.egg-info -dist -htmlcov -MANIFEST -setuptools-*.egg -.tox* -.noseids -.cache -.pytest_cache -.hypothesis - -# Stuff in the test directory. -zipmods.zip - -# Stuff in the doc directory. -_build -_spell -sample_html_beta - -# Stuff in the ci directory. -*.token diff --git a/.hgtags b/.hgtags deleted file mode 100644 index 48574a0fe..000000000 --- a/.hgtags +++ /dev/null @@ -1,69 +0,0 @@ -4105a4de000ef94bb1f80cecae7c93aaf282bdbc coverage-3.0b1 -79dd373074def79b3822ee9254307295bf991d16 coverage-3.0b2 -ad991769ad4f685089bacc594e689bab50a6db90 coverage-3.0b3 -9041c4e99c3f8181d35e47df6fd83ceedb1f47fc coverage-3.0 -483a54854358881c360c6a717709b45a5225bd69 coverage-3.0.1 -67f7698c6c0413cc335ed5af7dd981bcf0a126a4 coverage-3.1b1 -67f7698c6c0413cc335ed5af7dd981bcf0a126a4 coverage-3.1b1 -27b6be608b2be50b92b91610846117ab7c47831c coverage-3.1b1 -4d89e089a8f3964522b19d50b4e90be0f2e49b53 coverage-3.1 -524f15a1a176be1b9afb737ab1be99c972f8c928 coverage-3.2b1 -524f15a1a176be1b9afb737ab1be99c972f8c928 coverage-3.2b1 -9231251c603ef0d491b01799b3d1da7af54dd083 coverage-3.2b1 -9231251c603ef0d491b01799b3d1da7af54dd083 coverage-3.2b1 -da052544181e335a18b4141b8b7a47e8246aa9e8 coverage-3.2b1 -11f0fbec2bc9b22a5fd8687d8b94338a4a363e6c coverage-3.2b2 -d42ad49a0aaca90867644876948a7bdab0aa9970 coverage-3.2b3 -3e21d5dc7e9d9c64db56d84b245618ad5c1a4743 coverage-3.2b4 -e3a46ca12bda98e81329300994ca7573dc3af86e coverage-3.2 -e3a46ca12bda98e81329300994ca7573dc3af86e coverage-3.2 -2e1628da1ffce31ed5c58bcad657882de282c858 coverage-3.2 -f63624fedea89b2acb361e1d7d6c5375ebced2aa coverage-3.3 -91bf9589714b14ac4a2ab5995635736994b97348 coverage-3.3.1 -7983cc5256eafaa6c6c3cdb05266493b4c1f991a coverage-3.4b1 -fcc4407bece6ca1266c91734bfdc657b66e10290 coverage-3.4b2 -25f2c1579f1c9539b2c140b86f6c00cbfbf4c565 coverage-3.4 -b7db1026817a4ae98c58c561f6ef79e95d0e13f9 coverage-3.5b1 -7be6416f60b90b75d8c81e5369612b7f48dc26cd coverage-3.5 -9229ab7b00f9e6542b44e788916f4117bd27cfdb coverage-3.5.1b1 -465bedd8af1f79a61f6dda018f6aefe97a79ae4d coverage-3.5.1 -afc3c2ffdb323bf059f136c4c88199899ed93d36 coverage-3.5.2b1 -4eb95ddb88496eccf5f399ba69ca1769baf29dfd coverage-3.5.2 -5429bbb25e8f124f5401cce1effa3089dd792711 coverage-3.5.3 -3483c970350aedb2c8fbe33def962041628f0ba6 coverage-3.6b1 -44d084b18bf0b96f49964df344c1dcaee3802d2e coverage-3.6b2 -773375910ceaaebfce4b5badc584559bf139c862 coverage-3.6b3 -af20c6543c226fbc9deeba4a6d4114641374734a coverage-3.6 -092c0be6d011b6abf85b11476e3b548ed4d5b4c3 coverage-3.7 -41932e001f215955dff895a1fd2fe4133abb89dc coverage-3.7.1 -175fd36ea47ea668066965c80343e1de34f2a94a coverage-4.0a1 -3b97c7c3178f0b88f1ee98f4c58ad00ca3d8e3b4 coverage-4.0a2 -eec8e928880df1beafdf7d4bea87f784375b35d7 coverage-4.0a3 -989f42e9eb11b5864e5746cea675d95ff4cf645d coverage-4.0a4 -00a61f028fcb000ae2cbc77bfbe0ac4cfacfab65 coverage-4.0a5 -90debbdf56495e6c0422ceb5f53f8550d2ad86bf coverage-4.0a6 -825fb6db681e9cfb227f09adbee50b881a3380e2 coverage-4.0b1 -9ab0815227d29c03775f7f7ad6dba1b0d93db777 coverage-4.0b2 -82b0c5a85a7eb2156eebaa6b81b1f62fb4fe51b5 coverage-4.0b3 -3c3e507a247eb35251083b9528a99e50831c960f coverage-4.0 -8e727dc12de10fb8a302b04a8f2af3e00587889e coverage-4.0.1 -7428dab9307da4660878436fe71b696ca2048cf2 coverage-4.0.2 -5ab728b8fd55bd78352012a15a6541fa73dd9e2c coverage-4.0.3 -166fbccc94fb32e52a18508c5422ddae5d5184b9 coverage-4.1b1 -05377ccfe33a83068962a9010037db2d3ebdfa1a coverage-4.1b2 -4984b5b2273622a98d6e4b7de6461bbf6fae41e3 coverage-4.1b3 -1b57373355d1d0a2de258655c47412698d5f061a coverage-4.1 -c84cdae01ca45f85e396c65c20e0234e89ef1785 coverage-4.2b1 -d96ff67e4ef7d0c5e787ed5aafbc1b5a04f6e97a coverage-4.2 -e3e18c651fa059c909c25992d5b7a6db371de09b coverage-4.3 -6ecabe2f7a8d8676a05585bffe19f88b195698ae coverage-4.3.1 -dd2d866194d2eca05862230e6003c6e04fc2fdc0 coverage-4.3.2 -3714d7c42000deafd2fb2034fc8eee6a9ec4c280 coverage-4.3.3 -19d0627fca7a7e76c3c27df1d70ce7f536f1ff23 coverage-4.3.4 -30ab67f19960fafd836c89a08b5f7ee1510b8c03 coverage-4.4b1 -8ad6fa0931cd5eb9b7bb8e6dec6b066d2988fc8f coverage-4.4 -ed196840b79136f17ab493699ec83dcf7dbfe973 coverage-4.4.1 -b65ae46a6504b8d577e967bd3fdcfcaceec95528 coverage-4.4.2 -102b2250a123537e640cd014f5df281822e79cec coverage-4.5 -dda8b38e71d0bd2bde79d644f7265e1c02ce02f9 coverage-4.5.1 -865c64d99227b40e9f92586f63f2b61ebbe12d48 coverage-5.0a1 From d1e3d587ae8d9bbf4e093b649498746a7673a173 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jun 2018 08:39:37 -0400 Subject: [PATCH 081/952] Change url in the distribution to GitHub --- setup.py | 2 +- tests/test_setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 10e509a62..accfd18fa 100644 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ keywords='code coverage testing', license='Apache 2.0', classifiers=classifier_list, - url="https://bitbucket.org/ned/coveragepy", + url="https://github.com/nedbat/coveragepy", python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4", ) diff --git a/tests/test_setup.py b/tests/test_setup.py index 4467d2092..d786ca051 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -28,7 +28,7 @@ def test_metadata(self): out = output.splitlines() self.assertIn("measurement", out[0]) self.assertEqual(coverage.__version__, out[1]) - self.assertIn("bitbucket.org/ned/coveragepy", out[2]) + self.assertIn("github.com/nedbat/coveragepy", out[2]) self.assertIn("Ned Batchelder", out[3]) def test_more_metadata(self): From 8546117d2b70087f08c7fe54791461be254a98ef Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jun 2018 10:20:56 -0400 Subject: [PATCH 082/952] bom.py lost its BOM. Fix it. --- tests/test_html.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_html.py b/tests/test_html.py index b6f6f9eb8..4abe18ef8 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -640,6 +640,12 @@ def test_bom(self): self.output_dir("out/bom") with change_dir("src"): + # It's important that the source file really have a BOM, which can + # get lost, so check that it's really there. + with open("bom.py", "rb") as f: + first_three = f.read(3) + assert first_three == b"\xef\xbb\xbf" + # pylint: disable=import-error cov = coverage.Coverage() cov.start() From b6d4e7f3f8935aedffc606476a1ae2875522b75f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jun 2018 10:27:29 -0400 Subject: [PATCH 083/952] Explain some testing environment variables --- doc/contributing.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/contributing.rst b/doc/contributing.rst index d83175918..71fa6937e 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -128,6 +128,21 @@ To run just a few tests, you can use `pytest test selectors`_:: These command run the tests in one file, one class, and just one test, respectively. +You can also affect the test runs with environment variables: + +- COVERAGE_NO_PYTRACER disables the Python tracer if you only want to run the + CTracer tests. + +- COVERAGE_NO_CTRACER disables the C tracer if you only want to run the + PyTracer tests. + +- COVEGE_AST_DUMP will dump the AST tree as it is being used during code + parsing. + +- COVERAGE_KEEP_OUTPUT will save the output files that were generated by the + gold-file tests, ones that compare output files to saved gold files. + + Of course, run all the tests on every version of Python you have, before submitting a change. From 82ff458cf6d628166701ef20da13c971166cb1d7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jun 2018 12:46:24 -0400 Subject: [PATCH 084/952] Simplify a format --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index fe99f6300..a310c5a69 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -108,7 +108,7 @@ install: # Make a python3.4.bat file in the current directory so that tox will find it # and python3.4 will mean what we want it to. - - "python -c \"import os; open('python{0}.{1}.bat'.format(*os.environ['TOXENV'][2:]), 'w').write('@{0}\\\\python \\x25*\\n'.format(os.environ['PYTHON']))\"" + - "python -c \"import os; open('python{}.{}.bat'.format(*os.environ['TOXENV'][2:]), 'w').write('@{}\\\\python \\x25*\\n'.format(os.environ['PYTHON']))\"" build_script: # If not a metacov job, then build wheels and .exe installers. From 01d8d1fcb554896f7004e03556231acaee4c2f4b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jun 2018 13:45:16 -0400 Subject: [PATCH 085/952] Try forcing an explicit pip version --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a310c5a69..cf3f68829 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,7 +99,7 @@ install: # Upgrade to the latest version of pip to avoid it displaying warnings # about it being out of date. - - "python -m pip install --disable-pip-version-check --user --upgrade pip" + - "python -m pip install --disable-pip-version-check --user pip==10.0.1" # And upgrade virtualenv to get the latest pip inside .tox virtualenvs. - "python -m pip install --disable-pip-version-check --user --upgrade virtualenv" From a0f99e8ab4161dfb23c08defdc581e8e77f9b7ff Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jun 2018 13:49:58 -0400 Subject: [PATCH 086/952] Try Appveyor without --user --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index cf3f68829..8b9e1b619 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,9 +99,9 @@ install: # Upgrade to the latest version of pip to avoid it displaying warnings # about it being out of date. - - "python -m pip install --disable-pip-version-check --user pip==10.0.1" + - "python -m pip install --disable-pip-version-check --upgrade pip" # And upgrade virtualenv to get the latest pip inside .tox virtualenvs. - - "python -m pip install --disable-pip-version-check --user --upgrade virtualenv" + - "python -m pip install --disable-pip-version-check --upgrade virtualenv" # Install requirements. - "%CMD_IN_ENV% pip install -r requirements/ci.pip" From 14b311b7783be3bc2823f17e003d8b9c6efbd3ab Mon Sep 17 00:00:00 2001 From: cclauss Date: Mon, 25 Jun 2018 11:20:03 +0200 Subject: [PATCH 087/952] print() is a function in Python 3 --- lab/branches.py | 26 +++++++++++++------------- lab/run_trace.py | 4 ++-- lab/show_platform.py | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lab/branches.py b/lab/branches.py index 5a35f1bd1..c2b838dda 100644 --- a/lab/branches.py +++ b/lab/branches.py @@ -13,11 +13,11 @@ def my_function(x): i = 0 while True: - print "In while True" + print("In while True") if i > 0: break i += 1 - print "Left the True loop" + print("Left the True loop") # Notice that "while 1" also has this problem. Even though the compiler # knows there's no computation at the top of the loop, it's still expressed @@ -25,11 +25,11 @@ def my_function(x): i = 0 while 1: - print "In while 1" + print("In while 1") if i > 0: break i += 1 - print "Left the 1 loop" + print("Left the 1 loop") # Coverage.py lets developers exclude lines that they know will not be # executed. So far, the branch coverage doesn't use all that information @@ -40,9 +40,9 @@ def my_function(x): if x < 1000: # This branch is always taken - print "x is reasonable" + print("x is reasonable") else: # pragma: nocover - print "this never happens" + print("this never happens") # try-except structures are complex branches. An except clause with a # type is a three-way branch: there could be no exception, there could be @@ -57,9 +57,9 @@ def my_function(x): if y % 2: raise ValueError("y is odd!") except ValueError: - print "y must have been odd" - print "done with y" - print "done with 1, 2" + print("y must have been odd") + print("done with y") + print("done with 1, 2") # Another except clause, but this time all three cases are executed. No # partial lines are shown: @@ -71,11 +71,11 @@ def my_function(x): if y == 0: raise Exception("zero!") except ValueError: - print "y must have been odd" + print("y must have been odd") except: - print "y is something else" - print "done with y" - print "done with 0, 1, 2" + print("y is something else") + print("done with y") + print("done with 0, 1, 2") my_function(1) diff --git a/lab/run_trace.py b/lab/run_trace.py index ea9d9bb49..27c24a1da 100644 --- a/lab/run_trace.py +++ b/lab/run_trace.py @@ -14,13 +14,13 @@ def trace(frame, event, arg): # This can happen when Python is shutting down. return None - print "%s%s %s %d @%d" % ( + print("%s%s %s %d @%d" % ( " " * nest, event, os.path.basename(frame.f_code.co_filename), frame.f_lineno, frame.f_lasti, - ) + )) if event == 'call': nest += 1 diff --git a/lab/show_platform.py b/lab/show_platform.py index 76122d58f..e4f4dc2a7 100644 --- a/lab/show_platform.py +++ b/lab/show_platform.py @@ -13,4 +13,4 @@ n += "()" except: continue - print "%30s: %r" % (n, v) + print("%30s: %r" % (n, v)) From de0d4bdf03edbf5a95a71fb94352afd7810a1b18 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 25 Jun 2018 06:47:30 -0400 Subject: [PATCH 088/952] I guess it's travis.com now? --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 5072e84da..9898276c6 100644 --- a/README.rst +++ b/README.rst @@ -73,8 +73,8 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. .. _NOTICE.txt: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -.. |ci-status| image:: https://travis-ci.org/nedbat/coveragepy.svg?branch=master - :target: https://travis-ci.org/nedbat/coveragepy +.. |ci-status| image:: https://travis-ci.com/nedbat/coveragepy.svg?branch=master + :target: https://travis-ci.com/nedbat/coveragepy :alt: Build status .. |win-ci-status| image:: https://ci.appveyor.com/api/projects/status/kmeqpdje7h9r6vsf/branch/master?svg=true :target: https://ci.appveyor.com/project/nedbat/coveragepy From 7a099a2204e6863824f11432c807442f3ad5306c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 26 Jun 2018 06:49:36 -0400 Subject: [PATCH 089/952] Explain the lab directory --- lab/README.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 lab/README.txt diff --git a/lab/README.txt b/lab/README.txt new file mode 100644 index 000000000..3263667a4 --- /dev/null +++ b/lab/README.txt @@ -0,0 +1,4 @@ +The lab directory is not part of the installed coverage.py code. These programs +are tools I have used while diagnosing problems, investigating functionality, +and so on. They are not guaranteed to work, or to be suitable for any given +purpose. If you find them useful, enjoy! From e78bbdd5cd402b0542620629f7093fddede2fd32 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 27 Jun 2018 06:59:02 -0400 Subject: [PATCH 090/952] .hgignore is gone, no need to check it. --- igor.py | 1 - 1 file changed, 1 deletion(-) diff --git a/igor.py b/igor.py index f7ae6e66b..3eca875a0 100644 --- a/igor.py +++ b/igor.py @@ -294,7 +294,6 @@ def check_files(root, patterns, **kwargs): check_file("setup.py") check_file("igor.py") check_file("Makefile") - check_file(".hgignore") check_file(".travis.yml") check_files(".", ["*.rst", "*.txt"]) check_files(".", ["*.pip"]) From f9dcef3aa4d2b1858b695b552b016b1c71fd526a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 27 Jun 2018 08:24:57 -0400 Subject: [PATCH 091/952] Add a header to make diffs more readable --- tests/test_farm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_farm.py b/tests/test_farm.py index c9969c853..1f9a56d7a 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -249,7 +249,9 @@ def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scru text_diff.append('%s != %s' % (left_file, right_file)) left = left.splitlines() right = right.splitlines() + print(":::: diff {!r} and {!r}".format(left_file, right_file)) print("\n".join(difflib.Differ().compare(left, right))) + print(":::: end diff {!r} and {!r}".format(left_file, right_file)) assert not text_diff, "Files differ: %s" % '\n'.join(text_diff) if not left_extra: From e3150cdb0c4d3c093d16b8505296295c36aeb353 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 27 Jun 2018 08:25:14 -0400 Subject: [PATCH 092/952] Compare HTML more strictly --- tests/test_html.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/test_html.py b/tests/test_html.py index 4abe18ef8..7a6500bf5 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -564,6 +564,18 @@ def test_cant_find_static_files(self): cov.html_report() +def compare_html(dir1, dir2): + """Specialized compare function for HTML files.""" + scrubs = [ + (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), + (r'coverage.py v[\d.abc]+', 'coverage.py vVER'), + (r'created at \d\d\d\d-\d\d-\d\d \d\d:\d\d', 'created at DATE'), + # Some words are identifiers in one version, keywords in another. + (r'(print|True|False)', 'XXX'), + ] + return compare(dir1, dir2, file_pattern="*.html", scrubs=scrubs) + + class HtmlGoldTests(CoverageGoldTest): """Tests of HTML reporting that use gold files.""" @@ -580,7 +592,7 @@ def test_a(self): cov.stop() # pragma: nested cov.html_report(a, directory='../out/a') - compare("gold_a", "out/a", size_within=10, file_pattern="*.html") + compare_html("gold_a", "out/a") contains( "out/a/a_py.html", ('if 1 ' @@ -607,7 +619,7 @@ def test_b_branch(self): cov.stop() # pragma: nested cov.html_report(b, directory="../out/b_branch") - compare("gold_b_branch", "out/b_branch", size_within=10, file_pattern="*.html") + compare_html("gold_b_branch", "out/b_branch") contains( "out/b_branch/b_py.html", ('if x ' @@ -653,7 +665,7 @@ def test_bom(self): cov.stop() # pragma: nested cov.html_report(bom, directory="../out/bom") - compare("gold_bom", "out/bom", size_within=10, file_pattern="*.html") + compare_html("gold_bom", "out/bom") contains( "out/bom/bom_py.html", '"3×4 = 12, ÷2 = 6±0"', @@ -670,7 +682,7 @@ def test_isolatin1(self): cov.stop() # pragma: nested cov.html_report(isolatin1, directory="../out/isolatin1") - compare("gold_isolatin1", "out/isolatin1", size_within=10, file_pattern="*.html") + compare_html("gold_isolatin1", "out/isolatin1") contains( "out/isolatin1/isolatin1_py.html", '"3×4 = 12, ÷2 = 6±0"', @@ -687,7 +699,7 @@ def test_omit_1(self): cov.stop() # pragma: nested cov.html_report(directory="../out/omit_1") - compare("gold_omit_1", "out/omit_1", size_within=10, file_pattern="*.html") + compare_html("gold_omit_1", "out/omit_1") def test_omit_2(self): self.output_dir("out/omit_2") @@ -700,7 +712,7 @@ def test_omit_2(self): cov.stop() # pragma: nested cov.html_report(directory="../out/omit_2", omit=["m1.py"]) - compare("gold_omit_2", "out/omit_2", size_within=10, file_pattern="*.html") + compare_html("gold_omit_2", "out/omit_2") def test_omit_3(self): self.output_dir("out/omit_3") @@ -713,7 +725,7 @@ def test_omit_3(self): cov.stop() # pragma: nested cov.html_report(directory="../out/omit_3", omit=["m1.py", "m2.py"]) - compare("gold_omit_3", "out/omit_3", size_within=10, file_pattern="*.html") + compare_html("gold_omit_3", "out/omit_3") def test_omit_4(self): self.output_dir("out/omit_4") @@ -726,7 +738,7 @@ def test_omit_4(self): cov.stop() # pragma: nested cov.html_report(directory="../out/omit_4") - compare("gold_omit_4", "out/omit_4", size_within=10, file_pattern="*.html") + compare_html("gold_omit_4", "out/omit_4") def test_omit_5(self): self.output_dir("out/omit_5") @@ -739,7 +751,7 @@ def test_omit_5(self): cov.stop() # pragma: nested cov.html_report() - compare("gold_omit_5", "out/omit_5", size_within=10, file_pattern="*.html") + compare_html("gold_omit_5", "out/omit_5") def test_other(self): self.output_dir("out/other") @@ -757,7 +769,7 @@ def test_other(self): for p in glob.glob("out/other/*_other_py.html"): os.rename(p, "out/other/blah_blah_other_py.html") - compare("gold_other", "out/other", size_within=10, file_pattern="*.html") + compare_html("gold_other", "out/other") contains( "out/other/index.html", 'here.py', @@ -775,7 +787,7 @@ def test_partial(self): cov.stop() # pragma: nested cov.html_report(partial, directory="../out/partial") - compare("gold_partial", "out/partial", size_within=10, file_pattern="*.html") + compare_html("gold_partial", "out/partial") contains( "out/partial/partial_py.html", '

', @@ -806,7 +818,7 @@ def test_styled(self): cov.stop() # pragma: nested cov.html_report(a, directory="../out/styled", extra_css="extra.css") - compare("gold_styled", "out/styled", size_within=10, file_pattern="*.html") + compare_html("gold_styled", "out/styled") compare("gold_styled", "out/styled", size_within=10, file_pattern="*.css") contains( "out/styled/a_py.html", @@ -859,7 +871,7 @@ def test_unicode(self): cov.stop() # pragma: nested cov.html_report(unicode, directory="../out/unicode") - compare("gold_unicode", "out/unicode", size_within=10, file_pattern="*.html") + compare_html("gold_unicode", "out/unicode") contains( "out/unicode/unicode_py.html", '"ʎd˙ǝbɐɹǝʌoɔ"', From 8497ebc588dce9f1727321d91239fd57ed1ca558 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 27 Jun 2018 09:54:55 -0400 Subject: [PATCH 093/952] Updated html gold files --- tests/farm/html/gold_a/a_py.html | 24 ++++----- tests/farm/html/gold_a/index.html | 6 +-- tests/farm/html/gold_b_branch/b_py.html | 50 +++++++++---------- tests/farm/html/gold_b_branch/index.html | 10 ++-- tests/farm/html/gold_isolatin1/index.html | 6 +-- .../html/gold_isolatin1/isolatin1_py.html | 20 ++++---- tests/farm/html/gold_omit_1/index.html | 6 +-- tests/farm/html/gold_omit_1/m1_py.html | 18 +++---- tests/farm/html/gold_omit_1/m2_py.html | 18 +++---- tests/farm/html/gold_omit_1/m3_py.html | 18 +++---- tests/farm/html/gold_omit_1/main_py.html | 18 +++---- tests/farm/html/gold_omit_2/index.html | 6 +-- tests/farm/html/gold_omit_2/m2_py.html | 18 +++---- tests/farm/html/gold_omit_2/m3_py.html | 18 +++---- tests/farm/html/gold_omit_2/main_py.html | 18 +++---- tests/farm/html/gold_omit_3/index.html | 6 +-- tests/farm/html/gold_omit_3/m3_py.html | 18 +++---- tests/farm/html/gold_omit_3/main_py.html | 18 +++---- tests/farm/html/gold_omit_4/index.html | 6 +-- tests/farm/html/gold_omit_4/m1_py.html | 18 +++---- tests/farm/html/gold_omit_4/m3_py.html | 18 +++---- tests/farm/html/gold_omit_4/main_py.html | 18 +++---- tests/farm/html/gold_omit_5/index.html | 6 +-- tests/farm/html/gold_omit_5/m1_py.html | 18 +++---- tests/farm/html/gold_omit_5/main_py.html | 18 +++---- .../html/gold_other/blah_blah_other_py.html | 22 ++++---- tests/farm/html/gold_other/here_py.html | 22 ++++---- tests/farm/html/gold_other/index.html | 6 +-- tests/farm/html/gold_partial/index.html | 22 ++++---- tests/farm/html/gold_partial/partial_py.html | 48 ++++++++++-------- tests/farm/html/gold_styled/a_py.html | 24 ++++----- tests/farm/html/gold_styled/index.html | 6 +-- tests/farm/html/gold_unicode/index.html | 6 +-- tests/farm/html/gold_unicode/unicode_py.html | 22 ++++---- 34 files changed, 293 insertions(+), 283 deletions(-) diff --git a/tests/farm/html/gold_a/a_py.html b/tests/farm/html/gold_a/a_py.html index da7006154..b05b2fd13 100644 --- a/tests/farm/html/gold_a/a_py.html +++ b/tests/farm/html/gold_a/a_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -77,7 +77,7 @@

8

9

10

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -86,11 +86,11 @@

# A test file for HTML reporting by coverage.py. 

 

if 1 < 2: 

-

    # Needed a < to look at HTML entities. 

-

    a = 3 

+

# Needed a < to look at HTML entities. 

+

a = 3 

else: 

-

    a = 4 

- +

a = 4 

+ @@ -99,8 +99,8 @@

diff --git a/tests/farm/html/gold_a/index.html b/tests/farm/html/gold_a/index.html index 35507b6f6..7a88627c6 100644 --- a/tests/farm/html/gold_a/index.html +++ b/tests/farm/html/gold_a/index.html @@ -9,7 +9,7 @@ - + @@ -94,8 +94,8 @@

Coverage report: diff --git a/tests/farm/html/gold_b_branch/b_py.html b/tests/farm/html/gold_b_branch/b_py.html index 839e4c98f..5f6c43fa6 100644 --- a/tests/farm/html/gold_b_branch/b_py.html +++ b/tests/farm/html/gold_b_branch/b_py.html @@ -44,22 +44,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -101,7 +101,7 @@

30

31

32

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -110,33 +110,33 @@

# A test file for HTML reporting by coverage.py. 

 

def one(x): 

-

    # This will be a branch that misses the else. 

-

11    if x < 2: 

-

        a = 3 

-

    else: 

-

        a = 4 

+

# This will be a branch that misses the else. 

+

8 ↛ 11line 8 didn't jump to line 11, because the condition on line 8 was never false if x < 2: 

+

a = 3 

+

else: 

+

a = 4 

 

one(1) 

 

def two(x): 

-

    # A missed else that branches to "exit" 

-

exit    if x: 

-

        a = 5 

+

# A missed else that branches to "exit" 

+

17 ↛ exitline 17 didn't return from function 'two', because the condition on line 17 was never false if x: 

+

a = 5 

 

two(1) 

 

def three(): 

-

    try: 

-

        # This if has two branches, *neither* one taken. 

-

26   28        if name_error_this_variable_doesnt_exist: 

-

            a = 1 

-

        else: 

-

            a = 2 

-

    except: 

-

        pass 

+

try: 

+

# This if has two branches, *neither* one taken. 

+

25 ↛ 26,   25 ↛ 282 missed branches: 1) line 25 didn't jump to line 26, because the condition on line 25 was never true, 2) line 25 didn't jump to line 28, because the condition on line 25 was never false if name_error_this_variable_doesnt_exist: 

+

a = 1 

+

else: 

+

a = 2 

+

except: 

+

pass 

 

three() 

- + @@ -145,8 +145,8 @@

diff --git a/tests/farm/html/gold_b_branch/index.html b/tests/farm/html/gold_b_branch/index.html index ebc3a1069..0125efdc8 100644 --- a/tests/farm/html/gold_b_branch/index.html +++ b/tests/farm/html/gold_b_branch/index.html @@ -1,3 +1,6 @@ + + + @@ -6,7 +9,7 @@ - + @@ -81,7 +84,7 @@

Coverage report: - b + b.py 17 3 0 @@ -103,7 +106,8 @@

Coverage report: diff --git a/tests/farm/html/gold_isolatin1/index.html b/tests/farm/html/gold_isolatin1/index.html index ee49cc5e0..390df0c70 100644 --- a/tests/farm/html/gold_isolatin1/index.html +++ b/tests/farm/html/gold_isolatin1/index.html @@ -9,7 +9,7 @@ - + @@ -94,8 +94,8 @@

Coverage report: diff --git a/tests/farm/html/gold_isolatin1/isolatin1_py.html b/tests/farm/html/gold_isolatin1/isolatin1_py.html index 1c0f79d9c..83b2e3309 100644 --- a/tests/farm/html/gold_isolatin1/isolatin1_py.html +++ b/tests/farm/html/gold_isolatin1/isolatin1_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -75,7 +75,7 @@

6

7

8

- +

# -*- coding: iso8859-1 -*- 

@@ -84,9 +84,9 @@

 

# A Python source file in another encoding. 

 

-

math = "3×4 = 12, ÷2 = 6±0" 

+

math = "3×4 = 12, ÷2 = 6±0" 

assert len(math) == 18 

- + @@ -95,8 +95,8 @@

diff --git a/tests/farm/html/gold_omit_1/index.html b/tests/farm/html/gold_omit_1/index.html index 404d6b6ad..0fa38d16c 100644 --- a/tests/farm/html/gold_omit_1/index.html +++ b/tests/farm/html/gold_omit_1/index.html @@ -9,7 +9,7 @@ - + @@ -121,8 +121,8 @@

Coverage report: diff --git a/tests/farm/html/gold_omit_1/m1_py.html b/tests/farm/html/gold_omit_1/m1_py.html index 5d9541453..ebe9851d3 100644 --- a/tests/farm/html/gold_omit_1/m1_py.html +++ b/tests/farm/html/gold_omit_1/m1_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m1a = 1 

m1b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_1/m2_py.html b/tests/farm/html/gold_omit_1/m2_py.html index 12fc68486..b05205aec 100644 --- a/tests/farm/html/gold_omit_1/m2_py.html +++ b/tests/farm/html/gold_omit_1/m2_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m2a = 1 

m2b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_1/m3_py.html b/tests/farm/html/gold_omit_1/m3_py.html index e5b40653d..94860ee01 100644 --- a/tests/farm/html/gold_omit_1/m3_py.html +++ b/tests/farm/html/gold_omit_1/m3_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m3a = 1 

m3b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_1/main_py.html b/tests/farm/html/gold_omit_1/main_py.html index 7ae0d5f25..d4fd121a2 100644 --- a/tests/farm/html/gold_omit_1/main_py.html +++ b/tests/farm/html/gold_omit_1/main_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -80,7 +80,7 @@

11

12

13

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,7 +96,7 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

- + @@ -105,8 +105,8 @@

diff --git a/tests/farm/html/gold_omit_2/index.html b/tests/farm/html/gold_omit_2/index.html index 01c13e316..41006272b 100644 --- a/tests/farm/html/gold_omit_2/index.html +++ b/tests/farm/html/gold_omit_2/index.html @@ -9,7 +9,7 @@ - + @@ -112,8 +112,8 @@

Coverage report: diff --git a/tests/farm/html/gold_omit_2/m2_py.html b/tests/farm/html/gold_omit_2/m2_py.html index 12fc68486..b05205aec 100644 --- a/tests/farm/html/gold_omit_2/m2_py.html +++ b/tests/farm/html/gold_omit_2/m2_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m2a = 1 

m2b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_2/m3_py.html b/tests/farm/html/gold_omit_2/m3_py.html index e5b40653d..94860ee01 100644 --- a/tests/farm/html/gold_omit_2/m3_py.html +++ b/tests/farm/html/gold_omit_2/m3_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m3a = 1 

m3b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_2/main_py.html b/tests/farm/html/gold_omit_2/main_py.html index 7ae0d5f25..d4fd121a2 100644 --- a/tests/farm/html/gold_omit_2/main_py.html +++ b/tests/farm/html/gold_omit_2/main_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -80,7 +80,7 @@

11

12

13

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,7 +96,7 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

- + @@ -105,8 +105,8 @@

diff --git a/tests/farm/html/gold_omit_3/index.html b/tests/farm/html/gold_omit_3/index.html index 791929113..7ab3e451a 100644 --- a/tests/farm/html/gold_omit_3/index.html +++ b/tests/farm/html/gold_omit_3/index.html @@ -9,7 +9,7 @@ - + @@ -103,8 +103,8 @@

Coverage report: diff --git a/tests/farm/html/gold_omit_3/m3_py.html b/tests/farm/html/gold_omit_3/m3_py.html index e5b40653d..94860ee01 100644 --- a/tests/farm/html/gold_omit_3/m3_py.html +++ b/tests/farm/html/gold_omit_3/m3_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m3a = 1 

m3b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_3/main_py.html b/tests/farm/html/gold_omit_3/main_py.html index 7ae0d5f25..d4fd121a2 100644 --- a/tests/farm/html/gold_omit_3/main_py.html +++ b/tests/farm/html/gold_omit_3/main_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -80,7 +80,7 @@

11

12

13

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,7 +96,7 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

- + @@ -105,8 +105,8 @@

diff --git a/tests/farm/html/gold_omit_4/index.html b/tests/farm/html/gold_omit_4/index.html index 0e52f497e..2a1da8e5e 100644 --- a/tests/farm/html/gold_omit_4/index.html +++ b/tests/farm/html/gold_omit_4/index.html @@ -9,7 +9,7 @@ - + @@ -112,8 +112,8 @@

Coverage report: diff --git a/tests/farm/html/gold_omit_4/m1_py.html b/tests/farm/html/gold_omit_4/m1_py.html index 5d9541453..ebe9851d3 100644 --- a/tests/farm/html/gold_omit_4/m1_py.html +++ b/tests/farm/html/gold_omit_4/m1_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m1a = 1 

m1b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_4/m3_py.html b/tests/farm/html/gold_omit_4/m3_py.html index e5b40653d..94860ee01 100644 --- a/tests/farm/html/gold_omit_4/m3_py.html +++ b/tests/farm/html/gold_omit_4/m3_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m3a = 1 

m3b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_4/main_py.html b/tests/farm/html/gold_omit_4/main_py.html index 7ae0d5f25..d4fd121a2 100644 --- a/tests/farm/html/gold_omit_4/main_py.html +++ b/tests/farm/html/gold_omit_4/main_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -80,7 +80,7 @@

11

12

13

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,7 +96,7 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

- + @@ -105,8 +105,8 @@

diff --git a/tests/farm/html/gold_omit_5/index.html b/tests/farm/html/gold_omit_5/index.html index 509e7900c..252be5aef 100644 --- a/tests/farm/html/gold_omit_5/index.html +++ b/tests/farm/html/gold_omit_5/index.html @@ -9,7 +9,7 @@ - + @@ -103,8 +103,8 @@

Coverage report: diff --git a/tests/farm/html/gold_omit_5/m1_py.html b/tests/farm/html/gold_omit_5/m1_py.html index 5d9541453..ebe9851d3 100644 --- a/tests/farm/html/gold_omit_5/m1_py.html +++ b/tests/farm/html/gold_omit_5/m1_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -72,7 +72,7 @@

3

4

5

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,7 +80,7 @@

 

m1a = 1 

m1b = 2 

- + @@ -89,8 +89,8 @@

diff --git a/tests/farm/html/gold_omit_5/main_py.html b/tests/farm/html/gold_omit_5/main_py.html index 7ae0d5f25..b7c6eb999 100644 --- a/tests/farm/html/gold_omit_5/main_py.html +++ b/tests/farm/html/gold_omit_5/main_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -80,7 +80,7 @@

11

12

13

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,7 +96,7 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

- + @@ -105,8 +105,8 @@

diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index 4eda1194a..185949f28 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -74,17 +74,17 @@

5

6

7

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

-

# A file in another directory.  We're checking that it ends up in the 

+

# A file in another directory. We're checking that it ends up in the 

# HTML report. 

 

-

print("This is the other src!") 

- +

print("This is the other src!") 

+ @@ -93,8 +93,8 @@

diff --git a/tests/farm/html/gold_other/here_py.html b/tests/farm/html/gold_other/here_py.html index 82fc7cbae..30114ca14 100644 --- a/tests/farm/html/gold_other/here_py.html +++ b/tests/farm/html/gold_other/here_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -78,7 +78,7 @@

9

10

11

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -89,10 +89,10 @@

import other 

 

if 1 < 2: 

-

    h = 3 

+

h = 3 

else: 

-

    h = 4 

- +

h = 4 

+ @@ -101,8 +101,8 @@

diff --git a/tests/farm/html/gold_other/index.html b/tests/farm/html/gold_other/index.html index 9a8d72de6..f116d5120 100644 --- a/tests/farm/html/gold_other/index.html +++ b/tests/farm/html/gold_other/index.html @@ -9,7 +9,7 @@ - + @@ -103,8 +103,8 @@

Coverage report: diff --git a/tests/farm/html/gold_partial/index.html b/tests/farm/html/gold_partial/index.html index 40ffcc4c7..f4afed054 100644 --- a/tests/farm/html/gold_partial/index.html +++ b/tests/farm/html/gold_partial/index.html @@ -9,7 +9,7 @@ - + @@ -71,28 +71,28 @@

Coverage report: Total - 8 - 0 + 9 0 + 1 - 4 + 2 0 - 100% + 100% partial.py - 8 - 0 + 9 0 + 1 - 4 + 2 0 - 100% + 100% @@ -106,8 +106,8 @@

Coverage report: diff --git a/tests/farm/html/gold_partial/partial_py.html b/tests/farm/html/gold_partial/partial_py.html index 2d763fa3a..73b80adbd 100644 --- a/tests/farm/html/gold_partial/partial_py.html +++ b/tests/farm/html/gold_partial/partial_py.html @@ -30,10 +30,10 @@

Coverage for partial.py : Show keyboard shortcuts

- 8 statements   - 8 run + 9 statements   + 9 run 0 missing - 0 excluded + 1 excluded 0 partial @@ -44,22 +44,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -90,31 +90,37 @@

19

20

21

- +

22

+

23

+

24

+

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

 

-

# partial branches 

+

# partial branches and excluded lines 

 

-

a = 3 

+

a = 6 

 

-

while True: 

-

    break 

+

while True: 

+

break 

 

while 1: 

-

    break 

+

break 

 

-

while a:        # pragma: no branch 

-

    break 

+

while a: # pragma: no branch 

+

break 

 

if 0: 

-

    never_happen() 

+

never_happen() 

 

if 1: 

-

    a = 13 

- +

a = 21 

+

 

+

if a == 23: 

+

raise AssertionError("Can't") 

+ @@ -123,8 +129,8 @@

diff --git a/tests/farm/html/gold_styled/a_py.html b/tests/farm/html/gold_styled/a_py.html index af1d9121a..251cb8100 100644 --- a/tests/farm/html/gold_styled/a_py.html +++ b/tests/farm/html/gold_styled/a_py.html @@ -44,22 +44,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -79,7 +79,7 @@

8

9

10

- +

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -88,11 +88,11 @@

# A test file for HTML reporting by coverage.py. 

 

if 1 < 2: 

-

    # Needed a < to look at HTML entities. 

-

    a = 3 

+

# Needed a < to look at HTML entities. 

+

a = 3 

else: 

-

    a = 4 

- +

a = 4 

+ @@ -101,8 +101,8 @@

diff --git a/tests/farm/html/gold_styled/index.html b/tests/farm/html/gold_styled/index.html index 05a8fe21b..719fb9032 100644 --- a/tests/farm/html/gold_styled/index.html +++ b/tests/farm/html/gold_styled/index.html @@ -11,7 +11,7 @@ - + @@ -96,8 +96,8 @@

Coverage report: diff --git a/tests/farm/html/gold_unicode/index.html b/tests/farm/html/gold_unicode/index.html index 6d8268d8a..fe01fbd5d 100644 --- a/tests/farm/html/gold_unicode/index.html +++ b/tests/farm/html/gold_unicode/index.html @@ -9,7 +9,7 @@ - + @@ -94,8 +94,8 @@

Coverage report: diff --git a/tests/farm/html/gold_unicode/unicode_py.html b/tests/farm/html/gold_unicode/unicode_py.html index 5a6f3ff9f..8ce44fdd7 100644 --- a/tests/farm/html/gold_unicode/unicode_py.html +++ b/tests/farm/html/gold_unicode/unicode_py.html @@ -42,22 +42,22 @@

Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

@@ -75,7 +75,7 @@

6

7

8

- +

# -*- coding: utf-8 -*- 

@@ -84,9 +84,9 @@

 

# A Python source file with exotic characters. 

 

-

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

-

surrogate = "db40,dd00: x󠄀" 

- +

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

+

surrogate = "db40,dd00: x󠄀" 

+ @@ -95,8 +95,8 @@

From 9334303596d86d62c634c958d3fe6538b64fe4d6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 27 Jun 2018 10:55:47 -0400 Subject: [PATCH 094/952] Gold files can be in versioned subdirectories --- tests/test_farm.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/test_farm.py b/tests/test_farm.py index 1f9a56d7a..9fb48e85d 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -179,9 +179,37 @@ def run(cmds, rundir="src", outfile=None): fout.close() +def versioned_directory(d): + """Find a subdirectory of d specific to the Python version. + + For example, on Python 3.6.4 rc 1, it returns the first of these + directories that exists:: + + d/3.6.4.candidate.1 + d/3.6.4.candidate + d/3.6.4 + d/3.6 + d/3 + d + + Returns: a string, the path to an existing directory. + + """ + ver_parts = list(map(str, sys.version_info)) + for nparts in range(len(ver_parts), -1, -1): + version = ".".join(ver_parts[:nparts]) + subdir = os.path.join(d, version) + if os.path.exists(subdir): + return subdir + raise Exception("Directory missing: {}".format(d)) # pragma: only failure + + def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scrubs=None): """Compare files matching `file_pattern` in `dir1` and `dir2`. + A version-specific subdirectory of `dir1` or `dir2` will be used if + it exists. + `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. @@ -198,8 +226,8 @@ def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scru matches. """ - assert os.path.exists(dir1), "Left directory missing: %s" % dir1 - assert os.path.exists(dir2), "Right directory missing: %s" % dir2 + dir1 = versioned_directory(dir1) + dir2 = versioned_directory(dir2) dc = filecmp.dircmp(dir1, dir2) diff_files = fnmatch_list(dc.diff_files, file_pattern) From 18a4324a05afd552f99396b18d88ddcb025c9908 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 27 Jun 2018 10:56:50 -0400 Subject: [PATCH 095/952] Updated gold files for bom.py --- tests/farm/html/gold_bom/2/bom_py.html | 117 +++++++++++++++++++++++++ tests/farm/html/gold_bom/2/index.html | 104 ++++++++++++++++++++++ tests/farm/html/gold_bom/bom_py.html | 64 ++++++++------ tests/farm/html/gold_bom/index.html | 10 ++- 4 files changed, 265 insertions(+), 30 deletions(-) create mode 100644 tests/farm/html/gold_bom/2/bom_py.html create mode 100644 tests/farm/html/gold_bom/2/index.html diff --git a/tests/farm/html/gold_bom/2/bom_py.html b/tests/farm/html/gold_bom/2/bom_py.html new file mode 100644 index 000000000..5ef92d300 --- /dev/null +++ b/tests/farm/html/gold_bom/2/bom_py.html @@ -0,0 +1,117 @@ + + + + + + + + + + + Coverage for bom.py: 71% + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+ +
+

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

+

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

+

 

+

# A Python source file in utf-8, with BOM. 

+

math = "3×4 = 12, ÷2 = 6±0" 

+

 

+

import sys 

+

 

+

if sys.version_info >= (3, 0): 

+

assert len(math) == 18 

+

assert len(math.encode('utf-8')) == 21 

+

else: 

+

assert len(math) == 21 

+

assert len(math.decode('utf-8')) == 18 

+ +
+
+ + + + + diff --git a/tests/farm/html/gold_bom/2/index.html b/tests/farm/html/gold_bom/2/index.html new file mode 100644 index 000000000..883f74102 --- /dev/null +++ b/tests/farm/html/gold_bom/2/index.html @@ -0,0 +1,104 @@ + + + + + + + + Coverage report + + + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total72071%
bom.py72071%
+ +

+ No items found using the specified filter. +

+
+ + + + + diff --git a/tests/farm/html/gold_bom/bom_py.html b/tests/farm/html/gold_bom/bom_py.html index 127f2f451..054b17649 100644 --- a/tests/farm/html/gold_bom/bom_py.html +++ b/tests/farm/html/gold_bom/bom_py.html @@ -1,3 +1,6 @@ + + + @@ -5,7 +8,7 @@ - Coverage for bom: 71% + Coverage for bom.py: 71% @@ -20,7 +23,7 @@ -
@@ -101,7 +87,6 @@

30

31

32

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -136,20 +121,17 @@

pass 

 

three() 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_b_branch/index.html b/tests/farm/html/gold_b_branch/index.html index 0125efdc8..844f79e16 100644 --- a/tests/farm/html/gold_b_branch/index.html +++ b/tests/farm/html/gold_b_branch/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,75 +34,59 @@

Coverage report: s m x - b p - c   change column sorting

-
- - - - - - - - - -
Module statements missing excludedbranches partialcoverage
Total 17 3 06 470%
b.py 17 3 06 470%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_bom/2/bom_py.html b/tests/farm/html/gold_bom/2/bom_py.html index 5ef92d300..78c498fd1 100644 --- a/tests/farm/html/gold_bom/2/bom_py.html +++ b/tests/farm/html/gold_bom/2/bom_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for bom.py: 71% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -81,7 +68,6 @@

12

13

14

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -98,20 +84,17 @@

else: 

assert len(math) == 21 

assert len(math.decode('utf-8')) == 18 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_bom/2/index.html b/tests/farm/html/gold_bom/2/index.html index 883f74102..2d285ab15 100644 --- a/tests/farm/html/gold_bom/2/index.html +++ b/tests/farm/html/gold_bom/2/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,63 +34,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - -
Module statements missing excludedcoverage
Total 7 2 071%
bom.py 7 2 071%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_bom/bom_py.html b/tests/farm/html/gold_bom/bom_py.html index 054b17649..472f655bc 100644 --- a/tests/farm/html/gold_bom/bom_py.html +++ b/tests/farm/html/gold_bom/bom_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for bom.py: 71% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -81,7 +68,6 @@

12

13

14

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -98,20 +84,17 @@

else: 

assert len(math) == 21 

assert len(math.decode('utf-8')) == 18 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_bom/index.html b/tests/farm/html/gold_bom/index.html index cc4a55d82..0341c0d07 100644 --- a/tests/farm/html/gold_bom/index.html +++ b/tests/farm/html/gold_bom/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,63 +34,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - -
Module statements missing excludedcoverage
Total 7 2 071%
bom.py 7 2 071%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_isolatin1/index.html b/tests/farm/html/gold_isolatin1/index.html index 390df0c70..ec1253641 100644 --- a/tests/farm/html/gold_isolatin1/index.html +++ b/tests/farm/html/gold_isolatin1/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,63 +34,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - -
Module statements missing excludedcoverage
Total 2 0 0100%
isolatin1.py 2 0 0100%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_isolatin1/isolatin1_py.html b/tests/farm/html/gold_isolatin1/isolatin1_py.html index 83b2e3309..45d13f429 100644 --- a/tests/farm/html/gold_isolatin1/isolatin1_py.html +++ b/tests/farm/html/gold_isolatin1/isolatin1_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for isolatin1.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -75,7 +62,6 @@

6

7

8

-

# -*- coding: iso8859-1 -*- 

@@ -86,20 +72,17 @@

 

math = "3×4 = 12, ÷2 = 6±0" 

assert len(math) == 18 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_1/index.html b/tests/farm/html/gold_omit_1/index.html index 0fa38d16c..9ea591a47 100644 --- a/tests/farm/html/gold_omit_1/index.html +++ b/tests/farm/html/gold_omit_1/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,90 +34,72 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - - - - - -
Module statements missing excludedcoverage
Total 14 0 0100%
m1.py 2 0 0100%
m2.py 2 0 0100%
m3.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_1/m1_py.html b/tests/farm/html/gold_omit_1/m1_py.html index ebe9851d3..1557156cb 100644 --- a/tests/farm/html/gold_omit_1/m1_py.html +++ b/tests/farm/html/gold_omit_1/m1_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m1.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m1a = 1 

m1b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_1/m2_py.html b/tests/farm/html/gold_omit_1/m2_py.html index b05205aec..8f3102d11 100644 --- a/tests/farm/html/gold_omit_1/m2_py.html +++ b/tests/farm/html/gold_omit_1/m2_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m2.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m2a = 1 

m2b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_1/m3_py.html b/tests/farm/html/gold_omit_1/m3_py.html index 94860ee01..2d1e1d4c2 100644 --- a/tests/farm/html/gold_omit_1/m3_py.html +++ b/tests/farm/html/gold_omit_1/m3_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m3.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m3a = 1 

m3b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_1/main_py.html b/tests/farm/html/gold_omit_1/main_py.html index d4fd121a2..bc93b1a0c 100644 --- a/tests/farm/html/gold_omit_1/main_py.html +++ b/tests/farm/html/gold_omit_1/main_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for main.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -80,7 +67,6 @@

11

12

13

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,20 +82,17 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_2/index.html b/tests/farm/html/gold_omit_2/index.html index 41006272b..8c2576f27 100644 --- a/tests/farm/html/gold_omit_2/index.html +++ b/tests/farm/html/gold_omit_2/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,81 +34,65 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - - - -
Module statements missing excludedcoverage
Total 12 0 0100%
m2.py 2 0 0100%
m3.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_2/m2_py.html b/tests/farm/html/gold_omit_2/m2_py.html index b05205aec..8f3102d11 100644 --- a/tests/farm/html/gold_omit_2/m2_py.html +++ b/tests/farm/html/gold_omit_2/m2_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m2.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m2a = 1 

m2b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_2/m3_py.html b/tests/farm/html/gold_omit_2/m3_py.html index 94860ee01..2d1e1d4c2 100644 --- a/tests/farm/html/gold_omit_2/m3_py.html +++ b/tests/farm/html/gold_omit_2/m3_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m3.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m3a = 1 

m3b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_2/main_py.html b/tests/farm/html/gold_omit_2/main_py.html index d4fd121a2..bc93b1a0c 100644 --- a/tests/farm/html/gold_omit_2/main_py.html +++ b/tests/farm/html/gold_omit_2/main_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for main.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -80,7 +67,6 @@

11

12

13

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,20 +82,17 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_3/index.html b/tests/farm/html/gold_omit_3/index.html index 7ab3e451a..f0b32cc4f 100644 --- a/tests/farm/html/gold_omit_3/index.html +++ b/tests/farm/html/gold_omit_3/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,72 +34,58 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - -
Module statements missing excludedcoverage
Total 10 0 0100%
m3.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_3/m3_py.html b/tests/farm/html/gold_omit_3/m3_py.html index 94860ee01..2d1e1d4c2 100644 --- a/tests/farm/html/gold_omit_3/m3_py.html +++ b/tests/farm/html/gold_omit_3/m3_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m3.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m3a = 1 

m3b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_3/main_py.html b/tests/farm/html/gold_omit_3/main_py.html index d4fd121a2..bc93b1a0c 100644 --- a/tests/farm/html/gold_omit_3/main_py.html +++ b/tests/farm/html/gold_omit_3/main_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for main.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -80,7 +67,6 @@

11

12

13

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,20 +82,17 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_4/index.html b/tests/farm/html/gold_omit_4/index.html index 2a1da8e5e..7dadd229f 100644 --- a/tests/farm/html/gold_omit_4/index.html +++ b/tests/farm/html/gold_omit_4/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,81 +34,65 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - - - -
Module statements missing excludedcoverage
Total 12 0 0100%
m1.py 2 0 0100%
m3.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_4/m1_py.html b/tests/farm/html/gold_omit_4/m1_py.html index ebe9851d3..1557156cb 100644 --- a/tests/farm/html/gold_omit_4/m1_py.html +++ b/tests/farm/html/gold_omit_4/m1_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m1.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m1a = 1 

m1b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_4/m3_py.html b/tests/farm/html/gold_omit_4/m3_py.html index 94860ee01..2d1e1d4c2 100644 --- a/tests/farm/html/gold_omit_4/m3_py.html +++ b/tests/farm/html/gold_omit_4/m3_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m3.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m3a = 1 

m3b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_4/main_py.html b/tests/farm/html/gold_omit_4/main_py.html index d4fd121a2..bc93b1a0c 100644 --- a/tests/farm/html/gold_omit_4/main_py.html +++ b/tests/farm/html/gold_omit_4/main_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for main.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -80,7 +67,6 @@

11

12

13

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,20 +82,17 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_5/index.html b/tests/farm/html/gold_omit_5/index.html index 252be5aef..b9912d249 100644 --- a/tests/farm/html/gold_omit_5/index.html +++ b/tests/farm/html/gold_omit_5/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,72 +34,58 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - -
Module statements missing excludedcoverage
Total 10 0 0100%
m1.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_5/m1_py.html b/tests/farm/html/gold_omit_5/m1_py.html index ebe9851d3..1557156cb 100644 --- a/tests/farm/html/gold_omit_5/m1_py.html +++ b/tests/farm/html/gold_omit_5/m1_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m1.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -72,7 +59,6 @@

3

4

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -80,20 +66,17 @@

 

m1a = 1 

m1b = 2 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_omit_5/main_py.html b/tests/farm/html/gold_omit_5/main_py.html index b7c6eb999..bc93b1a0c 100644 --- a/tests/farm/html/gold_omit_5/main_py.html +++ b/tests/farm/html/gold_omit_5/main_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for main.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -80,7 +67,6 @@

11

12

13

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -96,20 +82,17 @@

assert m1.m1a == 1 

assert m2.m2a == 1 

assert m3.m3a == 1 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index 185949f28..4beb2a074 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for /Users/ned/coverage/trunk/tests/farm/html/othersrc/other.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -74,7 +61,6 @@

5

6

7

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -84,20 +70,17 @@

# HTML report. 

 

print("This is the other src!") 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_other/here_py.html b/tests/farm/html/gold_other/here_py.html index 30114ca14..c692a1c46 100644 --- a/tests/farm/html/gold_other/here_py.html +++ b/tests/farm/html/gold_other/here_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for here.py: 75% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -78,7 +65,6 @@

9

10

11

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -92,20 +78,17 @@

h = 3 

else: 

h = 4 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_other/index.html b/tests/farm/html/gold_other/index.html index f116d5120..27c194cf9 100644 --- a/tests/farm/html/gold_other/index.html +++ b/tests/farm/html/gold_other/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,72 +34,58 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - -
Module statements missing excludedcoverage
Total 5 1 080%
/Users/ned/coverage/trunk/tests/farm/html/othersrc/other.py 1 0 0100%
here.py 4 1 075%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_partial/index.html b/tests/farm/html/gold_partial/index.html index f4afed054..ca8919d78 100644 --- a/tests/farm/html/gold_partial/index.html +++ b/tests/farm/html/gold_partial/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,75 +34,59 @@

Coverage report: s m x - b p - c   change column sorting

-
- - - - - - - - - -
Module statements missing excludedbranches partialcoverage
Total 9 0 12 0100%
partial.py 9 0 12 0100%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_partial/partial_py.html b/tests/farm/html/gold_partial/partial_py.html index 73b80adbd..7f4ed31ad 100644 --- a/tests/farm/html/gold_partial/partial_py.html +++ b/tests/farm/html/gold_partial/partial_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for partial.py: 100% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,7 +51,6 @@

-
@@ -93,7 +79,6 @@

22

23

24

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -120,20 +105,17 @@

 

if a == 23: 

raise AssertionError("Can't") 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_styled/a_py.html b/tests/farm/html/gold_styled/a_py.html index 251cb8100..6e6f317ca 100644 --- a/tests/farm/html/gold_styled/a_py.html +++ b/tests/farm/html/gold_styled/a_py.html @@ -1,18 +1,11 @@ - - - - - Coverage for a.py: 67% - - @@ -22,26 +15,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,7 +51,6 @@

-
@@ -79,7 +65,6 @@

8

9

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

@@ -92,20 +77,17 @@

a = 3 

else: 

a = 4 

-

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_styled/index.html b/tests/farm/html/gold_styled/index.html index 719fb9032..c08815928 100644 --- a/tests/farm/html/gold_styled/index.html +++ b/tests/farm/html/gold_styled/index.html @@ -1,15 +1,10 @@ - - - Coverage report - - @@ -20,21 +15,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -44,63 +35,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - -
Module statements missing excludedcoverage
Total 3 1 067%
a.py 3 1 067%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_unicode/index.html b/tests/farm/html/gold_unicode/index.html index fe01fbd5d..c1f2fb673 100644 --- a/tests/farm/html/gold_unicode/index.html +++ b/tests/farm/html/gold_unicode/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,63 +34,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - -
Module statements missing excludedcoverage
Total 2 0 0100%
unicode.py 2 0 0100%
-

No items found using the specified filter.

- - - + \ No newline at end of file diff --git a/tests/farm/html/gold_unicode/unicode_py.html b/tests/farm/html/gold_unicode/unicode_py.html index 8ce44fdd7..7708f22e8 100644 --- a/tests/farm/html/gold_unicode/unicode_py.html +++ b/tests/farm/html/gold_unicode/unicode_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for unicode.py: 100% - @@ -20,26 +14,20 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -62,7 +50,6 @@

-
@@ -75,7 +62,6 @@

6

7

8

-

# -*- coding: utf-8 -*- 

@@ -86,20 +72,17 @@

 

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

surrogate = "db40,dd00: x󠄀" 

-

- - - + \ No newline at end of file From 0a4d90df6047c426c837e73c1d879b1c0addbb66 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 29 Jun 2018 19:53:58 -0400 Subject: [PATCH 107/952] Fix a too-long line --- tests/test_summary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_summary.py b/tests/test_summary.py index 65be280a8..adc1fcfa1 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -100,7 +100,8 @@ def test_report_omitting(self): # Try reporting while omitting some modules self.make_mycode() self.run_command("coverage run mycode.py") - report = self.report_from_command("coverage report --omit '%s/*,*/site-packages/*'" % TESTS_DIR) + omit = '{}/*,*/site-packages/*'.format(TESTS_DIR) + report = self.report_from_command("coverage report --omit '{}'".format(omit)) # Name Stmts Miss Cover # ------------------------------- From 9e783c683cf2013730a84c6f5e2e7f4439d7a352 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 29 Jun 2018 22:07:46 -0400 Subject: [PATCH 108/952] Suppress needless warnings during tests --- tests/conftest.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 4470b7514..ecfeaf24e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,9 +10,23 @@ import pytest import warnings +from coverage import env + @pytest.fixture(autouse=True) def set_warnings(): """Enable DeprecationWarnings during all tests.""" warnings.simplefilter("default") warnings.simplefilter("once", DeprecationWarning) + + # A warning to suppress: + # setuptools/py33compat.py:54: DeprecationWarning: The value of convert_charrefs will become + # True in 3.5. You are encouraged to set the value explicitly. + # unescape = getattr(html, 'unescape', html_parser.HTMLParser().unescape) + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message="The value of convert_charrefs will become True in 3.5.", + ) + if env.PYPY: + warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning) From 9472d24cc695ef67e37a96224b8c37818e164ea3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 29 Jun 2018 22:08:08 -0400 Subject: [PATCH 109/952] Jython isn't working, don't test it by default --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3732a19bb..e288fc075 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [tox] -envlist = py{27,34,35,36,37}, pypy{2,3}, jython, doc, lint +envlist = py{27,34,35,36,37}, pypy{2,3}, doc, lint skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True} toxworkdir = {env:TOXWORKDIR:.tox} From ef21d19ca4fe3fc0c88a1135721f2dfbc60506f3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 29 Jun 2018 22:29:15 -0400 Subject: [PATCH 110/952] ResourceWarning isn't in py2? --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index ecfeaf24e..666dcf769 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,5 +28,5 @@ def set_warnings(): category=DeprecationWarning, message="The value of convert_charrefs will become True in 3.5.", ) - if env.PYPY: + if env.PYPY and env.PY3: warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning) From 24406f95ad1049c60a2ebb907d69168b6d8b42e2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 30 Jun 2018 10:30:06 -0400 Subject: [PATCH 111/952] Spelling in pylint isn't worth the difficulty of installing enchant --- Makefile | 3 --- pylintrc | 10 ---------- requirements/dev.pip | 1 - 3 files changed, 14 deletions(-) diff --git a/Makefile b/Makefile index 5fe79f902..60182eb5d 100644 --- a/Makefile +++ b/Makefile @@ -39,9 +39,6 @@ lint: todo: -grep -R --include=*.py TODO $(LINTABLE) -spell: - -pylint --disable=all --enable=spelling $(LINTABLE) - pep8: pycodestyle --filename=*.py --repeat $(LINTABLE) diff --git a/pylintrc b/pylintrc index 368d9184c..d4ba155c6 100644 --- a/pylintrc +++ b/pylintrc @@ -327,13 +327,3 @@ ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes - -# -# SPELLING -# - -spelling-dict=en_US -# pylint doesn't strip the words, so insert a dummy x at the beginning to make -# the other words work properly. -# https://bitbucket.org/logilab/pylint/issue/398/spelling-words-need-to-be-stripped-or-the -spelling-private-dict-file=doc/dict.txt diff --git a/requirements/dev.pip b/requirements/dev.pip index 3fe88dd08..965c5f48b 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -12,7 +12,6 @@ greenlet==0.4.13 mock==2.0.0 PyContracts==1.8.3 -pyenchant==2.0.0 pylint==1.9.2 unittest-mixins==1.4 check-manifest==0.37 From 3a09d466015fbbd2ca3771cd3d425c3f4a3ebd38 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 30 Jun 2018 10:30:31 -0400 Subject: [PATCH 112/952] Run pylint on travis --- .travis.yml | 2 +- tox.ini | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index df238e57f..8b0165ae0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ # Tell Travis what to do -# https://travis-ci.org/nedbat/coveragepy +# https://travis-ci.com/nedbat/coveragepy language: python diff --git a/tox.ini b/tox.ini index e288fc075..beeb8567f 100644 --- a/tox.ini +++ b/tox.ini @@ -87,3 +87,12 @@ commands = check-manifest --ignore 'lab*,perf*,doc/sample_html*,.treerc' python setup.py check -r -s python -m pylint --notes= {env:LINTABLE} + +[travis] +python = + 2.7: py27, lint + 3.4: py34 + 3.5: py35 + 3.6: py36 + pypy: pypy + pypy3.5: pypy3 From 6cc7075c3db4ec6070c28b145d72c00e3a8a38df Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 30 Jun 2018 10:30:44 -0400 Subject: [PATCH 113/952] Quiet a pylint warning --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 666dcf769..aeccec888 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,4 +29,6 @@ def set_warnings(): message="The value of convert_charrefs will become True in 3.5.", ) if env.PYPY and env.PY3: + # pypy3 warns about unclosed files a lot. + # pylint: disable=undefined-variable warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning) From cbd62929068f24a6cacb634fc8bc04a65cbb70ac Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 30 Jun 2018 12:39:53 -0400 Subject: [PATCH 114/952] No need to prune things we never included --- MANIFEST.in | 2 -- 1 file changed, 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 536aa118e..f79021f7e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -40,7 +40,5 @@ recursive-include tests/farm */gold*/*.* */gold*/*/*.* recursive-include tests/farm/*/src * *.* recursive-include tests js/*.* qunit/*.* prune tests/eggsrc/build -prune tests/eggsrc/dist -prune tests/eggsrc/*.egg-info global-exclude *.py[co] From 309d7be253c36a3faee878bccfcf2a9995b168a9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 1 Jul 2018 09:20:06 -0400 Subject: [PATCH 115/952] Don't need the v3->v4 data converter in v5 --- CHANGES.rst | 2 ++ coverage/backward.py | 6 ----- coverage/pickle2json.py | 47 --------------------------------- tests/test_pickle2json.py | 55 --------------------------------------- 4 files changed, 2 insertions(+), 108 deletions(-) delete mode 100644 coverage/pickle2json.py delete mode 100644 tests/test_pickle2json.py diff --git a/CHANGES.rst b/CHANGES.rst index 116448714..1b6c8ddb5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,8 @@ Unreleased - HTML files no longer have trailing and extra whitespace. +- pickle2json has been removed. + .. _Bitbucket: https://bitbucket.org/ned/coveragepy .. _GitHub: https://github.com/nedbat/coveragepy diff --git a/coverage/backward.py b/coverage/backward.py index 5f59b23f0..b43e35f35 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -38,12 +38,6 @@ except NameError: unicode_class = str -# Where do pickles come from? -try: - import cPickle as pickle -except ImportError: - import pickle - # range or xrange? try: range = xrange # pylint: disable=redefined-builtin diff --git a/coverage/pickle2json.py b/coverage/pickle2json.py deleted file mode 100644 index 006558f15..000000000 --- a/coverage/pickle2json.py +++ /dev/null @@ -1,47 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - -"""Convert pickle to JSON for coverage.py.""" - -from coverage.backward import pickle -from coverage.data import CoverageData - - -def pickle_read_raw_data(cls_unused, file_obj): - """Replacement for CoverageData._read_raw_data.""" - return pickle.load(file_obj) - - -def pickle2json(infile, outfile): - """Convert a coverage.py 3.x pickle data file to a 4.x JSON data file.""" - try: - old_read_raw_data = CoverageData._read_raw_data - CoverageData._read_raw_data = pickle_read_raw_data - - covdata = CoverageData() - - with open(infile, 'rb') as inf: - covdata.read_fileobj(inf) - - covdata.write_file(outfile) - finally: - CoverageData._read_raw_data = old_read_raw_data - - -if __name__ == "__main__": - from optparse import OptionParser - - parser = OptionParser(usage="usage: %s [options]" % __file__) - parser.description = "Convert .coverage files from pickle to JSON format" - parser.add_option( - "-i", "--input-file", action="store", default=".coverage", - help="Name of input file. Default .coverage", - ) - parser.add_option( - "-o", "--output-file", action="store", default=".coverage", - help="Name of output file. Default .coverage", - ) - - (options, args) = parser.parse_args() - - pickle2json(options.input_file, options.output_file) diff --git a/tests/test_pickle2json.py b/tests/test_pickle2json.py deleted file mode 100644 index 37886bac0..000000000 --- a/tests/test_pickle2json.py +++ /dev/null @@ -1,55 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - -"""Tests for coverage.pickle2json""" - -from coverage.backward import pickle, iitems -from coverage.data import CoverageData -from coverage.pickle2json import pickle2json - -from tests.coveragetest import CoverageTest -from tests.test_data import DataTestHelpers, LINES_1, ARCS_3 - - -class Pickle2JsonTestInTempDir(DataTestHelpers, CoverageTest): - """Tests pickle2json.py.""" - - no_files_in_temp_dir = True - - def write_pickled_file(self, covdata, filename): - """Write coverage data as pickled `filename`.""" - # Create the file data. - file_data = {} - - if covdata._arcs: - file_data['arcs'] = dict((f, list(amap)) for f, amap in iitems(covdata._arcs)) - else: - file_data['lines'] = dict((f, list(lmap)) for f, lmap in iitems(covdata._lines)) - - # Write the pickle to the file. - with open(filename, 'wb') as file_obj: - pickle.dump(file_data, file_obj, 2) - - def test_read_write_lines_pickle(self): - # Test the old pickle format. - covdata1 = CoverageData() - covdata1.add_lines(LINES_1) - self.write_pickled_file(covdata1, "lines.pkl") - - pickle2json("lines.pkl", "lines.json") - - covdata2 = CoverageData() - covdata2.read_file("lines.json") - self.assert_lines1_data(covdata2) - - def test_read_write_arcs_pickle(self): - # Test the old pickle format. - covdata1 = CoverageData() - covdata1.add_arcs(ARCS_3) - self.write_pickled_file(covdata1, "arcs.pkl") - - pickle2json("arcs.pkl", "arcs.json") - - covdata2 = CoverageData() - covdata2.read_file("arcs.json") - self.assert_arcs3_data(covdata2) From c4672af21f90ffee564584dd9e52935841507b0c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 7 Jul 2018 08:29:09 -0400 Subject: [PATCH 116/952] CHANGES polishing --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1b6c8ddb5..0e80b26c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,7 +21,7 @@ Unreleased - HTML files no longer have trailing and extra whitespace. -- pickle2json has been removed. +- pickle2json, for converting v3 data files to v4 data files, has been removed. .. _Bitbucket: https://bitbucket.org/ned/coveragepy .. _GitHub: https://github.com/nedbat/coveragepy @@ -37,8 +37,8 @@ Version 5.0a1 --- 2018-06-05 - The location of the configuration file can now be specified with a ``COVERAGE_RCFILE`` environment variable, as requested in `issue 650`_. -- A new warning (already-imported) is issued if measurable files have already - been imported before coverage.py started measurement. See +- A new warning (``already-imported``) is issued if measurable files have + already been imported before coverage.py started measurement. See :ref:`cmd_warnings` for more information. - Running coverage many times for small runs in a single process should be From b6242f45e29d63b62989a48d1097e1c30bf22eaf Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 7 Jul 2018 09:05:54 -0400 Subject: [PATCH 117/952] Simplify a URL so that docs can build pre-release --- doc/install.rst | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/doc/install.rst b/doc/install.rst index 037fd62aa..62bd0b983 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -95,36 +95,19 @@ Checking the installation If all went well, you should be able to open a command prompt, and see coverage.py installed properly: -.. ifconfig:: not prerelease +.. In the output below, the URL should actually have the release in it for + pre-release, but Sphinx couldn't make a URL like that, so whatever. - .. parsed-literal:: +.. parsed-literal:: - $ coverage --version - Coverage.py, version |release| with C extension - Documentation at https://coverage.readthedocs.io - -.. ifconfig:: prerelease - - .. parsed-literal:: - - $ coverage --version - Coverage.py, version |release| with C extension - Documentation at https://coverage.readthedocs.io/en/coverage-|release| + $ coverage --version + Coverage.py, version |release| with C extension + Documentation at https://coverage.readthedocs.io You can also invoke coverage.py as a module: -.. ifconfig:: not prerelease +.. parsed-literal:: - .. parsed-literal:: - - $ python -m coverage --version - Coverage.py, version |release| with C extension - Documentation at https://coverage.readthedocs.io - -.. ifconfig:: prerelease - - .. parsed-literal:: - - $ python -m coverage --version - Coverage.py, version |release| with C extension - Documentation at https://coverage.readthedocs.io/en/coverage-|release| + $ python -m coverage --version + Coverage.py, version |release| with C extension + Documentation at https://coverage.readthedocs.io From b7ca9039c68fefff93f8c787f0c1589e097c8e4f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 8 Jul 2018 16:34:48 -0400 Subject: [PATCH 118/952] Be stricter about self._data --- coverage/cmdline.py | 2 +- coverage/control.py | 36 ++++++++++++++++-------------------- coverage/html.py | 6 +++--- coverage/summary.py | 3 ++- coverage/xmlreport.py | 3 ++- tests/test_plugins.py | 8 ++++---- tests/test_summary.py | 2 +- 7 files changed, 29 insertions(+), 31 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 2af301411..fba1112ff 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -659,7 +659,7 @@ def do_debug(self, args): print(" %s" % line) elif info == 'data': self.coverage.load() - data = self.coverage.data + data = self.coverage.get_data() print(info_header("data")) print("path: %s" % self.coverage._data_files.filename) if data: diff --git a/coverage/control.py b/coverage/control.py index aa93671c4..a5943aa8d 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -152,7 +152,7 @@ def __init__( self._warnings = [] # Other instance attributes, set later. - self.data = self._data_files = self._collector = None + self._data = self._data_files = self._collector = None self._plugins = None self._inorout = None self._inorout_class = InOrOut @@ -270,7 +270,7 @@ def _init(self): # Create the data file. We do this at construction time so that the # data file will be written into the directory where the process # started rather than wherever the process eventually chdir'd to. - self.data = CoverageData(debug=self._debug) + self._data = CoverageData(debug=self._debug) self._data_files = CoverageDataFiles( basename=self.config.data_file, warn=self._warn, debug=self._debug, ) @@ -395,7 +395,7 @@ def load(self): """Load previously-collected coverage data from the data file.""" self._init() self._collector.reset() - self._data_files.read(self.data) + self._data_files.read(self._data) def start(self): """Start measuring code coverage. @@ -449,7 +449,7 @@ def erase(self): """ self._init() self._collector.reset() - self.data.erase() + self._data.erase() self._data_files.erase(parallel=self.config.parallel) def clear_exclude(self, which='exclude'): @@ -502,8 +502,8 @@ def get_exclude_list(self, which='exclude'): def save(self): """Save the collected coverage data to the data file.""" self._init() - self.get_data() - self._data_files.write(self.data, suffix=self._data_suffix) + data = self.get_data() + self._data_files.write(data, suffix=self._data_suffix) def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. @@ -539,7 +539,7 @@ def combine(self, data_paths=None, strict=False): aliases.add(pattern, result) self._data_files.combine_parallel_data( - self.data, aliases=aliases, data_paths=data_paths, strict=strict, + self._data, aliases=aliases, data_paths=data_paths, strict=strict, ) def get_data(self): @@ -554,10 +554,10 @@ def get_data(self): """ self._init() - if self._collector.save_data(self.data): + if self._collector.save_data(self._data): self._post_save_work() - return self.data + return self._data def _post_save_work(self): """After saving data, look for warnings, post-work, etc. @@ -572,15 +572,15 @@ def _post_save_work(self): self._inorout.warn_unimported_source() # Find out if we got any data. - if not self.data and self._warn_no_data: + if not self._data and self._warn_no_data: self._warn("No data was collected.", slug="no-data-collected") # Find files that were never executed at all. for file_path, plugin_name in self._inorout.find_unexecuted_files(): - self.data.touch_file(file_path, plugin_name) + self._data.touch_file(file_path, plugin_name) if self.config.note: - self.data.add_run_info(note=self.config.note) + self._data.add_run_info(note=self.config.note) # Backward compatibility with version 1. def analysis(self, morf): @@ -621,11 +621,11 @@ def _analyze(self, it): Returns an `Analysis` object. """ - self.get_data() + data = self.get_data() if not isinstance(it, FileReporter): it = self._get_file_reporter(it) - return Analysis(self.data, it) + return Analysis(data, it) def _get_file_reporter(self, morf): """Get a FileReporter for a module or file name.""" @@ -634,7 +634,7 @@ def _get_file_reporter(self, morf): if isinstance(morf, string_class): abs_morf = abs_file(morf) - plugin_name = self.data.file_tracer(abs_morf) + plugin_name = self._data.file_tracer(abs_morf) if plugin_name: plugin = self._plugins.get(plugin_name) @@ -664,7 +664,7 @@ def _get_file_reporters(self, morfs=None): """ if not morfs: - morfs = self.data.measured_files() + morfs = self._data.measured_files() # Be sure we have a list. if not isinstance(morfs, (list, tuple)): @@ -696,7 +696,6 @@ def report( Returns a float, the total percentage covered. """ - self.get_data() self.config.from_args( ignore_errors=ignore_errors, report_omit=omit, report_include=include, show_missing=show_missing, skip_covered=skip_covered, @@ -718,7 +717,6 @@ def annotate( See :meth:`report` for other arguments. """ - self.get_data() self.config.from_args( ignore_errors=ignore_errors, report_omit=omit, report_include=include ) @@ -745,7 +743,6 @@ def html_report(self, morfs=None, directory=None, ignore_errors=None, Returns a float, the total percentage covered. """ - self.get_data() self.config.from_args( ignore_errors=ignore_errors, report_omit=omit, report_include=include, html_dir=directory, extra_css=extra_css, html_title=title, @@ -770,7 +767,6 @@ def xml_report( Returns a float, the total percentage covered. """ - self.get_data() self.config.from_args( ignore_errors=ignore_errors, report_omit=omit, report_include=include, xml_output=outfile, diff --git a/coverage/html.py b/coverage/html.py index 65aac9c13..186e9d22e 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -104,11 +104,11 @@ def __init__(self, cov, config): } self.source_tmpl = Templite(read_data("pyfile.html"), self.template_globals) - self.coverage = cov + self.data = cov.get_data() self.files = [] self.all_files_nums = [] - self.has_arcs = self.coverage.data.has_arcs() + self.has_arcs = self.data.has_arcs() self.status = HtmlStatus() self.extra_css = None self.totals = Numbers() @@ -169,7 +169,7 @@ def file_hash(self, source, fr): """Compute a hash that changes if the file needs to be re-reported.""" m = Hasher() m.update(source) - self.coverage.data.add_to_hash(fr.filename, m) + self.data.add_to_hash(fr.filename, m) return m.hexdigest() def html_file(self, fr, analysis): diff --git a/coverage/summary.py b/coverage/summary.py index 9fc60676d..95afbcf0b 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -16,7 +16,8 @@ class SummaryReporter(Reporter): def __init__(self, coverage, config): super(SummaryReporter, self).__init__(coverage, config) - self.branches = coverage.data.has_arcs() + data = coverage.get_data() + self.branches = data.has_arcs() def report(self, morfs, outfile=None): """Writes a report summarizing coverage statistics per module. diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 511270f13..5148b54a2 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -42,7 +42,8 @@ def __init__(self, coverage, config): self.source_paths.add(files.canonical_filename(src)) self.packages = {} self.xml_out = None - self.has_arcs = coverage.data.has_arcs() + self.data = coverage.get_data() + self.has_arcs = self.data.has_arcs() def report(self, morfs, outfile=None): """Generate a Cobertura-compatible XML report for `morfs`. diff --git a/tests/test_plugins.py b/tests/test_plugins.py index c9a8feaef..0987e41ac 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -369,19 +369,19 @@ def test_plugin2(self): _, statements, missing, _ = cov.analysis("foo_7.html") self.assertEqual(statements, [1, 2, 3, 4, 5, 6, 7]) self.assertEqual(missing, [1, 2, 3, 6, 7]) - self.assertIn("foo_7.html", cov.data.line_counts()) + self.assertIn("foo_7.html", cov.get_data().line_counts()) _, statements, missing, _ = cov.analysis("bar_4.html") self.assertEqual(statements, [1, 2, 3, 4]) self.assertEqual(missing, [1, 4]) - self.assertIn("bar_4.html", cov.data.line_counts()) + self.assertIn("bar_4.html", cov.get_data().line_counts()) - self.assertNotIn("quux_5.html", cov.data.line_counts()) + self.assertNotIn("quux_5.html", cov.get_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()) + self.assertIn("uni_3.html", cov.get_data().line_counts()) def test_plugin2_with_branch(self): self.make_render_and_caller() diff --git a/tests/test_summary.py b/tests/test_summary.py index adc1fcfa1..b28953706 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -771,7 +771,7 @@ def get_summary_text(self, coverage_data, options): cov = Coverage() cov.start() cov.stop() # pragma: nested - cov.data = coverage_data + cov._data = coverage_data printer = SummaryReporter(cov, options) destination = StringIO() printer.report([], destination) From 9a9fe6958412ff9ce447355081c74f7acb3bf857 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 19 Jul 2018 16:52:57 -0400 Subject: [PATCH 119/952] A better way to get the latest manylinux docker images --- Makefile | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 43d128340..29ecff372 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,8 @@ clean: sterile: clean -rm -rf .tox* + -docker image rm quay.io/pypa/manylinux1_i686 quay.io/pypa/manylinux1_x86_64 + LINTABLE = coverage tests igor.py setup.py __main__.py @@ -67,9 +69,6 @@ kit: wheel: tox -c tox_wheels.ini $(ARGS) -manylinux_clean: - docker image rm quay.io/pypa/manylinux1_i686 quay.io/pypa/manylinux1_x86_64 - manylinux: docker run -it --init --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/ci/manylinux.sh build docker run -it --init --rm -v `pwd`:/io quay.io/pypa/manylinux1_i686 /io/ci/manylinux.sh build From f6f0958b761927ef2a30dcf22642685eba3683d5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 22 Jul 2018 18:22:56 -0400 Subject: [PATCH 120/952] Update testing requirements (stop an xdist crash) --- requirements/dev.pip | 2 ++ requirements/pytest.pip | 2 +- requirements/tox.pip | 2 +- requirements/wheel.pip | 2 +- tox.ini | 4 ++-- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/requirements/dev.pip b/requirements/dev.pip index 965c5f48b..f9fa82657 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -4,6 +4,8 @@ # Requirements for doing local development work on coverage.py. # https://requires.io/github/nedbat/coveragepy/requirements/ +pip==18.0.0 + # PyPI requirements for running tests. -r tox.pip -r pytest.pip diff --git a/requirements/pytest.pip b/requirements/pytest.pip index 6b2433ad0..b23977b74 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -3,6 +3,6 @@ # The pytest specifics used by coverage.py -pytest==3.6.2 +pytest==3.6.3 pytest-xdist==1.22.2 flaky==3.4.0 diff --git a/requirements/tox.pip b/requirements/tox.pip index a209ac7fd..43b921c69 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -1,4 +1,4 @@ # The version of tox used by coverage.py -tox==3.0.0 +tox==3.1.2 # Adds env recreation on requirements file changes. tox-battery==0.5.1 diff --git a/requirements/wheel.pip b/requirements/wheel.pip index 9c6bf0cae..30ac5455c 100644 --- a/requirements/wheel.pip +++ b/requirements/wheel.pip @@ -1,3 +1,3 @@ # Things needed to make wheels for coverage.py -setuptools==39.2.0 +setuptools==40.0.0 wheel==0.31.1 diff --git a/tox.ini b/tox.ini index beeb8567f..102a7f546 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,8 @@ deps = # Check here for what might be out of date: # https://requires.io/github/nedbat/coveragepy/requirements/ -rrequirements/pytest.pip - pip==10.0.1 - setuptools==39.2.0 + pip==18.0 + setuptools==40.0.0 mock==2.0.0 PyContracts==1.8.3 unittest-mixins==1.4 From 918ecfa11b40fe34b867a5bc952ab238d18ed560 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 23 Jul 2018 19:28:27 -0400 Subject: [PATCH 121/952] Add a test to be sure 'combine' works both plain and --append --- tests/test_process.py | 64 +++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/tests/test_process.py b/tests/test_process.py index b4a881786..e022e727c 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -59,6 +59,9 @@ def test_environment(self): def make_b_or_c_py(self): """Create b_or_c.py, used in a few of these tests.""" + # "b_or_c.py b" will run 6 lines. + # "b_or_c.py c" will run 7 lines. + # Together, they run 8 lines. self.make_file("b_or_c.py", """\ import sys a = 1 @@ -66,6 +69,7 @@ def make_b_or_c_py(self): b = 1 else: c = 1 + c2 = 2 d = 1 print('done') """) @@ -91,11 +95,11 @@ def test_combine_parallel_data(self): # After combining, there should be only the .coverage file. self.assertEqual(self.number_of_data_files(), 1) - # Read the coverage file and see that b_or_c.py has all 7 lines + # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() data.read_file(".coverage") - self.assertEqual(data.line_counts()['b_or_c.py'], 7) + self.assertEqual(data.line_counts()['b_or_c.py'], 8) # Running combine again should fail, because there are no parallel data # files to combine. @@ -106,7 +110,7 @@ def test_combine_parallel_data(self): # And the originally combined data is still there. data = coverage.CoverageData() data.read_file(".coverage") - self.assertEqual(data.line_counts()['b_or_c.py'], 7) + self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_combine_parallel_data_with_a_corrupt_file(self): self.make_b_or_c_py() @@ -138,11 +142,11 @@ def test_combine_parallel_data_with_a_corrupt_file(self): # After combining, those two should be the only data files. self.assertEqual(self.number_of_data_files(), 2) - # Read the coverage file and see that b_or_c.py has all 7 lines + # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() data.read_file(".coverage") - self.assertEqual(data.line_counts()['b_or_c.py'], 7) + self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_combine_no_usable_files(self): # https://bitbucket.org/ned/coveragepy/issues/629/multiple-use-of-combine-leads-to-empty @@ -203,10 +207,42 @@ def test_combine_parallel_data_in_two_steps(self): # After combining, there should be only the .coverage file. self.assertEqual(self.number_of_data_files(), 1) - # Read the coverage file and see that b_or_c.py has all 7 lines + # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() data.read_file(".coverage") + self.assertEqual(data.line_counts()['b_or_c.py'], 8) + + def test_combine_parallel_data_no_append(self): + self.make_b_or_c_py() + + out = self.run_command("coverage run -p b_or_c.py b") + self.assertEqual(out, 'done\n') + self.assert_doesnt_exist(".coverage") + self.assertEqual(self.number_of_data_files(), 1) + + # Combine the (one) parallel coverage data file into .coverage . + self.run_command("coverage combine") + self.assert_exists(".coverage") + self.assertEqual(self.number_of_data_files(), 1) + + out = self.run_command("coverage run -p b_or_c.py c") + self.assertEqual(out, 'done\n') + self.assert_exists(".coverage") + self.assertEqual(self.number_of_data_files(), 2) + + # Combine the parallel coverage data files into .coverage, but don't + # use the data in .coverage already. + self.run_command("coverage combine") + self.assert_exists(".coverage") + + # After combining, there should be only the .coverage file. + self.assertEqual(self.number_of_data_files(), 1) + + # Read the coverage file and see that b_or_c.py has only 7 lines + # because we didn't keep the data from running b. + data = coverage.CoverageData() + data.read_file(".coverage") self.assertEqual(data.line_counts()['b_or_c.py'], 7) def test_append_data(self): @@ -222,11 +258,11 @@ def test_append_data(self): self.assert_exists(".coverage") self.assertEqual(self.number_of_data_files(), 1) - # Read the coverage file and see that b_or_c.py has all 7 lines + # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() data.read_file(".coverage") - self.assertEqual(data.line_counts()['b_or_c.py'], 7) + self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_append_data_with_different_file(self): self.make_b_or_c_py() @@ -246,11 +282,11 @@ def test_append_data_with_different_file(self): self.assert_doesnt_exist(".coverage") self.assert_exists(".mycovdata") - # Read the coverage file and see that b_or_c.py has all 7 lines + # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() data.read_file(".mycovdata") - self.assertEqual(data.line_counts()['b_or_c.py'], 7) + self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_append_can_create_a_data_file(self): self.make_b_or_c_py() @@ -293,18 +329,18 @@ def test_combine_with_rc(self): # After combining, there should be only the .coverage file. self.assertEqual(self.number_of_data_files(), 1) - # Read the coverage file and see that b_or_c.py has all 7 lines + # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() data.read_file(".coverage") - self.assertEqual(data.line_counts()['b_or_c.py'], 7) + self.assertEqual(data.line_counts()['b_or_c.py'], 8) # Reporting should still work even with the .rc file out = self.run_command("coverage report") self.assertMultiLineEqual(out, textwrap.dedent("""\ Name Stmts Miss Cover ------------------------------- - b_or_c.py 7 0 100% + b_or_c.py 8 0 100% """)) def test_combine_with_aliases(self): @@ -512,8 +548,6 @@ def main(): # After combining, there should be only the .coverage file. self.assertEqual(self.number_of_data_files(), 1) - # Read the coverage file and see that b_or_c.py has all 7 lines - # executed. data = coverage.CoverageData() data.read_file(".coverage") self.assertEqual(data.line_counts()['fork.py'], 9) From c24e594796b860531521be0190fc2f922c092c0e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 15 Jul 2018 11:59:33 -0400 Subject: [PATCH 122/952] CoverageData now also handles file operations --- coverage/cmdline.py | 2 +- coverage/control.py | 20 ++-- coverage/data.py | 272 ++++++++++++++++++++---------------------- tests/test_cmdline.py | 11 +- tests/test_data.py | 73 +++++------- 5 files changed, 173 insertions(+), 205 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index fba1112ff..4d1d1e72e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -661,7 +661,7 @@ def do_debug(self, args): self.coverage.load() data = self.coverage.get_data() print(info_header("data")) - print("path: %s" % self.coverage._data_files.filename) + print("path: %s" % self.coverage.get_data().filename) if data: print("has_arcs: %r" % data.has_arcs()) summary = data.line_counts(fullpath=True) diff --git a/coverage/control.py b/coverage/control.py index a5943aa8d..1760ee785 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -15,7 +15,7 @@ from coverage.backward import string_class, iitems from coverage.collector import Collector from coverage.config import read_coverage_config -from coverage.data import CoverageData, CoverageDataFiles +from coverage.data import CoverageData from coverage.debug import DebugControl, write_formatted_info from coverage.disposition import disposition_debug_msg from coverage.files import PathAliases, set_relative_directory, abs_file @@ -152,7 +152,7 @@ def __init__( self._warnings = [] # Other instance attributes, set later. - self._data = self._data_files = self._collector = None + self._data = self._collector = None self._plugins = None self._inorout = None self._inorout_class = InOrOut @@ -270,8 +270,7 @@ def _init(self): # Create the data file. We do this at construction time so that the # data file will be written into the directory where the process # started rather than wherever the process eventually chdir'd to. - self._data = CoverageData(debug=self._debug) - self._data_files = CoverageDataFiles( + self._data = CoverageData( basename=self.config.data_file, warn=self._warn, debug=self._debug, ) @@ -395,7 +394,7 @@ def load(self): """Load previously-collected coverage data from the data file.""" self._init() self._collector.reset() - self._data_files.read(self._data) + self._data.read() def start(self): """Start measuring code coverage. @@ -449,8 +448,7 @@ def erase(self): """ self._init() self._collector.reset() - self._data.erase() - self._data_files.erase(parallel=self.config.parallel) + self._data.erase(parallel=self.config.parallel) def clear_exclude(self, which='exclude'): """Clear the exclude list.""" @@ -503,7 +501,7 @@ def save(self): """Save the collected coverage data to the data file.""" self._init() data = self.get_data() - self._data_files.write(data, suffix=self._data_suffix) + data.write(suffix=self._data_suffix) def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. @@ -538,9 +536,7 @@ def combine(self, data_paths=None, strict=False): for pattern in paths[1:]: aliases.add(pattern, result) - self._data_files.combine_parallel_data( - self._data, aliases=aliases, data_paths=data_paths, strict=strict, - ) + self._data.combine_parallel_data(aliases=aliases, data_paths=data_paths, strict=strict) def get_data(self): """Get the collected data. @@ -827,7 +823,7 @@ def plugin_info(plugins): ('configs_attempted', self.config.attempted_config_files), ('configs_read', self.config.config_files_read), ('config_file', self.config.config_file), - ('data_path', self._data_files.filename), + ('data_path', self._data.filename), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), ('implementation', platform.python_implementation()), diff --git a/coverage/data.py b/coverage/data.py index 9f2d13086..6d30e2baf 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -57,8 +57,7 @@ class CoverageData(object): names in this API are case-sensitive, even on platforms with case-insensitive file systems. - To read a coverage.py data file, use :meth:`read_file`, or - :meth:`read_fileobj` if you have an already-opened file. You can then + To read a coverage.py data file, use :meth:`read_file`. You can then access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, or :meth:`file_tracer`. Run information is available with :meth:`run_infos`. @@ -78,8 +77,7 @@ class CoverageData(object): To add a file without any measured data, use :meth:`touch_file`. - You write to a named file with :meth:`write_file`, or to an already opened - file with :meth:`write_fileobj`. + You write to a named file with :meth:`write_file`. You can clear the data in memory with :meth:`erase`. Two data collections can be combined by using :meth:`update` on one :class:`CoverageData`, @@ -112,13 +110,19 @@ class CoverageData(object): # line data is easily recovered from the arcs: it is all the first elements # of the pairs that are greater than zero. - def __init__(self, debug=None): + def __init__(self, basename=None, warn=None, debug=None): """Create a CoverageData. + `warn` is the warning function to use. + + `basename` is the name of the file to use for storing data. + `debug` is a `DebugControl` object for writing debug messages. """ + self._warn = warn self._debug = debug + self.filename = os.path.abspath(basename or ".coverage") # A map from canonical Python source file name to a dictionary in # which there's an entry for each line number that has been @@ -262,7 +266,12 @@ def __nonzero__(self): __bool__ = __nonzero__ - def read_fileobj(self, file_obj): + def read(self): + """Read the coverage data.""" + if os.path.exists(self.filename): + self.read_file(self.filename) + + def _read_fileobj(self, file_obj): """Read the coverage data from the given file object. Should only be used on an empty CoverageData object. @@ -290,7 +299,7 @@ def read_file(self, filename): self._debug.write("Reading data from %r" % (filename,)) try: with self._open_for_reading(filename) as f: - self.read_fileobj(f) + self._read_fileobj(f) except Exception as exc: raise CoverageException( "Couldn't read data from '%s': %s: %s" % ( @@ -438,7 +447,34 @@ def touch_file(self, filename, plugin_name=""): self._validate() - def write_fileobj(self, file_obj): + def write(self, suffix=None): + """Write the collected coverage data to a file. + + `suffix` is a suffix to append to the base file name. This can be used + for multiple or parallel execution, so that many coverage data files + can exist simultaneously. A dot will be used to join the base name and + the suffix. + + """ + filename = self.filename + if suffix is True: + # If data_suffix was a simple true value, then make a suffix with + # plenty of distinguishing information. We do this here in + # `save()` at the last minute so that the pid will be correct even + # if the process forks. + extra = "" + if _TEST_NAME_FILE: # pragma: debugging + with open(_TEST_NAME_FILE) as f: + test_name = f.read() + extra = "." + test_name + dice = random.Random(os.urandom(8)).randint(0, 999999) + suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) + + if suffix: + filename += "." + suffix + self.write_file(filename) + + def _write_fileobj(self, file_obj): """Write the coverage data to `file_obj`.""" # Create the file data. @@ -465,16 +501,33 @@ def write_file(self, filename): if self._debug and self._debug.should('dataio'): self._debug.write("Writing data to %r" % (filename,)) with open(filename, 'w') as fdata: - self.write_fileobj(fdata) + self._write_fileobj(fdata) + + def erase(self, parallel=False): + """Erase the data in this object. + + If `parallel` is true, then also deletes data files created from the + basename by parallel-mode. - def erase(self): - """Erase the data in this object.""" + """ self._lines = None self._arcs = None self._file_tracers = {} self._runs = [] self._validate() + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing data file %r" % (self.filename,)) + file_be_gone(self.filename) + if parallel: + data_dir, local = os.path.split(self.filename) + localdot = local + '.*' + pattern = os.path.join(os.path.abspath(data_dir), localdot) + for filename in glob.glob(pattern): + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing parallel data file %r" % (filename,)) + file_be_gone(filename) + def update(self, other_data, aliases=None): """Update this data with data from another `CoverageData`. @@ -535,6 +588,69 @@ def update(self, other_data, aliases=None): self._validate() + def combine_parallel_data(self, aliases=None, data_paths=None, strict=False): + """Combine a number of data files together. + + Treat `self.filename` as a file prefix, and combine the data from all + of the data files starting with that prefix plus a dot. + + If `aliases` is provided, it's a `PathAliases` object that is used to + re-map paths to match the local machine's. + + If `data_paths` is provided, it is a list of directories or files to + combine. Directories are searched for files that start with + `self.filename` plus dot as a prefix, and those files are combined. + + If `data_paths` is not provided, then the directory portion of + `self.filename` is used as the directory to search for data files. + + Every data file found and combined is then deleted from disk. If a file + cannot be read, a warning will be issued, and the file will not be + deleted. + + If `strict` is true, and no files are found to combine, an error is + raised. + + """ + # Because of the os.path.abspath in the constructor, data_dir will + # never be an empty string. + data_dir, local = os.path.split(self.filename) + localdot = local + '.*' + + data_paths = data_paths or [data_dir] + files_to_combine = [] + for p in data_paths: + if os.path.isfile(p): + files_to_combine.append(os.path.abspath(p)) + elif os.path.isdir(p): + pattern = os.path.join(os.path.abspath(p), localdot) + files_to_combine.extend(glob.glob(pattern)) + else: + raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) + + if strict and not files_to_combine: + raise CoverageException("No data to combine") + + files_combined = 0 + for f in files_to_combine: + new_data = CoverageData(debug=self._debug) + try: + new_data.read_file(f) + except CoverageException as exc: + if self._warn: + # The CoverageException has the file name in it, so just + # use the message as the warning. + self._warn(str(exc)) + else: + self.update(new_data, aliases=aliases) + files_combined += 1 + if self._debug and self._debug.should('dataio'): + self._debug.write("Deleting combined data file %r" % (f,)) + file_be_gone(f) + + if strict and not files_combined: + raise CoverageException("No usable data files") + ## ## Miscellaneous ## @@ -609,140 +725,6 @@ def _has_arcs(self): return self._arcs is not None -class CoverageDataFiles(object): - """Manage the use of coverage data files.""" - - def __init__(self, basename=None, warn=None, debug=None): - """Create a CoverageDataFiles to manage data files. - - `warn` is the warning function to use. - - `basename` is the name of the file to use for storing data. - - `debug` is a `DebugControl` object for writing debug messages. - - """ - self.warn = warn - self.debug = debug - - # Construct the file name that will be used for data storage. - self.filename = os.path.abspath(basename or ".coverage") - - def erase(self, parallel=False): - """Erase the data from the file storage. - - If `parallel` is true, then also deletes data files created from the - basename by parallel-mode. - - """ - if self.debug and self.debug.should('dataio'): - self.debug.write("Erasing data file %r" % (self.filename,)) - file_be_gone(self.filename) - if parallel: - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - pattern = os.path.join(os.path.abspath(data_dir), localdot) - for filename in glob.glob(pattern): - if self.debug and self.debug.should('dataio'): - self.debug.write("Erasing parallel data file %r" % (filename,)) - file_be_gone(filename) - - def read(self, data): - """Read the coverage data.""" - if os.path.exists(self.filename): - data.read_file(self.filename) - - def write(self, data, suffix=None): - """Write the collected coverage data to a file. - - `suffix` is a suffix to append to the base file name. This can be used - for multiple or parallel execution, so that many coverage data files - can exist simultaneously. A dot will be used to join the base name and - the suffix. - - """ - filename = self.filename - if suffix is True: - # If data_suffix was a simple true value, then make a suffix with - # plenty of distinguishing information. We do this here in - # `save()` at the last minute so that the pid will be correct even - # if the process forks. - extra = "" - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE) as f: - test_name = f.read() - extra = "." + test_name - dice = random.Random(os.urandom(8)).randint(0, 999999) - suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) - - if suffix: - filename += "." + suffix - data.write_file(filename) - - def combine_parallel_data(self, data, aliases=None, data_paths=None, strict=False): - """Combine a number of data files together. - - Treat `self.filename` as a file prefix, and combine the data from all - of the data files starting with that prefix plus a dot. - - If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. - - If `data_paths` is provided, it is a list of directories or files to - combine. Directories are searched for files that start with - `self.filename` plus dot as a prefix, and those files are combined. - - If `data_paths` is not provided, then the directory portion of - `self.filename` is used as the directory to search for data files. - - Every data file found and combined is then deleted from disk. If a file - cannot be read, a warning will be issued, and the file will not be - deleted. - - If `strict` is true, and no files are found to combine, an error is - raised. - - """ - # Because of the os.path.abspath in the constructor, data_dir will - # never be an empty string. - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - - data_paths = data_paths or [data_dir] - files_to_combine = [] - for p in data_paths: - if os.path.isfile(p): - files_to_combine.append(os.path.abspath(p)) - elif os.path.isdir(p): - pattern = os.path.join(os.path.abspath(p), localdot) - files_to_combine.extend(glob.glob(pattern)) - else: - raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) - - if strict and not files_to_combine: - raise CoverageException("No data to combine") - - files_combined = 0 - for f in files_to_combine: - new_data = CoverageData(debug=self.debug) - try: - new_data.read_file(f) - except CoverageException as exc: - if self.warn: - # The CoverageException has the file name in it, so just - # use the message as the warning. - self.warn(str(exc)) - else: - data.update(new_data, aliases=aliases) - files_combined += 1 - if self.debug and self.debug.should('dataio'): - self.debug.write("Deleting combined data file %r" % (f,)) - file_be_gone(f) - - if strict and not files_combined: - raise CoverageException("No usable data files") - - def canonicalize_json_data(data): """Canonicalize our JSON data so it can be compared.""" for fname, lines in iitems(data.get('lines', {})): diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index b6fad76df..7fda79618 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -16,7 +16,7 @@ import coverage.cmdline from coverage import env from coverage.config import CoverageConfig -from coverage.data import CoverageData, CoverageDataFiles +from coverage.data import CoverageData from coverage.misc import ExceptionDuringRun from tests.coveragetest import CoverageTest, OK, ERR, command_line @@ -605,8 +605,7 @@ def test_debug_data(self): "file2.py": dict.fromkeys(range(1, 24)), }) data.add_file_tracers({"file1.py": "a_plugin"}) - data_files = CoverageDataFiles() - data_files.write(data) + data.write() self.command_line("debug data") self.assertMultiLineEqual(self.stdout(), textwrap.dedent("""\ @@ -617,16 +616,16 @@ def test_debug_data(self): 2 files: file1.py: 17 lines [a_plugin] file2.py: 23 lines - """).replace("FILENAME", data_files.filename)) + """).replace("FILENAME", data.filename)) def test_debug_data_with_no_data(self): - data_files = CoverageDataFiles() + data = CoverageData() self.command_line("debug data") self.assertMultiLineEqual(self.stdout(), textwrap.dedent("""\ -- data ------------------------------------------------------ path: FILENAME No data collected - """).replace("FILENAME", data_files.filename)) + """).replace("FILENAME", data.filename)) class CmdLineStdoutTest(BaseCmdLineTest): diff --git a/tests/test_data.py b/tests/test_data.py index 0d3172d42..3c0d602b9 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -11,8 +11,7 @@ import mock -from coverage.backward import StringIO -from coverage.data import CoverageData, CoverageDataFiles, debug_main, canonicalize_json_data +from coverage.data import CoverageData, debug_main, canonicalize_json_data from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -420,12 +419,10 @@ def test_empty_arcs_are_still_arcs(self): def test_read_and_write_are_opposites(self): covdata1 = CoverageData() covdata1.add_arcs(ARCS_3) - stringio = StringIO() - covdata1.write_fileobj(stringio) + covdata1.write() - stringio.seek(0) covdata2 = CoverageData() - covdata2.read_fileobj(stringio) + covdata2.read() self.assert_arcs3_data(covdata2) @@ -518,27 +515,23 @@ def test_debug_main(self): class CoverageDataFilesTest(DataTestHelpers, CoverageTest): - """Tests of CoverageDataFiles.""" + """Tests of CoverageData file handling.""" no_files_in_temp_dir = True - def setUp(self): - super(CoverageDataFilesTest, self).setUp() - self.data_files = CoverageDataFiles() - def test_reading_missing(self): self.assert_doesnt_exist(".coverage") covdata = CoverageData() - self.data_files.read(covdata) + covdata.read() self.assert_line_counts(covdata, {}) def test_writing_and_reading(self): covdata1 = CoverageData() covdata1.add_lines(LINES_1) - self.data_files.write(covdata1) + covdata1.write() covdata2 = CoverageData() - self.data_files.read(covdata2) + covdata2.read() self.assert_line_counts(covdata2, SUMMARY_1) def test_debug_output_with_debug_option(self): @@ -547,10 +540,10 @@ def test_debug_output_with_debug_option(self): debug = DebugControlString(options=["dataio"]) covdata1 = CoverageData(debug=debug) covdata1.add_lines(LINES_1) - self.data_files.write(covdata1) + covdata1.write() covdata2 = CoverageData(debug=debug) - self.data_files.read(covdata2) + covdata2.read() self.assert_line_counts(covdata2, SUMMARY_1) self.assertRegex( @@ -565,10 +558,10 @@ def test_debug_output_without_debug_option(self): debug = DebugControlString(options=[]) covdata1 = CoverageData(debug=debug) covdata1.add_lines(LINES_1) - self.data_files.write(covdata1) + covdata1.write() covdata2 = CoverageData(debug=debug) - self.data_files.read(covdata2) + covdata2.read() self.assert_line_counts(covdata2, SUMMARY_1) self.assertEqual(debug.get_output(), "") @@ -577,7 +570,7 @@ def test_explicit_suffix(self): self.assert_doesnt_exist(".coverage.SUFFIX") covdata = CoverageData() covdata.add_lines(LINES_1) - self.data_files.write(covdata, suffix='SUFFIX') + covdata.write(suffix='SUFFIX') self.assert_exists(".coverage.SUFFIX") self.assert_doesnt_exist(".coverage") @@ -587,7 +580,7 @@ def test_true_suffix(self): # suffix=True will make a randomly named data file. covdata1 = CoverageData() covdata1.add_lines(LINES_1) - self.data_files.write(covdata1, suffix=True) + covdata1.write(suffix=True) self.assert_doesnt_exist(".coverage") data_files1 = glob.glob(".coverage.*") self.assertEqual(len(data_files1), 1) @@ -595,7 +588,7 @@ def test_true_suffix(self): # Another suffix=True will choose a different name. covdata2 = CoverageData() covdata2.add_lines(LINES_1) - self.data_files.write(covdata2, suffix=True) + covdata2.write(suffix=True) self.assert_doesnt_exist(".coverage") data_files2 = glob.glob(".coverage.*") self.assertEqual(len(data_files2), 2) @@ -609,17 +602,17 @@ def test_combining(self): covdata1 = CoverageData() covdata1.add_lines(LINES_1) - self.data_files.write(covdata1, suffix='1') + covdata1.write(suffix='1') self.assert_exists(".coverage.1") self.assert_doesnt_exist(".coverage.2") covdata2 = CoverageData() covdata2.add_lines(LINES_2) - self.data_files.write(covdata2, suffix='2') + covdata2.write(suffix='2') self.assert_exists(".coverage.2") covdata3 = CoverageData() - self.data_files.combine_parallel_data(covdata3) + covdata3.combine_parallel_data() self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) self.assert_doesnt_exist(".coverage.1") @@ -628,22 +621,21 @@ def test_combining(self): def test_erasing(self): covdata1 = CoverageData() covdata1.add_lines(LINES_1) - self.data_files.write(covdata1) + covdata1.write() covdata1.erase() self.assert_line_counts(covdata1, {}) - self.data_files.erase() covdata2 = CoverageData() - self.data_files.read(covdata2) + covdata2.read() self.assert_line_counts(covdata2, {}) def test_erasing_parallel(self): self.make_file("datafile.1") self.make_file("datafile.2") self.make_file(".coverage") - data_files = CoverageDataFiles("datafile") - data_files.erase(parallel=True) + data = CoverageData("datafile") + data.erase(parallel=True) self.assert_doesnt_exist("datafile.1") self.assert_doesnt_exist("datafile.2") self.assert_exists(".coverage") @@ -659,7 +651,7 @@ def test_file_format(self): # Write with CoverageData, then read the JSON explicitly. covdata = CoverageData() covdata.add_lines(LINES_1) - self.data_files.write(covdata) + covdata.write() data = self.read_json_data_file(".coverage") @@ -676,7 +668,7 @@ def test_file_format_with_arcs(self): # Write with CoverageData, then read the JSON explicitly. covdata = CoverageData() covdata.add_arcs(ARCS_3) - self.data_files.write(covdata) + covdata.write() data = self.read_json_data_file(".coverage") @@ -689,14 +681,13 @@ def test_file_format_with_arcs(self): self.assertNotIn('file_tracers', data) def test_writing_to_other_file(self): - data_files = CoverageDataFiles(".otherfile") - covdata = CoverageData() + covdata = CoverageData(".otherfile") covdata.add_lines(LINES_1) - data_files.write(covdata) + covdata.write() self.assert_doesnt_exist(".coverage") self.assert_exists(".otherfile") - data_files.write(covdata, suffix="extra") + covdata.write(suffix="extra") self.assert_exists(".otherfile.extra") self.assert_doesnt_exist(".coverage") @@ -710,20 +701,20 @@ def test_combining_with_aliases(self): covdata1.add_file_tracers({ '/home/ned/proj/src/template.html': 'html.plugin', }) - self.data_files.write(covdata1, suffix='1') + covdata1.write(suffix='1') covdata2 = CoverageData() covdata2.add_lines({ r'c:\ned\test\a.py': {4: None, 5: None}, r'c:\ned\test\sub\b.py': {3: None, 6: None}, }) - self.data_files.write(covdata2, suffix='2') + covdata2.write(suffix='2') covdata3 = CoverageData() aliases = PathAliases() aliases.add("/home/ned/proj/src/", "./") aliases.add(r"c:\ned\test", "./") - self.data_files.combine_parallel_data(covdata3, aliases=aliases) + covdata3.combine_parallel_data(aliases=aliases) apy = canonical_filename('./a.py') sub_bpy = canonical_filename('./sub/b.py') @@ -750,7 +741,7 @@ def test_combining_from_different_directories(self): covdata_xxx.write_file('.coverage.xxx') covdata3 = CoverageData() - self.data_files.combine_parallel_data(covdata3, data_paths=['cov1', 'cov2']) + covdata3.combine_parallel_data(data_paths=['cov1', 'cov2']) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) @@ -776,7 +767,7 @@ def test_combining_from_files(self): covdata_xxx.write_file('cov2/.coverage.xxx') covdata3 = CoverageData() - self.data_files.combine_parallel_data(covdata3, data_paths=['cov1', 'cov2/.coverage.2']) + covdata3.combine_parallel_data(data_paths=['cov1', 'cov2/.coverage.2']) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) @@ -789,4 +780,4 @@ def test_combining_from_nonexistent_directories(self): covdata = CoverageData() msg = "Couldn't combine from non-existent path 'xyzzy'" with self.assertRaisesRegex(CoverageException, msg): - self.data_files.combine_parallel_data(covdata, data_paths=['xyzzy']) + covdata.combine_parallel_data(data_paths=['xyzzy']) From 7d71b1e052b2adead8c43bbc320582eab4938221 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 15 Jul 2018 16:26:48 -0400 Subject: [PATCH 123/952] Make file operations implicit on constructed filename --- coverage/data.py | 26 ++++++++------ tests/test_concurrency.py | 4 +-- tests/test_data.py | 76 ++++++++++++++++++++------------------- tests/test_debug.py | 4 +-- tests/test_process.py | 46 ++++++++++++------------ tests/test_summary.py | 2 +- 6 files changed, 83 insertions(+), 75 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 6d30e2baf..23e612a1e 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -57,7 +57,10 @@ class CoverageData(object): names in this API are case-sensitive, even on platforms with case-insensitive file systems. - To read a coverage.py data file, use :meth:`read_file`. You can then + A data file is associated with the data when the :class:`CoverageData` + is created. + + To read a coverage.py data file, use :meth:`read`. You can then access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, or :meth:`file_tracer`. Run information is available with :meth:`run_infos`. @@ -68,16 +71,15 @@ class CoverageData(object): most Python containers, you can determine if there is any data at all by using this object as a boolean value. - Most data files will be created by coverage.py itself, but you can use methods here to create data files if you like. The :meth:`add_lines`, :meth:`add_arcs`, and :meth:`add_file_tracers` methods add data, in ways that are convenient for coverage.py. The :meth:`add_run_info` method adds key-value pairs to the run information. - To add a file without any measured data, use :meth:`touch_file`. + To add a source file without any measured data, use :meth:`touch_file`. - You write to a named file with :meth:`write_file`. + Write the data to its file with :meth:`write`. You can clear the data in memory with :meth:`erase`. Two data collections can be combined by using :meth:`update` on one :class:`CoverageData`, @@ -267,9 +269,13 @@ def __nonzero__(self): __bool__ = __nonzero__ def read(self): - """Read the coverage data.""" + """Read the coverage data. + + It is fine for the file to not exist, in which case no data is read. + + """ if os.path.exists(self.filename): - self.read_file(self.filename) + self._read_file(self.filename) def _read_fileobj(self, file_obj): """Read the coverage data from the given file object. @@ -293,7 +299,7 @@ def _read_fileobj(self, file_obj): self._validate() - def read_file(self, filename): + def _read_file(self, filename): """Read the coverage data from `filename` into this object.""" if self._debug and self._debug.should('dataio'): self._debug.write("Reading data from %r" % (filename,)) @@ -472,7 +478,7 @@ def write(self, suffix=None): if suffix: filename += "." + suffix - self.write_file(filename) + self._write_file(filename) def _write_fileobj(self, file_obj): """Write the coverage data to `file_obj`.""" @@ -496,7 +502,7 @@ def _write_fileobj(self, file_obj): file_obj.write(self._GO_AWAY) json.dump(file_data, file_obj, separators=(',', ':')) - def write_file(self, filename): + def _write_file(self, filename): """Write the coverage data to `filename`.""" if self._debug and self._debug.should('dataio'): self._debug.write("Writing data to %r" % (filename,)) @@ -635,7 +641,7 @@ def combine_parallel_data(self, aliases=None, data_paths=None, strict=False): for f in files_to_combine: new_data = CoverageData(debug=self._debug) try: - new_data.read_file(f) + new_data._read_file(f) except CoverageException as exc: if self._warn: # The CoverageException has the file name in it, so just diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 58529ec52..a4f700ed4 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -235,8 +235,8 @@ def try_some_code(self, code, concurrency, the_module, expected_out=None): # Read the coverage file and see that try_it.py has all its lines # executed. - data = coverage.CoverageData() - data.read_file(".coverage") + data = coverage.CoverageData(".coverage") + data.read() # If the test fails, it's helpful to see this info: fname = abs_file("try_it.py") diff --git a/tests/test_data.py b/tests/test_data.py index 3c0d602b9..5deccef00 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -430,59 +430,58 @@ class CoverageDataTestInTempDir(DataTestHelpers, CoverageTest): """Tests of CoverageData that need a temporary directory to make files.""" def test_read_write_lines(self): - covdata1 = CoverageData() + covdata1 = CoverageData("lines.dat") covdata1.add_lines(LINES_1) - covdata1.write_file("lines.dat") + covdata1.write() - covdata2 = CoverageData() - covdata2.read_file("lines.dat") + covdata2 = CoverageData("lines.dat") + covdata2.read() self.assert_lines1_data(covdata2) def test_read_write_arcs(self): - covdata1 = CoverageData() + covdata1 = CoverageData("arcs.dat") covdata1.add_arcs(ARCS_3) - covdata1.write_file("arcs.dat") + covdata1.write() - covdata2 = CoverageData() - covdata2.read_file("arcs.dat") + covdata2 = CoverageData("arcs.dat") + covdata2.read() self.assert_arcs3_data(covdata2) def test_read_errors(self): - covdata = CoverageData() + msg = r"Couldn't read data from '.*[/\\]{0}': \S+" - msg = r"Couldn't read data from '{0}': \S+" self.make_file("xyzzy.dat", "xyzzy") with self.assertRaisesRegex(CoverageException, msg.format("xyzzy.dat")): - covdata.read_file("xyzzy.dat") + covdata = CoverageData("xyzzy.dat") + covdata.read() + self.assertFalse(covdata) self.make_file("empty.dat", "") with self.assertRaisesRegex(CoverageException, msg.format("empty.dat")): - covdata.read_file("empty.dat") - - with self.assertRaisesRegex(CoverageException, msg.format("nonexistent.dat")): - covdata.read_file("nonexistent.dat") + covdata = CoverageData("empty.dat") + covdata.read() + self.assertFalse(covdata) self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): - covdata.read_file("misleading.dat") - - # After all that, no data should be in our CoverageData. + covdata = CoverageData("misleading.dat") + covdata.read() self.assertFalse(covdata) def test_debug_main(self): - covdata1 = CoverageData() + covdata1 = CoverageData(".coverage") covdata1.add_lines(LINES_1) - covdata1.write_file(".coverage") + covdata1.write() debug_main([]) - covdata2 = CoverageData() + covdata2 = CoverageData("arcs.dat") covdata2.add_arcs(ARCS_3) covdata2.add_file_tracers({"y.py": "magic_plugin"}) covdata2.add_run_info(version="v3.14", chunks=["z", "a"]) - covdata2.write_file("arcs.dat") + covdata2.write() - covdata3 = CoverageData() - covdata3.write_file("empty.dat") + covdata3 = CoverageData("empty.dat") + covdata3.write() debug_main(["arcs.dat", "empty.dat"]) expected = { @@ -725,20 +724,20 @@ def test_combining_with_aliases(self): self.assertEqual(covdata3.file_tracer(template_html), 'html.plugin') def test_combining_from_different_directories(self): - covdata1 = CoverageData() + covdata1 = CoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) os.makedirs('cov1') - covdata1.write_file('cov1/.coverage.1') + covdata1.write() - covdata2 = CoverageData() + covdata2 = CoverageData('cov2/.coverage.2') covdata2.add_lines(LINES_2) os.makedirs('cov2') - covdata2.write_file('cov2/.coverage.2') + covdata2.write() # This data won't be included. - covdata_xxx = CoverageData() + covdata_xxx = CoverageData('.coverage.xxx') covdata_xxx.add_arcs(ARCS_3) - covdata_xxx.write_file('.coverage.xxx') + covdata_xxx.write() covdata3 = CoverageData() covdata3.combine_parallel_data(data_paths=['cov1', 'cov2']) @@ -750,21 +749,24 @@ def test_combining_from_different_directories(self): self.assert_exists(".coverage.xxx") def test_combining_from_files(self): - covdata1 = CoverageData() + covdata1 = CoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) os.makedirs('cov1') - covdata1.write_file('cov1/.coverage.1') + covdata1.write() - covdata2 = CoverageData() + covdata2 = CoverageData('cov2/.coverage.2') covdata2.add_lines(LINES_2) os.makedirs('cov2') - covdata2.write_file('cov2/.coverage.2') + covdata2.write() # This data won't be included. - covdata_xxx = CoverageData() + covdata_xxx = CoverageData('.coverage.xxx') covdata_xxx.add_arcs(ARCS_3) - covdata_xxx.write_file('.coverage.xxx') - covdata_xxx.write_file('cov2/.coverage.xxx') + covdata_xxx.write() + + covdata_2xxx = CoverageData('cov2/.coverage.xxx') + covdata_2xxx.add_arcs(ARCS_3) + covdata_2xxx.write() covdata3 = CoverageData() covdata3.combine_parallel_data(data_paths=['cov1', 'cov2/.coverage.2']) diff --git a/tests/test_debug.py b/tests/test_debug.py index 2699ca61f..c46e3dae1 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -136,10 +136,10 @@ def test_debug_callers(self): 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. + # be _write_file in data.py. 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+$") + self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) diff --git a/tests/test_process.py b/tests/test_process.py index e022e727c..341ad37cb 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -98,7 +98,7 @@ def test_combine_parallel_data(self): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) # Running combine again should fail, because there are no parallel data @@ -109,7 +109,7 @@ def test_combine_parallel_data(self): # And the originally combined data is still there. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_combine_parallel_data_with_a_corrupt_file(self): @@ -145,7 +145,7 @@ def test_combine_parallel_data_with_a_corrupt_file(self): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_combine_no_usable_files(self): @@ -179,7 +179,7 @@ def test_combine_no_usable_files(self): # Read the coverage file and see that b_or_c.py has 6 lines # executed (we only did b, not c). data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 6) def test_combine_parallel_data_in_two_steps(self): @@ -210,7 +210,7 @@ def test_combine_parallel_data_in_two_steps(self): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_combine_parallel_data_no_append(self): @@ -242,7 +242,7 @@ def test_combine_parallel_data_no_append(self): # Read the coverage file and see that b_or_c.py has only 7 lines # because we didn't keep the data from running b. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 7) def test_append_data(self): @@ -261,7 +261,7 @@ def test_append_data(self): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_append_data_with_different_file(self): @@ -284,8 +284,8 @@ def test_append_data_with_different_file(self): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. - data = coverage.CoverageData() - data.read_file(".mycovdata") + data = coverage.CoverageData(".mycovdata") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) def test_append_can_create_a_data_file(self): @@ -299,7 +299,7 @@ def test_append_can_create_a_data_file(self): # Read the coverage file and see that b_or_c.py has only 6 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 6) def test_combine_with_rc(self): @@ -332,7 +332,7 @@ def test_combine_with_rc(self): # Read the coverage file and see that b_or_c.py has all 8 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['b_or_c.py'], 8) # Reporting should still work even with the .rc file @@ -386,7 +386,7 @@ def test_combine_with_aliases(self): # Read the coverage data file and see that the two different x.py # files have been combined together. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() summary = data.line_counts(fullpath=True) self.assertEqual(len(summary), 1) actual = os.path.normcase(os.path.abspath(list(summary.keys())[0])) @@ -549,7 +549,7 @@ def main(): self.assertEqual(self.number_of_data_files(), 1) data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['fork.py'], 9) def test_warnings_during_reporting(self): @@ -655,8 +655,8 @@ def test_note(self): self.make_file("simple.py", """print('hello')""") self.run_command("coverage run simple.py") - data = CoverageData() - data.read_file("mydata.dat") + data = CoverageData("mydata.dat") + data.read() infos = data.run_infos() self.assertEqual(len(infos), 1) expected = u"These are musical notes: ♫𝅗𝅥♩" @@ -686,7 +686,7 @@ def test_fullcoverage(self): # pragma: no metacov out = self.run_command("python -m coverage run -L getenv.py") self.assertEqual(out, "FOOEY == BOO\n") data = coverage.CoverageData() - data.read_file(".coverage") + data.read() # The actual number of executed lines in os.py when it's # imported is 120 or so. Just running os.getenv executes # about 5. @@ -916,7 +916,7 @@ def excepthook(*args): # Read the coverage file and see that excepthook.py has 7 lines # executed. data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['excepthook.py'], 7) def test_excepthook_exit(self): @@ -1245,9 +1245,9 @@ def test_subprocess_with_pth_files(self): # pragma: no metacov # An existing data file should not be read when a subprocess gets # measured automatically. Create the data file here with bogus data in # it. - data = coverage.CoverageData() + data = coverage.CoverageData(".mycovdata") data.add_lines({os.path.abspath('sub.py'): dict.fromkeys(range(100))}) - data.write_file(".mycovdata") + data.write() self.make_file("coverage.ini", """\ [run] @@ -1261,8 +1261,8 @@ def test_subprocess_with_pth_files(self): # pragma: no metacov # Read the data from .coverage self.assert_exists(".mycovdata") - data = coverage.CoverageData() - data.read_file(".mycovdata") + data = coverage.CoverageData(".mycovdata") + data.read() self.assertEqual(data.line_counts()['sub.py'], 3) def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov @@ -1286,7 +1286,7 @@ def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov # assert that the combined .coverage data file is correct self.assert_exists(".coverage") data = coverage.CoverageData() - data.read_file(".coverage") + data.read() self.assertEqual(data.line_counts()['sub.py'], 3) # assert that there are *no* extra data files left over after a combine @@ -1376,7 +1376,7 @@ def path(basename): # Read the data from .coverage self.assert_exists(".coverage") data = coverage.CoverageData() - data.read_file(".coverage") + data.read() summary = data.line_counts() print(summary) self.assertEqual(summary[source + '.py'], 3) diff --git a/tests/test_summary.py b/tests/test_summary.py index b28953706..980fd3d4d 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -161,7 +161,7 @@ def test_run_omit_vs_report_omit(self): # Read the data written, to see that the right files have been omitted from running. covdata = CoverageData() - covdata.read_file(".coverage") + covdata.read() files = [os.path.basename(p) for p in covdata.measured_files()] self.assertIn("covmod1.py", files) self.assertNotIn("covmodzip1.py", files) From b2c530f0bafd2379a79152feebe4f96e1c2511c3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 26 Jul 2018 18:58:18 -0400 Subject: [PATCH 124/952] Remove a mention of Python 2.6 --- tests/coveragetest.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 8e3fa05db..caddbeedd 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -355,10 +355,8 @@ def assert_starts_with(self, s, prefix, msg=None): def assert_recent_datetime(self, dt, seconds=10, msg=None): """Assert that `dt` marks a time at most `seconds` seconds ago.""" age = datetime.datetime.now() - dt - # Python2.6 doesn't have total_seconds :( - self.assertEqual(age.days, 0, msg) - self.assertGreaterEqual(age.seconds, 0, msg) - self.assertLessEqual(age.seconds, seconds, msg) + self.assertGreaterEqual(age.total_seconds(), 0, msg) + self.assertLessEqual(age.total_seconds(), seconds, msg) def command_line(self, args, ret=OK, _covpkg=None): """Run `args` through the command line. From e301a01b772cfab9f567724e01df33e862d3b72f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 29 Jul 2018 18:46:02 -0400 Subject: [PATCH 125/952] WIP WIP WIP --- coverage/data.py | 121 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/coverage/data.py b/coverage/data.py index 23e612a1e..afb12df4a 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -12,6 +12,7 @@ import random import re import socket +import sqlite3 from coverage import env from coverage.backward import iitems, string_class @@ -731,6 +732,126 @@ def _has_arcs(self): return self._arcs is not None +SCHEMA = """ +create table schema ( + version integer +); + +insert into schema (version) values (1); + +create table file ( + id integer primary key, + path text, + tracer text, + unique(path) +); + +create table line ( + file_id integer, + lineno integer, + unique(file_id, lineno) +); +""" + +def _create_db(filename): + con = sqlite3.connect(filename) + with con: + for stmt in SCHEMA.split(';'): + con.execute(stmt.strip()) + con.close() + + +class CoverageDataSqlite(object): + def __init__(self, basename=None, warn=None, debug=None): + self.filename = os.path.abspath(basename or ".coverage") + self._warn = warn + self._debug = debug + + self._file_map = {} + self._db = None + + def _reset(self): + self._file_map = {} + if self._db is not None: + self._db.close() + self._db = None + + def _connect(self): + if self._db is None: + if not os.path.exists(self.filename): + if self._debug and self._debug.should('dataio'): + self._debug.write("Creating data file %r" % (self.filename,)) + _create_db(self.filename) + self._db = sqlite3.connect(self.filename) + for path, id in self._db.execute("select path, id from file"): + self._file_map[path] = id + return self._db + + def _file_id(self, filename): + if filename not in self._file_map: + with self._connect() as con: + cur = con.cursor() + cur.execute("insert into file (path) values (?)", (filename,)) + self._file_map[filename] = cur.lastrowid + return self._file_map[filename] + + def add_lines(self, line_data): + """Add measured line data. + + `line_data` is a dictionary mapping file names to dictionaries:: + + { filename: { lineno: None, ... }, ...} + + """ + with self._connect() as con: + for filename, linenos in iitems(line_data): + file_id = self._file_id(filename) + for lineno in linenos: + con.execute( + "insert or ignore into line (file_id, lineno) values (?, ?)", + (file_id, lineno), + ) + + def add_file_tracers(self, file_tracers): + """Add per-file plugin information. + + `file_tracers` is { filename: plugin_name, ... } + + """ + with self._connect() as con: + for filename, tracer in iitems(file_tracers): + con.execute( + "insert into file (path, tracer) values (?, ?) on duplicate key update", + (filename, tracer), + ) + + def erase(self, parallel=False): + """Erase the data in this object. + + If `parallel` is true, then also deletes data files created from the + basename by parallel-mode. + + """ + self._reset() + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing data file %r" % (self.filename,)) + file_be_gone(self.filename) + if parallel: + data_dir, local = os.path.split(self.filename) + localdot = local + '.*' + pattern = os.path.join(os.path.abspath(data_dir), localdot) + for filename in glob.glob(pattern): + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing parallel data file %r" % (filename,)) + file_be_gone(filename) + + def write(self, suffix=None): + """Write the collected coverage data to a file.""" + pass + +CoverageData = CoverageDataSqlite + + def canonicalize_json_data(data): """Canonicalize our JSON data so it can be compared.""" for fname, lines in iitems(data.get('lines', {})): From e7f8cd3804245104657e41b548a431801f6c1cee Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 31 Jul 2018 06:09:00 -0400 Subject: [PATCH 126/952] Move sqlite into sqldata.py --- coverage/data.py | 119 +----------------------------- coverage/sqldata.py | 172 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 118 deletions(-) create mode 100644 coverage/sqldata.py diff --git a/coverage/data.py b/coverage/data.py index afb12df4a..eda1a341d 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -12,7 +12,6 @@ import random import re import socket -import sqlite3 from coverage import env from coverage.backward import iitems, string_class @@ -732,123 +731,7 @@ def _has_arcs(self): return self._arcs is not None -SCHEMA = """ -create table schema ( - version integer -); - -insert into schema (version) values (1); - -create table file ( - id integer primary key, - path text, - tracer text, - unique(path) -); - -create table line ( - file_id integer, - lineno integer, - unique(file_id, lineno) -); -""" - -def _create_db(filename): - con = sqlite3.connect(filename) - with con: - for stmt in SCHEMA.split(';'): - con.execute(stmt.strip()) - con.close() - - -class CoverageDataSqlite(object): - def __init__(self, basename=None, warn=None, debug=None): - self.filename = os.path.abspath(basename or ".coverage") - self._warn = warn - self._debug = debug - - self._file_map = {} - self._db = None - - def _reset(self): - self._file_map = {} - if self._db is not None: - self._db.close() - self._db = None - - def _connect(self): - if self._db is None: - if not os.path.exists(self.filename): - if self._debug and self._debug.should('dataio'): - self._debug.write("Creating data file %r" % (self.filename,)) - _create_db(self.filename) - self._db = sqlite3.connect(self.filename) - for path, id in self._db.execute("select path, id from file"): - self._file_map[path] = id - return self._db - - def _file_id(self, filename): - if filename not in self._file_map: - with self._connect() as con: - cur = con.cursor() - cur.execute("insert into file (path) values (?)", (filename,)) - self._file_map[filename] = cur.lastrowid - return self._file_map[filename] - - def add_lines(self, line_data): - """Add measured line data. - - `line_data` is a dictionary mapping file names to dictionaries:: - - { filename: { lineno: None, ... }, ...} - - """ - with self._connect() as con: - for filename, linenos in iitems(line_data): - file_id = self._file_id(filename) - for lineno in linenos: - con.execute( - "insert or ignore into line (file_id, lineno) values (?, ?)", - (file_id, lineno), - ) - - def add_file_tracers(self, file_tracers): - """Add per-file plugin information. - - `file_tracers` is { filename: plugin_name, ... } - - """ - with self._connect() as con: - for filename, tracer in iitems(file_tracers): - con.execute( - "insert into file (path, tracer) values (?, ?) on duplicate key update", - (filename, tracer), - ) - - def erase(self, parallel=False): - """Erase the data in this object. - - If `parallel` is true, then also deletes data files created from the - basename by parallel-mode. - - """ - self._reset() - if self._debug and self._debug.should('dataio'): - self._debug.write("Erasing data file %r" % (self.filename,)) - file_be_gone(self.filename) - if parallel: - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - pattern = os.path.join(os.path.abspath(data_dir), localdot) - for filename in glob.glob(pattern): - if self._debug and self._debug.should('dataio'): - self._debug.write("Erasing parallel data file %r" % (filename,)) - file_be_gone(filename) - - def write(self, suffix=None): - """Write the collected coverage data to a file.""" - pass - +from coverage.sqldata import CoverageDataSqlite CoverageData = CoverageDataSqlite diff --git a/coverage/sqldata.py b/coverage/sqldata.py new file mode 100644 index 000000000..ee0798e36 --- /dev/null +++ b/coverage/sqldata.py @@ -0,0 +1,172 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Sqlite coverage data.""" + +import os +import sqlite3 + +from coverage.backward import iitems +from coverage.misc import CoverageException, file_be_gone + + +SCHEMA = """ +create table schema ( + version integer +); + +insert into schema (version) values (1); + +create table meta ( + name text, + value text, + unique(name) +); + +create table file ( + id integer primary key, + path text, + tracer text, + unique(path) +); + +create table line ( + file_id integer, + lineno integer, + unique(file_id, lineno) +); + +create table arc ( + file_id integer, + fromno integer, + tono integer, + unique(file_id, fromno, tono) +); +""" + +def _create_db(filename, schema): + con = sqlite3.connect(filename) + with con: + for stmt in schema.split(';'): + con.execute(stmt.strip()) + con.close() + + +class CoverageDataSqlite(object): + def __init__(self, basename=None, warn=None, debug=None): + self.filename = os.path.abspath(basename or ".coverage") + self._warn = warn + self._debug = debug + + self._file_map = {} + self._db = None + self._have_read = False + + def _reset(self): + self._file_map = {} + if self._db is not None: + self._db.close() + self._db = None + + def _connect(self): + if self._db is None: + if not os.path.exists(self.filename): + if self._debug and self._debug.should('dataio'): + self._debug.write("Creating data file %r" % (self.filename,)) + _create_db(self.filename, SCHEMA) + self._db = sqlite3.connect(self.filename) + for path, id in self._db.execute("select path, id from file"): + self._file_map[path] = id + return self._db + + def _file_id(self, filename): + self._start_writing() + if filename not in self._file_map: + with self._connect() as con: + cur = con.cursor() + cur.execute("insert into file (path) values (?)", (filename,)) + self._file_map[filename] = cur.lastrowid + return self._file_map[filename] + + def add_lines(self, line_data): + """Add measured line data. + + `line_data` is a dictionary mapping file names to dictionaries:: + + { filename: { lineno: None, ... }, ...} + + """ + self._start_writing() + with self._connect() as con: + for filename, linenos in iitems(line_data): + file_id = self._file_id(filename) + for lineno in linenos: + con.execute( + "insert or ignore into line (file_id, lineno) values (?, ?)", + (file_id, lineno), + ) + + def add_file_tracers(self, file_tracers): + """Add per-file plugin information. + + `file_tracers` is { filename: plugin_name, ... } + + """ + self._start_writing() + with self._connect() as con: + for filename, tracer in iitems(file_tracers): + con.execute( + "insert into file (path, tracer) values (?, ?) on duplicate key update", + (filename, tracer), + ) + + def erase(self, parallel=False): + """Erase the data in this object. + + If `parallel` is true, then also deletes data files created from the + basename by parallel-mode. + + """ + self._reset() + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing data file %r" % (self.filename,)) + file_be_gone(self.filename) + if parallel: + data_dir, local = os.path.split(self.filename) + localdot = local + '.*' + pattern = os.path.join(os.path.abspath(data_dir), localdot) + for filename in glob.glob(pattern): + if self._debug and self._debug.should('dataio'): + self._debug.write("Erasing parallel data file %r" % (filename,)) + file_be_gone(filename) + + def read(self): + self._have_read = True + + def write(self, suffix=None): + """Write the collected coverage data to a file.""" + pass + + def _start_writing(self): + if not self._have_read: + self.erase() + self._have_read = True + + def has_arcs(self): + return False # TODO! + + def measured_files(self): + """A list of all files that had been measured.""" + self._connect() + return list(self._file_map) + + def file_tracer(self, filename): + """Get the plugin name of the file tracer for a file. + + Returns the name of the plugin that handles this file. If the file was + measured, but didn't use a plugin, then "" is returned. If the file + was not measured, then None is returned. + + """ + with self._connect() as con: + pass From 0db04f814339e143422c211fdba351554fcc8f77 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 31 Jul 2018 06:19:05 -0400 Subject: [PATCH 127/952] Report works --- coverage/sqldata.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index ee0798e36..db2b0b173 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -141,6 +141,7 @@ def erase(self, parallel=False): file_be_gone(filename) def read(self): + self._connect() # TODO: doesn't look right self._have_read = True def write(self, suffix=None): @@ -157,7 +158,6 @@ def has_arcs(self): def measured_files(self): """A list of all files that had been measured.""" - self._connect() return list(self._file_map) def file_tracer(self, filename): @@ -168,5 +168,9 @@ def file_tracer(self, filename): was not measured, then None is returned. """ + return "" # TODO + + def lines(self, filename): with self._connect() as con: - pass + file_id = self._file_id(filename) + return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] From 48e7984b5c28a6d6a324bdb0fa62ae626be60f8a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 31 Jul 2018 07:23:45 -0400 Subject: [PATCH 128/952] SQL debugging --- coverage/sqldata.py | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index db2b0b173..056800434 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -44,12 +44,6 @@ ); """ -def _create_db(filename, schema): - con = sqlite3.connect(filename) - with con: - for stmt in schema.split(';'): - con.execute(stmt.strip()) - con.close() class CoverageDataSqlite(object): @@ -73,8 +67,14 @@ def _connect(self): if not os.path.exists(self.filename): if self._debug and self._debug.should('dataio'): self._debug.write("Creating data file %r" % (self.filename,)) - _create_db(self.filename, SCHEMA) - self._db = sqlite3.connect(self.filename) + self._db = Sqlite(self.filename, self._debug) + with self._db: + for stmt in SCHEMA.split(';'): + stmt = stmt.strip() + if stmt: + self._db.execute(stmt) + else: + self._db = Sqlite(self.filename, self._debug) for path, id in self._db.execute("select path, id from file"): self._file_map[path] = id return self._db @@ -83,8 +83,7 @@ def _file_id(self, filename): self._start_writing() if filename not in self._file_map: with self._connect() as con: - cur = con.cursor() - cur.execute("insert into file (path) values (?)", (filename,)) + cur = con.execute("insert into file (path) values (?)", (filename,)) self._file_map[filename] = cur.lastrowid return self._file_map[filename] @@ -174,3 +173,28 @@ def lines(self, filename): with self._connect() as con: file_id = self._file_id(filename) return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] + + +class Sqlite(object): + def __init__(self, filename, debug): + self.debug = debug if debug.should('sql') else None + if self.debug: + self.debug.write("Connecting to {!r}".format(filename)) + self.con = sqlite3.connect(filename) + + def close(self): + self.con.close() + + def __enter__(self): + self.con.__enter__() + return self + + def __exit__(self, exc_type, exc_value, traceback): + return self.con.__exit__(exc_type, exc_value, traceback) + + def execute(self, sql, parameters=()): + if self.debug: + tail = " with {!r}".format(parameters) if parameters else "" + self.debug.write("Executing {!r}{}".format(sql, tail)) + cur = self.con.execute(sql, parameters) + return cur From b457052020ec90fdba964ff8bd5abe6d92032e6b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 3 Aug 2018 07:44:56 -0400 Subject: [PATCH 129/952] Make writing data faster --- coverage/data.py | 6 +++--- coverage/sqldata.py | 27 ++++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index eda1a341d..e9243166f 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -22,7 +22,7 @@ os = isolate_module(os) -class CoverageData(object): +class CoverageJsonData(object): """Manages collected coverage data, including file storage. This class is the public supported API to the data coverage.py collects @@ -731,8 +731,8 @@ def _has_arcs(self): return self._arcs is not None -from coverage.sqldata import CoverageDataSqlite -CoverageData = CoverageDataSqlite +from coverage.sqldata import CoverageSqliteData +CoverageData = CoverageSqliteData def canonicalize_json_data(data): diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 056800434..39d6268b6 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -46,7 +46,7 @@ -class CoverageDataSqlite(object): +class CoverageSqliteData(object): def __init__(self, basename=None, warn=None, debug=None): self.filename = os.path.abspath(basename or ".coverage") self._warn = warn @@ -99,11 +99,11 @@ def add_lines(self, line_data): with self._connect() as con: for filename, linenos in iitems(line_data): file_id = self._file_id(filename) - for lineno in linenos: - con.execute( - "insert or ignore into line (file_id, lineno) values (?, ?)", - (file_id, lineno), - ) + data = [(file_id, lineno) for lineno in linenos] + con.executemany( + "insert or ignore into line (file_id, lineno) values (?, ?)", + data, + ) def add_file_tracers(self, file_tracers): """Add per-file plugin information. @@ -177,11 +177,16 @@ def lines(self, filename): class Sqlite(object): def __init__(self, filename, debug): - self.debug = debug if debug.should('sql') else None + self.debug = debug if (debug and debug.should('sql')) else None if self.debug: self.debug.write("Connecting to {!r}".format(filename)) self.con = sqlite3.connect(filename) + # This pragma makes writing faster. It disables rollbacks, but we never need them. + self.con.execute("pragma journal_mode=off") + # This pragma makes writing faster. + self.con.execute("pragma synchronous=off") + def close(self): self.con.close() @@ -196,5 +201,9 @@ def execute(self, sql, parameters=()): if self.debug: tail = " with {!r}".format(parameters) if parameters else "" self.debug.write("Executing {!r}{}".format(sql, tail)) - cur = self.con.execute(sql, parameters) - return cur + return self.con.execute(sql, parameters) + + def executemany(self, sql, data): + if self.debug: + self.debug.write("Executing many {!r}".format(sql)) + return self.con.executemany(sql, data) From 9244c218d282d6e7542487521d9ea0f17bc0c89d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 3 Aug 2018 10:44:19 -0400 Subject: [PATCH 130/952] Use a Sqlite application_id to identify the file. --- coverage/sqldata.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 39d6268b6..1e5fb5705 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -44,6 +44,7 @@ ); """ +APP_ID = 0x0c07ea6e # Kind of looks like "coverage" class CoverageSqliteData(object): @@ -69,12 +70,18 @@ def _connect(self): self._debug.write("Creating data file %r" % (self.filename,)) self._db = Sqlite(self.filename, self._debug) with self._db: + self._db.execute("pragma application_id = {}".format(APP_ID)) for stmt in SCHEMA.split(';'): stmt = stmt.strip() if stmt: self._db.execute(stmt) else: self._db = Sqlite(self.filename, self._debug) + with self._db: + for app_id, in self._db.execute("pragma application_id"): + app_id = int(app_id) + if app_id != APP_ID: + raise Exception("Doesn't look like a coverage data file: 0x{:08x} != 0x{:08x}".format(app_id, APP_ID)) for path, id in self._db.execute("select path, id from file"): self._file_map[path] = id return self._db From 0c5af4612210fa08113ea93372f877ef13aaa007 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 3 Aug 2018 20:57:36 -0400 Subject: [PATCH 131/952] Can measure and report branches --- coverage/sqldata.py | 118 +++++++++++++++++++++++++++++++++----------- 1 file changed, 89 insertions(+), 29 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 1e5fb5705..cddece312 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -18,9 +18,8 @@ insert into schema (version) values (1); create table meta ( - name text, - value text, - unique(name) + has_lines boolean, + has_arcs boolean ); create table file ( @@ -44,8 +43,9 @@ ); """ -APP_ID = 0x0c07ea6e # Kind of looks like "coverage" - +# >>> struct.unpack(">i", b"\xc0\x7e\x8a\x6e") # "coverage", kind of. +# (-1065448850,) +APP_ID = -1065448850 class CoverageSqliteData(object): def __init__(self, basename=None, warn=None, debug=None): @@ -55,35 +55,58 @@ def __init__(self, basename=None, warn=None, debug=None): self._file_map = {} self._db = None + # Are we in sync with the data file? self._have_read = False + self._has_lines = False + self._has_arcs = False + def _reset(self): self._file_map = {} if self._db is not None: self._db.close() self._db = None + self._have_read = False + + def _create_db(self): + if self._debug and self._debug.should('dataio'): + self._debug.write("Creating data file {!r}".format(self.filename)) + self._db = Sqlite(self.filename, self._debug) + with self._db: + self._db.execute("pragma application_id = {}".format(APP_ID)) + for stmt in SCHEMA.split(';'): + stmt = stmt.strip() + if stmt: + self._db.execute(stmt) + self._db.execute( + "insert into meta (has_lines, has_arcs) values (?, ?)", + (self._has_lines, self._has_arcs) + ) + + def _open_db(self): + if self._debug and self._debug.should('dataio'): + self._debug.write("Opening data file {!r}".format(self.filename)) + self._db = Sqlite(self.filename, self._debug) + with self._db: + for app_id, in self._db.execute("pragma application_id"): + app_id = int(app_id) + if app_id != APP_ID: + raise CoverageException( + "File {!r} doesn't look like a coverage data file: " + "0x{:08x} != 0x{:08x}".format(self.filename, app_id, APP_ID) + ) + for row in self._db.execute("select has_lines, has_arcs from meta"): + self._has_lines, self._has_arcs = row + + for path, id in self._db.execute("select path, id from file"): + self._file_map[path] = id def _connect(self): if self._db is None: - if not os.path.exists(self.filename): - if self._debug and self._debug.should('dataio'): - self._debug.write("Creating data file %r" % (self.filename,)) - self._db = Sqlite(self.filename, self._debug) - with self._db: - self._db.execute("pragma application_id = {}".format(APP_ID)) - for stmt in SCHEMA.split(';'): - stmt = stmt.strip() - if stmt: - self._db.execute(stmt) + if os.path.exists(self.filename): + self._open_db() else: - self._db = Sqlite(self.filename, self._debug) - with self._db: - for app_id, in self._db.execute("pragma application_id"): - app_id = int(app_id) - if app_id != APP_ID: - raise Exception("Doesn't look like a coverage data file: 0x{:08x} != 0x{:08x}".format(app_id, APP_ID)) - for path, id in self._db.execute("select path, id from file"): - self._file_map[path] = id + self._create_db() return self._db def _file_id(self, filename): @@ -103,6 +126,7 @@ def add_lines(self, line_data): """ self._start_writing() + self._choose_lines_or_arcs(lines=True) with self._connect() as con: for filename, linenos in iitems(line_data): file_id = self._file_id(filename) @@ -112,6 +136,36 @@ def add_lines(self, line_data): data, ) + def add_arcs(self, arc_data): + """Add measured arc data. + + `arc_data` is a dictionary mapping file names to dictionaries:: + + { filename: { (l1,l2): None, ... }, ...} + + """ + self._start_writing() + self._choose_lines_or_arcs(arcs=True) + with self._connect() as con: + for filename, arcs in iitems(arc_data): + file_id = self._file_id(filename) + data = [(file_id, fromno, tono) for fromno, tono in arcs] + con.executemany( + "insert or ignore into arc (file_id, fromno, tono) values (?, ?, ?)", + data, + ) + + def _choose_lines_or_arcs(self, lines=False, arcs=False): + if lines and self._has_arcs: + raise CoverageException("Can't add lines to existing arc data") + if arcs and self._has_lines: + raise CoverageException("Can't add arcs to existing line data") + if not self._has_arcs and not self._has_lines: + self._has_lines = lines + self._has_arcs = arcs + with self._connect() as con: + con.execute("update meta set has_lines = ?, has_arcs = ?", (lines, arcs)) + def add_file_tracers(self, file_tracers): """Add per-file plugin information. @@ -120,10 +174,11 @@ def add_file_tracers(self, file_tracers): """ self._start_writing() with self._connect() as con: - for filename, tracer in iitems(file_tracers): - con.execute( + data = list(iitems(file_tracers)) + if data: + con.executemany( "insert into file (path, tracer) values (?, ?) on duplicate key update", - (filename, tracer), + data, ) def erase(self, parallel=False): @@ -135,7 +190,7 @@ def erase(self, parallel=False): """ self._reset() if self._debug and self._debug.should('dataio'): - self._debug.write("Erasing data file %r" % (self.filename,)) + self._debug.write("Erasing data file {!r}".format(self.filename)) file_be_gone(self.filename) if parallel: data_dir, local = os.path.split(self.filename) @@ -143,7 +198,7 @@ def erase(self, parallel=False): pattern = os.path.join(os.path.abspath(data_dir), localdot) for filename in glob.glob(pattern): if self._debug and self._debug.should('dataio'): - self._debug.write("Erasing parallel data file %r" % (filename,)) + self._debug.write("Erasing parallel data file {!r}".format(filename)) file_be_gone(filename) def read(self): @@ -160,7 +215,7 @@ def _start_writing(self): self._have_read = True def has_arcs(self): - return False # TODO! + return self._has_arcs def measured_files(self): """A list of all files that had been measured.""" @@ -181,6 +236,11 @@ def lines(self, filename): file_id = self._file_id(filename) return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] + def arcs(self, filename): + with self._connect() as con: + file_id = self._file_id(filename) + return [pair for pair in con.execute("select fromno, tono from arc where file_id = ?", (file_id,))] + class Sqlite(object): def __init__(self, filename, debug): From 2f0d57856550ef7ad248e4e6127700bdabb91e7d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 4 Aug 2018 07:36:13 -0400 Subject: [PATCH 132/952] Pull combine_parallel_data out of CoverageData --- coverage/control.py | 4 +- coverage/data.py | 134 +++++++++++++++++++++++--------------------- tests/test_data.py | 12 ++-- 3 files changed, 77 insertions(+), 73 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 1760ee785..2f084cc21 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -15,7 +15,7 @@ from coverage.backward import string_class, iitems from coverage.collector import Collector from coverage.config import read_coverage_config -from coverage.data import CoverageData +from coverage.data import CoverageData, combine_parallel_data from coverage.debug import DebugControl, write_formatted_info from coverage.disposition import disposition_debug_msg from coverage.files import PathAliases, set_relative_directory, abs_file @@ -536,7 +536,7 @@ def combine(self, data_paths=None, strict=False): for pattern in paths[1:]: aliases.add(pattern, result) - self._data.combine_parallel_data(aliases=aliases, data_paths=data_paths, strict=strict) + combine_parallel_data(self._data, aliases=aliases, data_paths=data_paths, strict=strict) def get_data(self): """Get the collected data. diff --git a/coverage/data.py b/coverage/data.py index e9243166f..0b3b640bb 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -594,69 +594,6 @@ def update(self, other_data, aliases=None): self._validate() - def combine_parallel_data(self, aliases=None, data_paths=None, strict=False): - """Combine a number of data files together. - - Treat `self.filename` as a file prefix, and combine the data from all - of the data files starting with that prefix plus a dot. - - If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. - - If `data_paths` is provided, it is a list of directories or files to - combine. Directories are searched for files that start with - `self.filename` plus dot as a prefix, and those files are combined. - - If `data_paths` is not provided, then the directory portion of - `self.filename` is used as the directory to search for data files. - - Every data file found and combined is then deleted from disk. If a file - cannot be read, a warning will be issued, and the file will not be - deleted. - - If `strict` is true, and no files are found to combine, an error is - raised. - - """ - # Because of the os.path.abspath in the constructor, data_dir will - # never be an empty string. - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - - data_paths = data_paths or [data_dir] - files_to_combine = [] - for p in data_paths: - if os.path.isfile(p): - files_to_combine.append(os.path.abspath(p)) - elif os.path.isdir(p): - pattern = os.path.join(os.path.abspath(p), localdot) - files_to_combine.extend(glob.glob(pattern)) - else: - raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) - - if strict and not files_to_combine: - raise CoverageException("No data to combine") - - files_combined = 0 - for f in files_to_combine: - new_data = CoverageData(debug=self._debug) - try: - new_data._read_file(f) - except CoverageException as exc: - if self._warn: - # The CoverageException has the file name in it, so just - # use the message as the warning. - self._warn(str(exc)) - else: - self.update(new_data, aliases=aliases) - files_combined += 1 - if self._debug and self._debug.should('dataio'): - self._debug.write("Deleting combined data file %r" % (f,)) - file_be_gone(f) - - if strict and not files_combined: - raise CoverageException("No usable data files") - ## ## Miscellaneous ## @@ -731,9 +668,76 @@ def _has_arcs(self): return self._arcs is not None -from coverage.sqldata import CoverageSqliteData -CoverageData = CoverageSqliteData +which = os.environ.get("COV_STORAGE", "json") +if which == "json": + CoverageData = CoverageJsonData +elif which == "sql": + from coverage.sqldata import CoverageSqliteData + CoverageData = CoverageSqliteData + +def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): + """Combine a number of data files together. + + Treat `data.filename` as a file prefix, and combine the data from all + of the data files starting with that prefix plus a dot. + + If `aliases` is provided, it's a `PathAliases` object that is used to + re-map paths to match the local machine's. + + If `data_paths` is provided, it is a list of directories or files to + combine. Directories are searched for files that start with + `data.filename` plus dot as a prefix, and those files are combined. + + If `data_paths` is not provided, then the directory portion of + `data.filename` is used as the directory to search for data files. + + Every data file found and combined is then deleted from disk. If a file + cannot be read, a warning will be issued, and the file will not be + deleted. + + If `strict` is true, and no files are found to combine, an error is + raised. + + """ + # Because of the os.path.abspath in the constructor, data_dir will + # never be an empty string. + data_dir, local = os.path.split(data.filename) + localdot = local + '.*' + + data_paths = data_paths or [data_dir] + files_to_combine = [] + for p in data_paths: + if os.path.isfile(p): + files_to_combine.append(os.path.abspath(p)) + elif os.path.isdir(p): + pattern = os.path.join(os.path.abspath(p), localdot) + files_to_combine.extend(glob.glob(pattern)) + else: + raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) + + if strict and not files_to_combine: + raise CoverageException("No data to combine") + + files_combined = 0 + for f in files_to_combine: + try: + new_data = CoverageData(f, debug=data._debug) + new_data.read() + except CoverageException as exc: + if data._warn: + # The CoverageException has the file name in it, so just + # use the message as the warning. + data._warn(str(exc)) + else: + data.update(new_data, aliases=aliases) + files_combined += 1 + if data._debug and data._debug.should('dataio'): + data._debug.write("Deleting combined data file %r" % (f,)) + file_be_gone(f) + + if strict and not files_combined: + raise CoverageException("No usable data files") def canonicalize_json_data(data): """Canonicalize our JSON data so it can be compared.""" diff --git a/tests/test_data.py b/tests/test_data.py index 5deccef00..702f4554a 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -11,7 +11,7 @@ import mock -from coverage.data import CoverageData, debug_main, canonicalize_json_data +from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -611,7 +611,7 @@ def test_combining(self): self.assert_exists(".coverage.2") covdata3 = CoverageData() - covdata3.combine_parallel_data() + combine_parallel_data(covdata3) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) self.assert_doesnt_exist(".coverage.1") @@ -713,7 +713,7 @@ def test_combining_with_aliases(self): aliases = PathAliases() aliases.add("/home/ned/proj/src/", "./") aliases.add(r"c:\ned\test", "./") - covdata3.combine_parallel_data(aliases=aliases) + combine_parallel_data(covdata3, aliases=aliases) apy = canonical_filename('./a.py') sub_bpy = canonical_filename('./sub/b.py') @@ -740,7 +740,7 @@ def test_combining_from_different_directories(self): covdata_xxx.write() covdata3 = CoverageData() - covdata3.combine_parallel_data(data_paths=['cov1', 'cov2']) + combine_parallel_data(covdata3, data_paths=['cov1', 'cov2']) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) @@ -769,7 +769,7 @@ def test_combining_from_files(self): covdata_2xxx.write() covdata3 = CoverageData() - covdata3.combine_parallel_data(data_paths=['cov1', 'cov2/.coverage.2']) + combine_parallel_data(covdata3, data_paths=['cov1', 'cov2/.coverage.2']) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) @@ -782,4 +782,4 @@ def test_combining_from_nonexistent_directories(self): covdata = CoverageData() msg = "Couldn't combine from non-existent path 'xyzzy'" with self.assertRaisesRegex(CoverageException, msg): - covdata.combine_parallel_data(data_paths=['xyzzy']) + combine_parallel_data(covdata, data_paths=['xyzzy']) From f1561b04f58fdd04b86d2ed0dc858a1222752b02 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 4 Aug 2018 10:03:47 -0400 Subject: [PATCH 133/952] Improved debugging --- coverage/debug.py | 17 +++++++++++++++++ coverage/misc.py | 10 ---------- coverage/results.py | 3 ++- coverage/sqldata.py | 19 ++++++++++++++----- doc/cmd.rst | 2 ++ 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index d63a90705..fd27c7314 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -31,6 +31,8 @@ class DebugControl(object): """Control and output for debugging.""" + show_repr_attr = False # For SimpleRepr + def __init__(self, options, output): """Configure the options and output file for debugging.""" self.options = list(options) + FORCED_DEBUG @@ -71,6 +73,10 @@ def write(self, msg): `msg` is the line to write. A newline will be appended. """ + if self.should('self'): + caller_self = inspect.stack()[1][0].f_locals.get('self') + if caller_self is not None: + msg = "[self: {!r}] {}".format(caller_self, msg) self.output.write(msg+"\n") if self.should('callers'): dump_stack_frames(out=self.output, skip=1) @@ -167,6 +173,17 @@ def add_pid_and_tid(text): return text +class SimpleRepr(object): + """A mixin implementing a simple __repr__.""" + def __repr__(self): + show_attrs = ((k, v) for k, v in self.__dict__.items() if getattr(v, "show_repr_attr", True)) + return "<{klass} @0x{id:x} {attrs}>".format( + klass=self.__class__.__name__, + id=id(self), + attrs=" ".join("{}={!r}".format(k, v) for k, v in show_attrs), + ) + + def filter_text(text, filters): """Run `text` through a series of filters. diff --git a/coverage/misc.py b/coverage/misc.py index fff2a1875..78ec027fe 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -249,16 +249,6 @@ def _needs_to_implement(that, func_name): ) -class SimpleRepr(object): - """A mixin implementing a simple __repr__.""" - def __repr__(self): - return "<{klass} @{id:x} {attrs}>".format( - klass=self.__class__.__name__, - id=id(self) & 0xFFFFFF, - attrs=" ".join("{}={!r}".format(k, v) for k, v in self.__dict__.items()), - ) - - class BaseCoverageException(Exception): """The base of all Coverage exceptions.""" pass diff --git a/coverage/results.py b/coverage/results.py index 7e3bd268d..fb919c9b8 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -6,7 +6,8 @@ import collections from coverage.backward import iitems -from coverage.misc import contract, format_lines, SimpleRepr +from coverage.debug import SimpleRepr +from coverage.misc import contract, format_lines class Analysis(object): diff --git a/coverage/sqldata.py b/coverage/sqldata.py index cddece312..296e353e1 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -7,6 +7,7 @@ import sqlite3 from coverage.backward import iitems +from coverage.debug import SimpleRepr from coverage.misc import CoverageException, file_be_gone @@ -47,7 +48,7 @@ # (-1065448850,) APP_ID = -1065448850 -class CoverageSqliteData(object): +class CoverageSqliteData(SimpleRepr): def __init__(self, basename=None, warn=None, debug=None): self.filename = os.path.abspath(basename or ".coverage") self._warn = warn @@ -125,6 +126,10 @@ def add_lines(self, line_data): { filename: { lineno: None, ... }, ...} """ + if self._debug and self._debug.should('dataop'): + self._debug.write("Adding lines: %d files, %d lines total" % ( + len(line_data), sum(len(lines) for lines in line_data.values()) + )) self._start_writing() self._choose_lines_or_arcs(lines=True) with self._connect() as con: @@ -144,6 +149,10 @@ def add_arcs(self, arc_data): { filename: { (l1,l2): None, ... }, ...} """ + if self._debug and self._debug.should('dataop'): + self._debug.write("Adding arcs: %d files, %d arcs total" % ( + len(arc_data), sum(len(arcs) for arcs in arc_data.values()) + )) self._start_writing() self._choose_lines_or_arcs(arcs=True) with self._connect() as con: @@ -242,7 +251,7 @@ def arcs(self, filename): return [pair for pair in con.execute("select fromno, tono from arc where file_id = ?", (file_id,))] -class Sqlite(object): +class Sqlite(SimpleRepr): def __init__(self, filename, debug): self.debug = debug if (debug and debug.should('sql')) else None if self.debug: @@ -250,9 +259,9 @@ def __init__(self, filename, debug): self.con = sqlite3.connect(filename) # This pragma makes writing faster. It disables rollbacks, but we never need them. - self.con.execute("pragma journal_mode=off") + self.execute("pragma journal_mode=off") # This pragma makes writing faster. - self.con.execute("pragma synchronous=off") + self.execute("pragma synchronous=off") def close(self): self.con.close() @@ -272,5 +281,5 @@ def execute(self, sql, parameters=()): def executemany(self, sql, data): if self.debug: - self.debug.write("Executing many {!r}".format(sql)) + self.debug.write("Executing many {!r} with {} rows".format(sql, len(data))) return self.con.executemany(sql, data) diff --git a/doc/cmd.rst b/doc/cmd.rst index d198178f6..908b2ee98 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -486,6 +486,8 @@ to log: * ``process``: show process creation information, and changes in the current directory. +* ``self``: annotate each debug message with the object printing the message. + * ``sys``: before starting, dump all the system and environment information, as with :ref:`coverage debug sys `. From b147ea9dafe38e08083842f89502fefd9ba790d7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 5 Aug 2018 21:40:40 -0400 Subject: [PATCH 134/952] Simple tool to compare json and sql storage --- lab/gendata.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lab/gendata.py diff --git a/lab/gendata.py b/lab/gendata.py new file mode 100644 index 000000000..0e9c6b6fa --- /dev/null +++ b/lab/gendata.py @@ -0,0 +1,40 @@ +import random +import time + +from coverage.data import CoverageJsonData +from coverage.sqldata import CoverageSqliteData + +NUM_FILES = 1000 +NUM_LINES = 1000 + +def gen_data(cdata): + rnd = random.Random() + rnd.seed(17) + + def linenos(num_lines, prob): + return (n for n in range(num_lines) if random.random() < prob) + + start = time.time() + for i in range(NUM_FILES): + filename = f"/src/foo/project/file{i}.py" + line_data = { filename: dict.fromkeys(linenos(NUM_LINES, .6)) } + cdata.add_lines(line_data) + + cdata.write() + end = time.time() + delta = end - start + return delta + +class DummyData: + def add_lines(self, line_data): + return + def write(self): + return + +overhead = gen_data(DummyData()) +jtime = gen_data(CoverageJsonData("gendata.json")) - overhead +stime = gen_data(CoverageSqliteData("gendata.db")) - overhead +print(f"Overhead: {overhead:.3f}s") +print(f"JSON: {jtime:.3f}s") +print(f"SQLite: {stime:.3f}s") +print(f"{stime / jtime:.3f}x slower") From e4eeacc837105c6b83d509454bec31b8fe41f84c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 5 Aug 2018 21:41:29 -0400 Subject: [PATCH 135/952] Remove use_cache --- coverage/control.py | 6 ------ doc/api_coverage.rst | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index a5943aa8d..03238910f 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -385,12 +385,6 @@ def set_option(self, option_name, value): """ self.config.set_option(option_name, value) - def use_cache(self, usecache): - """Obsolete method.""" - self._init() - if not usecache: - self._warn("use_cache(False) is no longer supported.") - def load(self): """Load previously-collected coverage data from the data file.""" self._init() diff --git a/doc/api_coverage.rst b/doc/api_coverage.rst index 9ee959671..334336050 100644 --- a/doc/api_coverage.rst +++ b/doc/api_coverage.rst @@ -12,7 +12,7 @@ The Coverage class .. autoclass:: Coverage :members: - :exclude-members: use_cache, sys_info + :exclude-members: sys_info :special-members: __init__ From 0efbc7e1634c7717ac242bcf92f42f979271e2f2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 5 Aug 2018 21:42:34 -0400 Subject: [PATCH 136/952] Better wrapping --- coverage/cmdline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index fba1112ff..2ebf7031e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -390,8 +390,10 @@ def get_prog_name(self): class CoverageScript(object): """The command-line interface to coverage.py.""" - def __init__(self, _covpkg=None, _run_python_file=None, - _run_python_module=None, _help_fn=None, _path_exists=None): + def __init__( + self, _covpkg=None, _run_python_file=None, + _run_python_module=None, _help_fn=None, _path_exists=None, + ): # _covpkg is for dependency injection, so we can test this code. if _covpkg: self.covpkg = _covpkg From eb52c3a3d8a7e5105f4da78da87a6a629fe9982d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 6 Aug 2018 18:38:24 -0400 Subject: [PATCH 137/952] Update testing dependencies --- doc/requirements.pip | 4 ++-- requirements/dev.pip | 2 +- requirements/pytest.pip | 4 ++-- requirements/tox.pip | 2 +- tox.ini | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/requirements.pip b/doc/requirements.pip index c1297408e..71b171348 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -3,9 +3,9 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ pyenchant==2.0.0 -sphinx==1.7.5 +sphinx==1.7.6 sphinxcontrib-spelling==4.2.0 -sphinx_rtd_theme==0.4.0 +sphinx_rtd_theme==0.4.1 # A version of doc8 with a -q flag. git+https://github.com/nedbat/doc8.git#egg=doc8==0.0 diff --git a/requirements/dev.pip b/requirements/dev.pip index f9fa82657..d7ed31317 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -11,7 +11,7 @@ pip==18.0.0 -r pytest.pip # for linting. -greenlet==0.4.13 +greenlet==0.4.14 mock==2.0.0 PyContracts==1.8.3 pylint==1.9.2 diff --git a/requirements/pytest.pip b/requirements/pytest.pip index b23977b74..23264ee93 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -3,6 +3,6 @@ # The pytest specifics used by coverage.py -pytest==3.6.3 -pytest-xdist==1.22.2 +pytest==3.7.1 +pytest-xdist==1.22.5 flaky==3.4.0 diff --git a/requirements/tox.pip b/requirements/tox.pip index 43b921c69..a84d77e9e 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -1,4 +1,4 @@ # The version of tox used by coverage.py -tox==3.1.2 +tox==3.1.3 # Adds env recreation on requirements file changes. tox-battery==0.5.1 diff --git a/tox.ini b/tox.ini index 102a7f546..0f81c200b 100644 --- a/tox.ini +++ b/tox.ini @@ -21,8 +21,8 @@ deps = #-e/Users/ned/unittest_mixins # gevent 1.3 causes a failure: https://bitbucket.org/ned/coveragepy/issues/663/gevent-132-on-windows-fails py{27,34,35,36}: gevent==1.2.2 - py{27,34,35,36,37}: eventlet==0.23.0 - py{27,34,35,36,37}: greenlet==0.4.13 + py{27,34,35,36,37}: eventlet==0.24.1 + py{27,34,35,36,37}: greenlet==0.4.14 # Windows can't update the pip version with pip running, so use Python # to install things. From b1c2eedd9c4637c4074d62251d532dc2cfe44a64 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 7 Aug 2018 06:05:05 -0400 Subject: [PATCH 138/952] assert_file_count --- tests/coveragetest.py | 8 ++++++++ tests/test_api.py | 3 +++ tests/test_data.py | 2 +- tests/test_testing.py | 17 +++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index caddbeedd..94f508528 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -6,6 +6,7 @@ import contextlib import datetime import functools +import glob import os import random import re @@ -347,6 +348,13 @@ def assert_doesnt_exist(self, fname): msg = "File %r shouldn't exist" % fname self.assertTrue(not os.path.exists(fname), msg) + def assert_file_count(self, pattern, count): + """Assert that there are `count` files matching `pattern`.""" + files = glob.glob(pattern) + msg = "There should be {} files matching {!r}, but there are these: {}" + msg = msg.format(count, pattern, files) + self.assertEqual(len(files), count, msg) + def assert_starts_with(self, s, prefix, msg=None): """Assert that `s` starts with `prefix`.""" if not s.startswith(prefix): diff --git a/tests/test_api.py b/tests/test_api.py index 248784ffb..d0b8efe9c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -357,6 +357,7 @@ def make_good_data_files(self): cov = coverage.Coverage(data_suffix=True) self.start_import_stop(cov, "code2") cov.save() + self.assert_file_count(".coverage.*", 2) def make_bad_data_file(self): """Make one bad data file.""" @@ -387,6 +388,8 @@ def test_combining_twice(self): cov1.combine() cov1.save() self.check_code1_code2(cov1) + self.assert_file_count(".coverage.*", 0) + self.assert_exists(".coverage") cov2 = coverage.Coverage() with self.assertRaisesRegex(CoverageException, r"No data to combine"): diff --git a/tests/test_data.py b/tests/test_data.py index 0d3172d42..5e7b7fc40 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -582,7 +582,7 @@ def test_explicit_suffix(self): self.assert_doesnt_exist(".coverage") def test_true_suffix(self): - self.assertEqual(glob.glob(".coverage.*"), []) + self.assert_file_count(".coverage.*", 0) # suffix=True will make a randomly named data file. covdata1 = CoverageData() diff --git a/tests/test_testing.py b/tests/test_testing.py index d8cd0ef05..2b01584ec 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -51,6 +51,23 @@ def test_file_exists(self): with self.assertRaises(AssertionError): self.assert_exists("shadow.txt") + def test_file_count(self): + self.make_file("abcde.txt", "abcde") + self.make_file("axczz.txt", "axczz") + self.make_file("afile.txt", "afile") + self.assert_file_count("a*.txt", 3) + self.assert_file_count("*c*.txt", 2) + self.assert_file_count("afile.*", 1) + self.assert_file_count("*.q", 0) + with self.assertRaises(AssertionError): + self.assert_file_count("a*.txt", 13) + with self.assertRaises(AssertionError): + self.assert_file_count("*c*.txt", 12) + with self.assertRaises(AssertionError): + self.assert_file_count("afile.*", 11) + with self.assertRaises(AssertionError): + self.assert_file_count("*.q", 10) + def test_assert_startwith(self): self.assert_starts_with("xyzzy", "xy") self.assert_starts_with("xyz\nabc", "xy") From 2f966f127499acc90fafc4dd361aef280e55862c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 7 Aug 2018 06:28:00 -0400 Subject: [PATCH 139/952] Pytest should always summarize the failing tests at the end --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 1ca402b31..69c64e7ed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = -q -n3 --strict --no-flaky-report +addopts = -q -n3 --strict --no-flaky-report -rfe markers = expensive: too slow to run during "make smoke" From 305c5fdc378879adaac518ddeac7c22a0c92f49d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 7 Aug 2018 07:17:56 -0400 Subject: [PATCH 140/952] Check data file existence while testing combining --- tests/test_api.py | 3 ++- tests/test_data.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index d0b8efe9c..a860c7da4 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -379,8 +379,9 @@ def test_combining_corrupt_data(self): # We got the results from code1 and code2 properly. self.check_code1_code2(cov) - # The bad file still exists. + # The bad file still exists, but it's the only parallel data file left. self.assert_exists(".coverage.foo") + self.assert_file_count(".coverage.*", 1) def test_combining_twice(self): self.make_good_data_files() diff --git a/tests/test_data.py b/tests/test_data.py index 5e7b7fc40..59c4a5bb7 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -604,26 +604,25 @@ def test_true_suffix(self): self.assertTrue(all(str(os.getpid()) in fn for fn in data_files2)) def test_combining(self): - self.assert_doesnt_exist(".coverage.1") - self.assert_doesnt_exist(".coverage.2") + self.assert_file_count(".coverage.*", 0) covdata1 = CoverageData() covdata1.add_lines(LINES_1) self.data_files.write(covdata1, suffix='1') self.assert_exists(".coverage.1") - self.assert_doesnt_exist(".coverage.2") + self.assert_file_count(".coverage.*", 1) covdata2 = CoverageData() covdata2.add_lines(LINES_2) self.data_files.write(covdata2, suffix='2') self.assert_exists(".coverage.2") + self.assert_file_count(".coverage.*", 2) covdata3 = CoverageData() self.data_files.combine_parallel_data(covdata3) self.assert_line_counts(covdata3, SUMMARY_1_2) self.assert_measured_files(covdata3, MEASURED_FILES_1_2) - self.assert_doesnt_exist(".coverage.1") - self.assert_doesnt_exist(".coverage.2") + self.assert_file_count(".coverage.*", 0) def test_erasing(self): covdata1 = CoverageData() @@ -644,8 +643,7 @@ def test_erasing_parallel(self): self.make_file(".coverage") data_files = CoverageDataFiles("datafile") data_files.erase(parallel=True) - self.assert_doesnt_exist("datafile.1") - self.assert_doesnt_exist("datafile.2") + self.assert_file_count("datafile.*", 0) self.assert_exists(".coverage") def read_json_data_file(self, fname): @@ -719,11 +717,16 @@ def test_combining_with_aliases(self): }) self.data_files.write(covdata2, suffix='2') + self.assert_file_count(".coverage.*", 2) + covdata3 = CoverageData() aliases = PathAliases() aliases.add("/home/ned/proj/src/", "./") aliases.add(r"c:\ned\test", "./") self.data_files.combine_parallel_data(covdata3, aliases=aliases) + self.assert_file_count(".coverage.*", 0) + # covdata3 hasn't been written yet. Should this file exist or not? + #self.assert_exists(".coverage") apy = canonical_filename('./a.py') sub_bpy = canonical_filename('./sub/b.py') From f4dd527cfdb0d0e996a2b30311ada6ec1395692d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 7 Aug 2018 10:10:45 -0400 Subject: [PATCH 141/952] Trying to diagnose a failure https://github.com/pytest-dev/pytest/issues/3753#issuecomment-411022974 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 8b0165ae0..786bc4658 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,7 @@ install: script: - tox + - pip freeze - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then python igor.py combine_html; fi - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then pip install codecov; fi - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then codecov -X gcov --file coverage.xml; fi From a6bdd9c634e296993e7795840ade3d3c8f618e5e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 7 Aug 2018 10:52:47 -0400 Subject: [PATCH 142/952] More diagnosis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 786bc4658..c2840e297 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ env: install: - pip install -r requirements/ci.pip + - pip freeze script: - tox From e3fee935a7c7c3cdd52b28d5da41010bc6a41af1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 7 Aug 2018 11:58:32 -0400 Subject: [PATCH 143/952] Force pluggy version --- requirements/pytest.pip | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/pytest.pip b/requirements/pytest.pip index 23264ee93..e8c59d295 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -4,5 +4,6 @@ # The pytest specifics used by coverage.py pytest==3.7.1 +pluggy>=0.7 # pytest needs this, but pip doesn't understand pytest-xdist==1.22.5 flaky==3.4.0 From 7ee6a3e1179cd102d8d15510970e1d77657190a0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 8 Aug 2018 20:33:00 -0400 Subject: [PATCH 144/952] Simplify counting data files --- tests/test_process.py | 64 ++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/tests/test_process.py b/tests/test_process.py index e022e727c..1d2771496 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -25,14 +25,6 @@ class ProcessTest(CoverageTest): """Tests of the per-process behavior of coverage.py.""" - def data_files(self): - """Return the names of coverage data files in this directory.""" - return [f for f in os.listdir('.') if (f.startswith('.coverage.') or f == '.coverage')] - - def number_of_data_files(self): - """Return the number of coverage data files in this directory.""" - return len(self.data_files()) - def test_save_on_exit(self): self.make_file("mycode.py", """\ h = "Hello" @@ -79,21 +71,21 @@ def test_combine_parallel_data(self): out = self.run_command("coverage run -p b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 1) out = self.run_command("coverage run -p b_or_c.py c") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") # After two -p runs, there should be two .coverage.machine.123 files. - self.assertEqual(self.number_of_data_files(), 2) + self.assert_file_count(".coverage.*", 2) # Combine the parallel coverage data files into .coverage . self.run_command("coverage combine") self.assert_exists(".coverage") # After combining, there should be only the .coverage file. - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) # Read the coverage file and see that b_or_c.py has all 8 lines # executed. @@ -117,14 +109,14 @@ def test_combine_parallel_data_with_a_corrupt_file(self): out = self.run_command("coverage run -p b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 1) out = self.run_command("coverage run -p b_or_c.py c") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") # After two -p runs, there should be two .coverage.machine.123 files. - self.assertEqual(self.number_of_data_files(), 2) + self.assert_file_count(".coverage.*", 2) # Make a bogus data file. self.make_file(".coverage.bad", "This isn't a coverage data file.") @@ -140,7 +132,7 @@ def test_combine_parallel_data_with_a_corrupt_file(self): self.assertRegex(out, warning_regex) # After combining, those two should be the only data files. - self.assertEqual(self.number_of_data_files(), 2) + self.assert_file_count(".coverage.*", 1) # Read the coverage file and see that b_or_c.py has all 8 lines # executed. @@ -154,7 +146,7 @@ def test_combine_no_usable_files(self): out = self.run_command("coverage run b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_exists(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) # Make bogus data files. self.make_file(".coverage.bad1", "This isn't a coverage data file.") @@ -174,7 +166,8 @@ def test_combine_no_usable_files(self): self.assertRegex(out, r"No usable data files") # After combining, we should have a main file and two parallel files. - self.assertEqual(self.number_of_data_files(), 3) + self.assert_exists(".coverage") + self.assert_file_count(".coverage.*", 2) # Read the coverage file and see that b_or_c.py has 6 lines # executed (we only did b, not c). @@ -188,24 +181,24 @@ def test_combine_parallel_data_in_two_steps(self): out = self.run_command("coverage run -p b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 1) # Combine the (one) parallel coverage data file into .coverage . self.run_command("coverage combine") self.assert_exists(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) out = self.run_command("coverage run -p b_or_c.py c") self.assertEqual(out, 'done\n') self.assert_exists(".coverage") - self.assertEqual(self.number_of_data_files(), 2) + self.assert_file_count(".coverage.*", 1) # Combine the parallel coverage data files into .coverage . self.run_command("coverage combine --append") self.assert_exists(".coverage") # After combining, there should be only the .coverage file. - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) # Read the coverage file and see that b_or_c.py has all 8 lines # executed. @@ -219,17 +212,17 @@ def test_combine_parallel_data_no_append(self): out = self.run_command("coverage run -p b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 1) # Combine the (one) parallel coverage data file into .coverage . self.run_command("coverage combine") self.assert_exists(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) out = self.run_command("coverage run -p b_or_c.py c") self.assertEqual(out, 'done\n') self.assert_exists(".coverage") - self.assertEqual(self.number_of_data_files(), 2) + self.assert_file_count(".coverage.*", 1) # Combine the parallel coverage data files into .coverage, but don't # use the data in .coverage already. @@ -237,7 +230,7 @@ def test_combine_parallel_data_no_append(self): self.assert_exists(".coverage") # After combining, there should be only the .coverage file. - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) # Read the coverage file and see that b_or_c.py has only 7 lines # because we didn't keep the data from running b. @@ -251,12 +244,12 @@ def test_append_data(self): out = self.run_command("coverage run b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_exists(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) out = self.run_command("coverage run --append b_or_c.py c") self.assertEqual(out, 'done\n') self.assert_exists(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) # Read the coverage file and see that b_or_c.py has all 8 lines # executed. @@ -294,7 +287,7 @@ def test_append_can_create_a_data_file(self): out = self.run_command("coverage run --append b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_exists(".coverage") - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) # Read the coverage file and see that b_or_c.py has only 6 lines # executed. @@ -319,7 +312,7 @@ def test_combine_with_rc(self): self.assert_doesnt_exist(".coverage") # After two runs, there should be two .coverage.machine.123 files. - self.assertEqual(self.number_of_data_files(), 2) + self.assert_file_count(".coverage.*", 2) # Combine the parallel coverage data files into .coverage . self.run_command("coverage combine") @@ -327,7 +320,7 @@ def test_combine_with_rc(self): self.assert_exists(".coveragerc") # After combining, there should be only the .coverage file. - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) # Read the coverage file and see that b_or_c.py has all 8 lines # executed. @@ -375,13 +368,13 @@ def test_combine_with_aliases(self): out = self.run_command("coverage run " + os.path.normpath("d2/x.py")) self.assertEqual(out, '4 5\n') - self.assertEqual(self.number_of_data_files(), 2) + self.assert_file_count(".coverage.*", 2) self.run_command("coverage combine") self.assert_exists(".coverage") # After combining, there should be only the .coverage file. - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) # Read the coverage data file and see that the two different x.py # files have been combined together. @@ -534,19 +527,20 @@ def main(): # After running the forking program, there should be two # .coverage.machine.123 files. - self.assertEqual(self.number_of_data_files(), 2) + self.assert_file_count(".coverage.*", 2) # The two data files should have different random numbers at the end of # the file name. - nums = set(name.rpartition(".")[-1] for name in self.data_files()) - self.assertEqual(len(nums), 2, "Same random: %s" % (self.data_files(),)) + data_files = glob.glob(".coverage.*") + nums = set(name.rpartition(".")[-1] for name in data_files) + self.assertEqual(len(nums), 2, "Same random: %s" % (data_files,)) # Combine the parallel coverage data files into .coverage . self.run_command("coverage combine") self.assert_exists(".coverage") # After combining, there should be only the .coverage file. - self.assertEqual(self.number_of_data_files(), 1) + self.assert_file_count(".coverage.*", 0) data = coverage.CoverageData() data.read_file(".coverage") From b44dae19881b970b05913b09911dea5c3951eaf9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 8 Aug 2018 22:13:59 -0400 Subject: [PATCH 145/952] No need to erase before running --- coverage/cmdline.py | 4 ---- tests/test_cmdline.py | 16 ---------------- 2 files changed, 20 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 2ebf7031e..c21acda64 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -619,10 +619,6 @@ def do_run(self, options, args): ) return ERR - if not self.coverage.get_option("run:parallel"): - if not options.append: - self.coverage.erase() - # Run the script. self.coverage.start() code_ran = True diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index b6fad76df..b8a659f17 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -358,7 +358,6 @@ def test_run(self): # run calls coverage.erase first. self.cmd_executes("run foo.py", """\ .Coverage() - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -386,7 +385,6 @@ def test_run(self): # --timid sets a flag, and program arguments get passed through. self.cmd_executes("run --timid foo.py abc 123", """\ .Coverage(timid=True) - .erase() .start() .run_python_file('foo.py', ['foo.py', 'abc', '123']) .stop() @@ -395,7 +393,6 @@ def test_run(self): # -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) - .erase() .start() .run_python_file('foo.py', ['foo.py', '-a', '-b']) .stop() @@ -403,7 +400,6 @@ def test_run(self): """) self.cmd_executes("run --branch foo.py", """\ .Coverage(branch=True) - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -411,7 +407,6 @@ def test_run(self): """) self.cmd_executes("run --rcfile=myrc.rc foo.py", """\ .Coverage(config_file="myrc.rc") - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -419,7 +414,6 @@ def test_run(self): """) self.cmd_executes("run --include=pre1,pre2 foo.py", """\ .Coverage(include=["pre1", "pre2"]) - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -427,7 +421,6 @@ def test_run(self): """) self.cmd_executes("run --omit=opre1,opre2 foo.py", """\ .Coverage(omit=["opre1", "opre2"]) - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -435,7 +428,6 @@ def test_run(self): """) self.cmd_executes("run --include=pre1,pre2 --omit=opre1,opre2 foo.py", """\ .Coverage(include=["pre1", "pre2"], omit=["opre1", "opre2"]) - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -443,7 +435,6 @@ def test_run(self): """) self.cmd_executes("run --source=quux,hi.there,/home/bar foo.py", """\ .Coverage(source=["quux", "hi.there", "/home/bar"]) - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -451,7 +442,6 @@ def test_run(self): """) self.cmd_executes("run --concurrency=gevent foo.py", """\ .Coverage(concurrency='gevent') - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -459,7 +449,6 @@ def test_run(self): """) self.cmd_executes("run --concurrency=multiprocessing foo.py", """\ .Coverage(concurrency='multiprocessing') - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -492,7 +481,6 @@ def test_multiprocessing_needs_config_file(self): def test_run_debug(self): self.cmd_executes("run --debug=opt1 foo.py", """\ .Coverage(debug=["opt1"]) - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -500,7 +488,6 @@ def test_run_debug(self): """) self.cmd_executes("run --debug=opt1,opt2 foo.py", """\ .Coverage(debug=["opt1","opt2"]) - .erase() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -510,7 +497,6 @@ def test_run_debug(self): def test_run_module(self): self.cmd_executes("run -m mymodule", """\ .Coverage() - .erase() .start() .run_python_module('mymodule', ['mymodule']) .stop() @@ -518,7 +504,6 @@ def test_run_module(self): """) self.cmd_executes("run -m mymodule -qq arg1 arg2", """\ .Coverage() - .erase() .start() .run_python_module('mymodule', ['mymodule', '-qq', 'arg1', 'arg2']) .stop() @@ -526,7 +511,6 @@ def test_run_module(self): """) self.cmd_executes("run --branch -m mymodule", """\ .Coverage(branch=True) - .erase() .start() .run_python_module('mymodule', ['mymodule']) .stop() From 85725034b429fe46cf26429ce3bad0d53db82f3e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 21:15:24 -0400 Subject: [PATCH 146/952] Simplify how run --append works. I don't know why it was using combine after, when .load before seems like the obvious way to do it. --- coverage/cmdline.py | 10 ++++------ tests/test_cmdline.py | 18 +++++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 1b7955d3f..14948d1c4 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -392,7 +392,7 @@ class CoverageScript(object): def __init__( self, _covpkg=None, _run_python_file=None, - _run_python_module=None, _help_fn=None, _path_exists=None, + _run_python_module=None, _help_fn=None, ): # _covpkg is for dependency injection, so we can test this code. if _covpkg: @@ -405,7 +405,6 @@ def __init__( self.run_python_file = _run_python_file or run_python_file self.run_python_module = _run_python_module or run_python_module self.help_fn = _help_fn or self.help - self.path_exists = _path_exists or os.path.exists self.global_option = False self.coverage = None @@ -619,6 +618,9 @@ def do_run(self, options, args): ) return ERR + if options.append: + self.coverage.load() + # Run the script. self.coverage.start() code_ran = True @@ -634,10 +636,6 @@ def do_run(self, options, args): finally: self.coverage.stop() if code_ran: - if options.append: - data_file = self.coverage.get_option("run:data_file") - if self.path_exists(data_file): - self.coverage.combine(data_paths=[data_file]) self.coverage.save() return OK diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index ecd4d8b39..b12f92ea6 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -70,34 +70,31 @@ def model_object(self): return mk - def mock_command_line(self, args, path_exists=None): + def mock_command_line(self, args): """Run `args` through the command line, with a Mock. Returns the Mock it used and the status code returned. """ m = self.model_object() - m.path_exists.return_value = path_exists 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, ) return m, ret - def cmd_executes(self, args, code, ret=OK, path_exists=None): + def cmd_executes(self, args, code, ret=OK): """Assert that the `args` end up executing the sequence in `code`.""" - m1, r1 = self.mock_command_line(args, path_exists=path_exists) + m1, r1 = self.mock_command_line(args) self.assertEqual(r1, ret, "Wrong status: got %r, wanted %r" % (r1, ret)) # Remove all indentation, and change ".foo()" to "m2.foo()". code = re.sub(r"(?m)^\s+", "", code) code = re.sub(r"(?m)^\.", "m2.", code) m2 = self.model_object() - m2.path_exists.return_value = path_exists code_obj = compile(code, "", "exec") eval(code_obj, globals(), {'m2': m2}) # pylint: disable=eval-used @@ -366,22 +363,21 @@ def test_run(self): # run -a combines with an existing data file before saving. self.cmd_executes("run -a foo.py", """\ .Coverage() + .load() .start() .run_python_file('foo.py', ['foo.py']) .stop() - .path_exists('.coverage') - .combine(data_paths=['.coverage']) .save() - """, path_exists=True) + """) # run -a doesn't combine anything if the data file doesn't exist. self.cmd_executes("run -a foo.py", """\ .Coverage() + .load() .start() .run_python_file('foo.py', ['foo.py']) .stop() - .path_exists('.coverage') .save() - """, 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) From 3335bb8df9226fbb3fb71dca65b7f795ee5c9552 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 21:32:59 -0400 Subject: [PATCH 147/952] Keep the env var naming scheme --- coverage/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/data.py b/coverage/data.py index 0b3b640bb..db9cd5267 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -668,7 +668,7 @@ def _has_arcs(self): return self._arcs is not None -which = os.environ.get("COV_STORAGE", "json") +which = os.environ.get("COVERAGE_STORAGE", "json") if which == "json": CoverageData = CoverageJsonData elif which == "sql": From 88b20059eb8902b38b76921b42fd7cb4c23346be Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 22:22:38 -0400 Subject: [PATCH 148/952] Clean any .coverage files we find --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 29ecff372..4512ad477 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ clean: -rm -f coverage/*,cover -rm -f MANIFEST -rm -f .coverage .coverage.* coverage.xml .metacov* + -rm -f */.coverage */*/.coverage */*/*/.coverage */*/*/*/.coverage */*/*/*/*/.coverage */*/*/*/*/*/.coverage -rm -f tests/zipmods.zip -rm -rf tests/eggsrc/build tests/eggsrc/dist tests/eggsrc/*.egg-info -rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz From 02b2bd8afe3cd171e4bd454ccf244f788ccded3c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 22:22:45 -0400 Subject: [PATCH 149/952] Forgot an import --- coverage/sqldata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 296e353e1..80188fca4 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,6 +3,7 @@ """Sqlite coverage data.""" +import glob import os import sqlite3 From 90bb6a77e02cbac6a23723b5907d5f59d1db1b82 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 10 Aug 2018 16:15:00 -0400 Subject: [PATCH 150/952] Move a common method outside the data classes --- coverage/data.py | 29 +++++++++++++++-------------- coverage/html.py | 3 ++- coverage/sqldata.py | 7 +++++++ tests/test_data.py | 9 +++++---- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index db9cd5267..9c82ccef5 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -641,20 +641,6 @@ def _validate_invariants(self): for key in val: assert isinstance(key, string_class), "Key in _runs shouldn't be %r" % (key,) - def add_to_hash(self, filename, hasher): - """Contribute `filename`'s data to the `hasher`. - - `hasher` is a `coverage.misc.Hasher` instance to be updated with - the file's data. It should only get the results data, not the run - data. - - """ - if self._has_arcs(): - hasher.update(sorted(self.arcs(filename) or [])) - else: - hasher.update(sorted(self.lines(filename) or [])) - hasher.update(self.file_tracer(filename)) - ## ## Internal ## @@ -676,6 +662,21 @@ def _has_arcs(self): CoverageData = CoverageSqliteData +def add_data_to_hash(data, filename, hasher): + """Contribute `filename`'s data to the `hasher`. + + `hasher` is a `coverage.misc.Hasher` instance to be updated with + the file's data. It should only get the results data, not the run + data. + + """ + if data.has_arcs(): + hasher.update(sorted(data.arcs(filename) or [])) + else: + hasher.update(sorted(data.lines(filename) or [])) + hasher.update(data.file_tracer(filename)) + + def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): """Combine a number of data files together. diff --git a/coverage/html.py b/coverage/html.py index 186e9d22e..2acc2656e 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -12,6 +12,7 @@ import coverage from coverage import env from coverage.backward import iitems +from coverage.data import add_data_to_hash from coverage.files import flat_rootname from coverage.misc import CoverageException, file_be_gone, Hasher, isolate_module from coverage.report import Reporter @@ -169,7 +170,7 @@ def file_hash(self, source, fr): """Compute a hash that changes if the file needs to be re-reported.""" m = Hasher() m.update(source) - self.data.add_to_hash(fr.filename, m) + add_data_to_hash(self.data, fr.filename, m) return m.hexdigest() def html_file(self, fr, analysis): diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 80188fca4..25a6d62da 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -242,6 +242,13 @@ def file_tracer(self, filename): return "" # TODO def lines(self, filename): + if self.has_arcs(): + arcs = self.arcs(filename) + if arcs is not None: + import itertools + all_lines = itertools.chain.from_iterable(arcs) + return list(set(l for l in all_lines if l > 0)) + with self._connect() as con: file_id = self._file_id(filename) return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] diff --git a/tests/test_data.py b/tests/test_data.py index 68b2c3759..a450f90b6 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -12,6 +12,7 @@ import mock from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data +from coverage.data import add_data_to_hash from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -364,7 +365,7 @@ def test_add_to_hash_with_lines(self): covdata = CoverageData() covdata.add_lines(LINES_1) hasher = mock.Mock() - covdata.add_to_hash("a.py", hasher) + add_data_to_hash(covdata, "a.py", hasher) self.assertEqual(hasher.method_calls, [ mock.call.update([1, 2]), # lines mock.call.update(""), # file_tracer name @@ -375,7 +376,7 @@ def test_add_to_hash_with_arcs(self): covdata.add_arcs(ARCS_3) covdata.add_file_tracers({"y.py": "hologram_plugin"}) hasher = mock.Mock() - covdata.add_to_hash("y.py", hasher) + add_data_to_hash(covdata, "y.py", hasher) self.assertEqual(hasher.method_calls, [ mock.call.update([(-1, 17), (17, 23), (23, -1)]), # arcs mock.call.update("hologram_plugin"), # file_tracer name @@ -386,7 +387,7 @@ def test_add_to_lines_hash_with_missing_file(self): covdata = CoverageData() covdata.add_lines(LINES_1) hasher = mock.Mock() - covdata.add_to_hash("missing.py", hasher) + add_data_to_hash(covdata, "missing.py", hasher) self.assertEqual(hasher.method_calls, [ mock.call.update([]), mock.call.update(None), @@ -398,7 +399,7 @@ def test_add_to_arcs_hash_with_missing_file(self): covdata.add_arcs(ARCS_3) covdata.add_file_tracers({"y.py": "hologram_plugin"}) hasher = mock.Mock() - covdata.add_to_hash("missing.py", hasher) + add_data_to_hash(covdata, "missing.py", hasher) self.assertEqual(hasher.method_calls, [ mock.call.update([]), mock.call.update(None), From 8562aeb29eddf3349f5c363c1842f9822b18a450 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 10 Aug 2018 16:39:22 -0400 Subject: [PATCH 151/952] Move line_counts out of the data classes --- coverage/cmdline.py | 3 ++- coverage/data.py | 39 ++++++++++++++++++++------------------- tests/test_api.py | 3 ++- tests/test_concurrency.py | 3 ++- tests/test_data.py | 8 ++++---- tests/test_plugins.py | 9 +++++---- tests/test_process.py | 35 ++++++++++++++++++----------------- 7 files changed, 53 insertions(+), 47 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 14948d1c4..23d2aec3e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -14,6 +14,7 @@ from coverage import env from coverage.collector import CTracer +from coverage.data import line_counts from coverage.debug import info_formatter, info_header from coverage.execfile import run_python_file, run_python_module from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource @@ -660,7 +661,7 @@ def do_debug(self, args): print("path: %s" % self.coverage.get_data().filename) if data: print("has_arcs: %r" % data.has_arcs()) - summary = data.line_counts(fullpath=True) + summary = line_counts(data, fullpath=True) filenames = sorted(summary.keys()) print("\n%d files:" % len(filenames)) for f in filenames: diff --git a/coverage/data.py b/coverage/data.py index 9c82ccef5..44b754393 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -244,25 +244,6 @@ def measured_files(self): """A list of all files that had been measured.""" return list(self._arcs or self._lines or {}) - def line_counts(self, fullpath=False): - """Return a dict summarizing the line coverage data. - - Keys are based on the file names, and values are the number of executed - lines. If `fullpath` is true, then the keys are the full pathnames of - the files, otherwise they are the basenames of the files. - - Returns a dict mapping file names to counts of lines. - - """ - summ = {} - if fullpath: - filename_fn = lambda f: f - else: - filename_fn = os.path.basename - for filename in self.measured_files(): - summ[filename_fn(filename)] = len(self.lines(filename)) - return summ - def __nonzero__(self): return bool(self._lines or self._arcs) @@ -662,6 +643,26 @@ def _has_arcs(self): CoverageData = CoverageSqliteData +def line_counts(data, fullpath=False): + """Return a dict summarizing the line coverage data. + + Keys are based on the file names, and values are the number of executed + lines. If `fullpath` is true, then the keys are the full pathnames of + the files, otherwise they are the basenames of the files. + + Returns a dict mapping file names to counts of lines. + + """ + summ = {} + if fullpath: + filename_fn = lambda f: f + else: + filename_fn = os.path.basename + for filename in data.measured_files(): + summ[filename_fn(filename)] = len(data.lines(filename)) + return summ + + def add_data_to_hash(data, filename, hasher): """Contribute `filename`'s data to the `hasher`. diff --git a/tests/test_api.py b/tests/test_api.py index a860c7da4..3e7e2f06b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -13,6 +13,7 @@ import coverage from coverage import env from coverage.backward import StringIO, import_local_file +from coverage.data import line_counts from coverage.misc import CoverageException from coverage.report import Reporter @@ -576,7 +577,7 @@ def coverage_usepkgs(self, **kwargs): import usepkgs # pragma: nested # pylint: disable=import-error, unused-variable cov.stop() # pragma: nested data = cov.get_data() - summary = data.line_counts() + summary = line_counts(data) for k, v in list(summary.items()): assert k.endswith(".py") summary[k[:-3]] = v diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index a4f700ed4..9e2d73d9c 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -14,6 +14,7 @@ import coverage from coverage import env from coverage.backward import import_local_file +from coverage.data import line_counts from coverage.files import abs_file from tests.coveragetest import CoverageTest @@ -245,7 +246,7 @@ def try_some_code(self, code, concurrency, the_module, expected_out=None): print_simple_annotation(code, linenos) lines = line_count(code) - self.assertEqual(data.line_counts()['try_it.py'], lines) + self.assertEqual(line_counts(data)['try_it.py'], lines) def test_threads(self): code = (THREAD + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT) diff --git a/tests/test_data.py b/tests/test_data.py index a450f90b6..7ca6f6552 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -12,7 +12,7 @@ import mock from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data -from coverage.data import add_data_to_hash +from coverage.data import add_data_to_hash, line_counts from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -74,9 +74,9 @@ class DataTestHelpers(CoverageTest): """Test helpers for data tests.""" - def assert_line_counts(self, covdata, line_counts, fullpath=False): - """Check that the line_counts of `covdata` is `line_counts`.""" - self.assertEqual(covdata.line_counts(fullpath), line_counts) + def assert_line_counts(self, covdata, counts, fullpath=False): + """Check that the line_counts of `covdata` is `counts`.""" + self.assertEqual(line_counts(covdata, fullpath), counts) def assert_measured_files(self, covdata, measured): """Check that `covdata`'s measured files are `measured`.""" diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 0987e41ac..2d0f84266 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -8,6 +8,7 @@ import coverage from coverage import env from coverage.backward import StringIO +from coverage.data import line_counts from coverage.control import Plugins from coverage.misc import CoverageException @@ -369,19 +370,19 @@ def test_plugin2(self): _, statements, missing, _ = cov.analysis("foo_7.html") self.assertEqual(statements, [1, 2, 3, 4, 5, 6, 7]) self.assertEqual(missing, [1, 2, 3, 6, 7]) - self.assertIn("foo_7.html", cov.get_data().line_counts()) + self.assertIn("foo_7.html", line_counts(cov.get_data())) _, statements, missing, _ = cov.analysis("bar_4.html") self.assertEqual(statements, [1, 2, 3, 4]) self.assertEqual(missing, [1, 4]) - self.assertIn("bar_4.html", cov.get_data().line_counts()) + self.assertIn("bar_4.html", line_counts(cov.get_data())) - self.assertNotIn("quux_5.html", cov.get_data().line_counts()) + self.assertNotIn("quux_5.html", line_counts(cov.get_data())) _, statements, missing, _ = cov.analysis("uni_3.html") self.assertEqual(statements, [1, 2, 3]) self.assertEqual(missing, [1]) - self.assertIn("uni_3.html", cov.get_data().line_counts()) + self.assertIn("uni_3.html", line_counts(cov.get_data())) def test_plugin2_with_branch(self): self.make_render_and_caller() diff --git a/tests/test_process.py b/tests/test_process.py index ede866914..48083f220 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -16,6 +16,7 @@ import coverage from coverage import env, CoverageData +from coverage.data import line_counts from coverage.misc import output_encoding from tests.coveragetest import CoverageTest @@ -91,7 +92,7 @@ def test_combine_parallel_data(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) # Running combine again should fail, because there are no parallel data # files to combine. @@ -102,7 +103,7 @@ def test_combine_parallel_data(self): # And the originally combined data is still there. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_combine_parallel_data_with_a_corrupt_file(self): self.make_b_or_c_py() @@ -138,7 +139,7 @@ def test_combine_parallel_data_with_a_corrupt_file(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_combine_no_usable_files(self): # https://bitbucket.org/ned/coveragepy/issues/629/multiple-use-of-combine-leads-to-empty @@ -173,7 +174,7 @@ def test_combine_no_usable_files(self): # executed (we only did b, not c). data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 6) + self.assertEqual(line_counts(data)['b_or_c.py'], 6) def test_combine_parallel_data_in_two_steps(self): self.make_b_or_c_py() @@ -204,7 +205,7 @@ def test_combine_parallel_data_in_two_steps(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_combine_parallel_data_no_append(self): self.make_b_or_c_py() @@ -236,7 +237,7 @@ def test_combine_parallel_data_no_append(self): # because we didn't keep the data from running b. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 7) + self.assertEqual(line_counts(data)['b_or_c.py'], 7) def test_append_data(self): self.make_b_or_c_py() @@ -255,7 +256,7 @@ def test_append_data(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_append_data_with_different_file(self): self.make_b_or_c_py() @@ -279,7 +280,7 @@ def test_append_data_with_different_file(self): # executed. data = coverage.CoverageData(".mycovdata") data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) def test_append_can_create_a_data_file(self): self.make_b_or_c_py() @@ -293,7 +294,7 @@ def test_append_can_create_a_data_file(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 6) + self.assertEqual(line_counts(data)['b_or_c.py'], 6) def test_combine_with_rc(self): self.make_b_or_c_py() @@ -326,7 +327,7 @@ def test_combine_with_rc(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['b_or_c.py'], 8) + self.assertEqual(line_counts(data)['b_or_c.py'], 8) # Reporting should still work even with the .rc file out = self.run_command("coverage report") @@ -380,7 +381,7 @@ def test_combine_with_aliases(self): # files have been combined together. data = coverage.CoverageData() data.read() - summary = data.line_counts(fullpath=True) + summary = line_counts(data, fullpath=True) self.assertEqual(len(summary), 1) actual = os.path.normcase(os.path.abspath(list(summary.keys())[0])) expected = os.path.normcase(os.path.abspath('src/x.py')) @@ -544,7 +545,7 @@ def main(): data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['fork.py'], 9) + self.assertEqual(line_counts(data)['fork.py'], 9) def test_warnings_during_reporting(self): # While fixing issue #224, the warnings were being printed far too @@ -684,7 +685,7 @@ def test_fullcoverage(self): # pragma: no metacov # The actual number of executed lines in os.py when it's # imported is 120 or so. Just running os.getenv executes # about 5. - self.assertGreater(data.line_counts()['os.py'], 50) + self.assertGreater(line_counts(data)['os.py'], 50) def test_lang_c(self): if env.JYTHON: @@ -911,7 +912,7 @@ def excepthook(*args): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['excepthook.py'], 7) + self.assertEqual(line_counts(data)['excepthook.py'], 7) def test_excepthook_exit(self): if env.PYPY or env.JYTHON: @@ -1257,7 +1258,7 @@ def test_subprocess_with_pth_files(self): # pragma: no metacov self.assert_exists(".mycovdata") data = coverage.CoverageData(".mycovdata") data.read() - self.assertEqual(data.line_counts()['sub.py'], 3) + self.assertEqual(line_counts(data)['sub.py'], 3) def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov # https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of @@ -1281,7 +1282,7 @@ def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov self.assert_exists(".coverage") data = coverage.CoverageData() data.read() - self.assertEqual(data.line_counts()['sub.py'], 3) + self.assertEqual(line_counts(data)['sub.py'], 3) # assert that there are *no* extra data files left over after a combine data_files = glob.glob(os.getcwd() + '/.coverage*') @@ -1371,7 +1372,7 @@ def path(basename): self.assert_exists(".coverage") data = coverage.CoverageData() data.read() - summary = data.line_counts() + summary = line_counts(data) print(summary) self.assertEqual(summary[source + '.py'], 3) self.assertEqual(len(summary), 1) From 420c1b10ddeed1da66a2ffb81d7ac2af32939be5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 07:40:05 -0400 Subject: [PATCH 152/952] Implement more --- coverage/data.py | 6 ++-- coverage/sqldata.py | 71 ++++++++++++++++++++++++++++++++++++--------- tests/test_data.py | 19 +++++++----- 3 files changed, 71 insertions(+), 25 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 44b754393..4b8b7eb22 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -635,10 +635,10 @@ def _has_arcs(self): return self._arcs is not None -which = os.environ.get("COVERAGE_STORAGE", "json") -if which == "json": +STORAGE = os.environ.get("COVERAGE_STORAGE", "json") +if STORAGE == "json": CoverageData = CoverageJsonData -elif which == "sql": +elif STORAGE == "sql": from coverage.sqldata import CoverageSqliteData CoverageData = CoverageSqliteData diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 25a6d62da..9d25d92c3 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -6,6 +6,7 @@ import glob import os import sqlite3 +import struct from coverage.backward import iitems from coverage.debug import SimpleRepr @@ -45,9 +46,13 @@ ); """ -# >>> struct.unpack(">i", b"\xc0\x7e\x8a\x6e") # "coverage", kind of. -# (-1065448850,) -APP_ID = -1065448850 +APP_ID = 0xc07e8a6e # "coverage", kind of. + +def unsigned_to_signed(val): + return struct.unpack('>i', struct.pack('>I', val))[0] + +def signed_to_unsigned(val): + return struct.unpack('>I', struct.pack('>i', val))[0] class CoverageSqliteData(SimpleRepr): def __init__(self, basename=None, warn=None, debug=None): @@ -75,7 +80,7 @@ def _create_db(self): self._debug.write("Creating data file {!r}".format(self.filename)) self._db = Sqlite(self.filename, self._debug) with self._db: - self._db.execute("pragma application_id = {}".format(APP_ID)) + self._db.execute("pragma application_id = {}".format(unsigned_to_signed(APP_ID))) for stmt in SCHEMA.split(';'): stmt = stmt.strip() if stmt: @@ -91,10 +96,10 @@ def _open_db(self): self._db = Sqlite(self.filename, self._debug) with self._db: for app_id, in self._db.execute("pragma application_id"): - app_id = int(app_id) + app_id = signed_to_unsigned(int(app_id)) if app_id != APP_ID: raise CoverageException( - "File {!r} doesn't look like a coverage data file: " + "Couldn't use {!r}: wrong application_id: " "0x{:08x} != 0x{:08x}".format(self.filename, app_id, APP_ID) ) for row in self._db.execute("select has_lines, has_arcs from meta"): @@ -111,6 +116,19 @@ def _connect(self): self._create_db() return self._db + def __nonzero__(self): + try: + with self._connect() as con: + if self.has_arcs(): + rows = con.execute("select * from arc limit 1") + else: + rows = con.execute("select * from line limit 1") + return bool(list(rows)) + except CoverageException: + return False + + __bool__ = __nonzero__ + def _file_id(self, filename): self._start_writing() if filename not in self._file_map: @@ -184,13 +202,28 @@ def add_file_tracers(self, file_tracers): """ self._start_writing() with self._connect() as con: - data = list(iitems(file_tracers)) - if data: - con.executemany( - "insert into file (path, tracer) values (?, ?) on duplicate key update", - data, + for filename, plugin_name in iitems(file_tracers): + con.execute( + "update file set tracer = ? where path = ?", + (plugin_name, filename) ) + def touch_file(self, filename, plugin_name=""): + """Ensure that `filename` appears in the data, empty if needed. + + `plugin_name` is the name of the plugin resposible for this file. It is used + to associate the right filereporter, etc. + """ + if self._debug and self._debug.should('dataop'): + self._debug.write("Touching %r" % (filename,)) + if not self._has_arcs and not self._has_lines: + raise CoverageException("Can't touch files in an empty CoverageSqliteData") + + file_id = self._file_id(filename) + if plugin_name: + # Set the tracer for this file + self.add_file_tracers({filename: plugin_name}) + def erase(self, parallel=False): """Erase the data in this object. @@ -239,7 +272,10 @@ def file_tracer(self, filename): was not measured, then None is returned. """ - return "" # TODO + with self._connect() as con: + for tracer, in con.execute("select tracer from file where path = ?", (filename,)): + return tracer or "" + return None def lines(self, filename): if self.has_arcs(): @@ -258,13 +294,17 @@ def arcs(self, filename): file_id = self._file_id(filename) return [pair for pair in con.execute("select fromno, tono from arc where file_id = ?", (file_id,))] + def run_infos(self): + return [] # TODO + class Sqlite(SimpleRepr): def __init__(self, filename, debug): self.debug = debug if (debug and debug.should('sql')) else None if self.debug: self.debug.write("Connecting to {!r}".format(filename)) - self.con = sqlite3.connect(filename) + self.filename = filename + self.con = sqlite3.connect(self.filename) # This pragma makes writing faster. It disables rollbacks, but we never need them. self.execute("pragma journal_mode=off") @@ -285,7 +325,10 @@ def execute(self, sql, parameters=()): if self.debug: tail = " with {!r}".format(parameters) if parameters else "" self.debug.write("Executing {!r}{}".format(sql, tail)) - return self.con.execute(sql, parameters) + try: + return self.con.execute(sql, parameters) + except sqlite3.Error as exc: + raise CoverageException("Couldn't use data file {!r}: {}".format(self.filename, exc)) def executemany(self, sql, data): if self.debug: diff --git a/tests/test_data.py b/tests/test_data.py index 7ca6f6552..5e75b012b 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -12,7 +12,7 @@ import mock from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data -from coverage.data import add_data_to_hash, line_counts +from coverage.data import add_data_to_hash, line_counts, STORAGE from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -105,7 +105,7 @@ def assert_arcs3_data(self, covdata): class CoverageDataTest(DataTestHelpers, CoverageTest): """Test cases for CoverageData.""" - run_in_temp_dir = False + run_in_temp_dir = STORAGE == "sql" def test_empty_data_is_false(self): covdata = CoverageData() @@ -449,7 +449,7 @@ def test_read_write_arcs(self): self.assert_arcs3_data(covdata2) def test_read_errors(self): - msg = r"Couldn't read data from '.*[/\\]{0}': \S+" + msg = r"Couldn't .* '.*[/\\]{0}': \S+" self.make_file("xyzzy.dat", "xyzzy") with self.assertRaisesRegex(CoverageException, msg.format("xyzzy.dat")): @@ -463,11 +463,12 @@ def test_read_errors(self): covdata.read() self.assertFalse(covdata) - self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") - with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): - covdata = CoverageData("misleading.dat") - covdata.read() - self.assertFalse(covdata) + if STORAGE == "json": + self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") + with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): + covdata = CoverageData("misleading.dat") + covdata.read() + self.assertFalse(covdata) def test_debug_main(self): covdata1 = CoverageData(".coverage") @@ -640,6 +641,8 @@ def test_erasing_parallel(self): def read_json_data_file(self, fname): """Read a JSON data file for testing the JSON directly.""" + if STORAGE != "json": + self.skipTest("Not using JSON for data storage") with open(fname, 'r') as fdata: go_away = fdata.read(len(CoverageData._GO_AWAY)) self.assertEqual(go_away, CoverageData._GO_AWAY) From e70a13b69912591a81dfded0261fa3f847232ba1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 08:46:56 -0400 Subject: [PATCH 153/952] Don't add data by asking about data --- coverage/sqldata.py | 37 ++++++++++++++++++++++++++----------- tests/test_data.py | 7 +++++++ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 9d25d92c3..e84c82fc0 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -114,6 +114,7 @@ def _connect(self): self._open_db() else: self._create_db() + self._have_read = True return self._db def __nonzero__(self): @@ -129,13 +130,19 @@ def __nonzero__(self): __bool__ = __nonzero__ - def _file_id(self, filename): - self._start_writing() + def _file_id(self, filename, add=False): + """Get the file id for `filename`. + + If filename is not in the database yet, add if it `add` is True. + If `add` is not True, return None. + """ if filename not in self._file_map: - with self._connect() as con: - cur = con.execute("insert into file (path) values (?)", (filename,)) - self._file_map[filename] = cur.lastrowid - return self._file_map[filename] + if add: + self._start_writing() + with self._connect() as con: + cur = con.execute("insert into file (path) values (?)", (filename,)) + self._file_map[filename] = cur.lastrowid + return self._file_map.get(filename) def add_lines(self, line_data): """Add measured line data. @@ -153,7 +160,7 @@ def add_lines(self, line_data): self._choose_lines_or_arcs(lines=True) with self._connect() as con: for filename, linenos in iitems(line_data): - file_id = self._file_id(filename) + file_id = self._file_id(filename, add=True) data = [(file_id, lineno) for lineno in linenos] con.executemany( "insert or ignore into line (file_id, lineno) values (?, ?)", @@ -176,7 +183,7 @@ def add_arcs(self, arc_data): self._choose_lines_or_arcs(arcs=True) with self._connect() as con: for filename, arcs in iitems(arc_data): - file_id = self._file_id(filename) + file_id = self._file_id(filename, add=True) data = [(file_id, fromno, tono) for fromno, tono in arcs] con.executemany( "insert or ignore into arc (file_id, fromno, tono) values (?, ?, ?)", @@ -219,7 +226,7 @@ def touch_file(self, filename, plugin_name=""): if not self._has_arcs and not self._has_lines: raise CoverageException("Can't touch files in an empty CoverageSqliteData") - file_id = self._file_id(filename) + file_id = self._file_id(filename, add=True) if plugin_name: # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) @@ -287,12 +294,20 @@ def lines(self, filename): with self._connect() as con: file_id = self._file_id(filename) - return [lineno for lineno, in con.execute("select lineno from line where file_id = ?", (file_id,))] + if file_id is None: + return None + else: + linenos = con.execute("select lineno from line where file_id = ?", (file_id,)) + return [lineno for lineno, in linenos] def arcs(self, filename): with self._connect() as con: file_id = self._file_id(filename) - return [pair for pair in con.execute("select fromno, tono from arc where file_id = ?", (file_id,))] + if file_id is None: + return None + else: + arcs = con.execute("select fromno, tono from arc where file_id = ?", (file_id,)) + return [pair for pair in arcs] def run_infos(self): return [] # TODO diff --git a/tests/test_data.py b/tests/test_data.py index 5e75b012b..b2e4644c4 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -361,6 +361,13 @@ def test_update_file_tracer_vs_no_file_tracer(self): with self.assertRaisesRegex(CoverageException, msg): covdata2.update(covdata1) + def test_asking_isnt_measuring(self): + # Asking about an unmeasured file shouldn't make it seem measured. + covdata = CoverageData() + self.assert_measured_files(covdata, []) + self.assertEqual(covdata.arcs("missing.py"), None) + self.assert_measured_files(covdata, []) + def test_add_to_hash_with_lines(self): covdata = CoverageData() covdata.add_lines(LINES_1) From 0812699cab9226a342dd9b914d3e14ceccdf7691 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 16:42:44 -0400 Subject: [PATCH 154/952] A little better --- coverage/sqldata.py | 2 +- tests/test_data.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index e84c82fc0..ce78c63ba 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -226,7 +226,7 @@ def touch_file(self, filename, plugin_name=""): if not self._has_arcs and not self._has_lines: raise CoverageException("Can't touch files in an empty CoverageSqliteData") - file_id = self._file_id(filename, add=True) + self._file_id(filename, add=True) if plugin_name: # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) diff --git a/tests/test_data.py b/tests/test_data.py index b2e4644c4..424e1c155 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -763,14 +763,14 @@ def test_combining_from_different_directories(self): self.assert_exists(".coverage.xxx") def test_combining_from_files(self): + os.makedirs('cov1') covdata1 = CoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) - os.makedirs('cov1') covdata1.write() + os.makedirs('cov2') covdata2 = CoverageData('cov2/.coverage.2') covdata2.add_lines(LINES_2) - os.makedirs('cov2') covdata2.write() # This data won't be included. From 975ebec2f81c4e5c992d65e6984c8ab122feffa6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 16:45:19 -0400 Subject: [PATCH 155/952] Hasher needed more information about nesting to avoid collisions --- coverage/misc.py | 1 + tests/test_misc.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/coverage/misc.py b/coverage/misc.py index fff2a1875..50c8d8acd 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -226,6 +226,7 @@ def update(self, v): continue self.update(k) self.update(a) + self.md5.update(b'.') def hexdigest(self): """Retrieve the hex digest of the hash.""" diff --git a/tests/test_misc.py b/tests/test_misc.py index f3d485cc2..1d01537ba 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -47,6 +47,13 @@ def test_dict_hashing(self): h2.update({'b': 23, 'a': 17}) self.assertEqual(h1.hexdigest(), h2.hexdigest()) + def test_dict_collision(self): + h1 = Hasher() + h1.update({'a': 17, 'b': {'c': 1, 'd': 2}}) + h2 = Hasher() + h2.update({'a': 17, 'b': {'c': 1}, 'd': 2}) + self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + class RemoveFileTest(CoverageTest): """Tests of misc.file_be_gone.""" From c362e44f3ebeda9929c3537df96eecfa218d83c2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 19:08:19 -0400 Subject: [PATCH 156/952] Error handling in add_file_tracers --- coverage/sqldata.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index ce78c63ba..dbeced848 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,6 +3,10 @@ """Sqlite coverage data.""" +# TODO: check the schema +# TODO: factor out dataop debugging to a wrapper class? +# TODO: make sure all dataop debugging is in place somehow + import glob import os import sqlite3 @@ -210,6 +214,21 @@ def add_file_tracers(self, file_tracers): self._start_writing() with self._connect() as con: for filename, plugin_name in iitems(file_tracers): + file_id = self._file_id(filename) + if file_id is None: + raise CoverageException( + "Can't add file tracer data for unmeasured file '%s'" % (filename,) + ) + + cur = con.execute("select tracer from file where id = ?", (file_id,)) + [existing_plugin] = cur.fetchone() + if existing_plugin is not None and existing_plugin != plugin_name: + raise CoverageException( + "Conflicting file tracer name for '%s': %r vs %r" % ( + filename, existing_plugin, plugin_name, + ) + ) + con.execute( "update file set tracer = ? where path = ?", (plugin_name, filename) From 3d6b9819921f5be15168631452053c63424fa8d8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 11 Aug 2018 19:57:05 -0400 Subject: [PATCH 157/952] Sqlite update() method --- coverage/sqldata.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index dbeced848..996a2ae8c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -14,6 +14,7 @@ from coverage.backward import iitems from coverage.debug import SimpleRepr +from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone @@ -250,6 +251,37 @@ def touch_file(self, filename, plugin_name=""): # Set the tracer for this file self.add_file_tracers({filename: plugin_name}) + def update(self, other_data, aliases=None): + if self._has_lines and other_data._has_arcs: + raise CoverageException("Can't combine arc data with line data") + if self._has_arcs and other_data._has_lines: + raise CoverageException("Can't combine line data with arc data") + + aliases = aliases or PathAliases() + + # lines + if other_data._has_lines: + for filename in other_data.measured_files(): + lines = set(other_data.lines(filename)) + filename = aliases.map(filename) + lines.update(self.lines(filename) or ()) + self.add_lines({filename: lines}) + + # arcs + if other_data._has_arcs: + for filename in other_data.measured_files(): + arcs = set(other_data.arcs(filename)) + filename = aliases.map(filename) + arcs.update(self.arcs(filename) or ()) + self.add_arcs({filename: arcs}) + + # file_tracers + for filename in other_data.measured_files(): + other_plugin = other_data.file_tracer(filename) + filename = aliases.map(filename) + self.add_file_tracers({filename: other_plugin}) + + def erase(self, parallel=False): """Erase the data in this object. From 5997b823da8d60d909e776424d4ba488bb3927ec Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 12 Aug 2018 07:05:33 -0400 Subject: [PATCH 158/952] Start moving suffix to constructor --- coverage/data.py | 6 ++++-- tests/test_data.py | 29 +++++++++++++++-------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 4b8b7eb22..15d0a2731 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -112,7 +112,7 @@ class CoverageJsonData(object): # line data is easily recovered from the arcs: it is all the first elements # of the pairs that are greater than zero. - def __init__(self, basename=None, warn=None, debug=None): + def __init__(self, basename=None, suffix=None, warn=None, debug=None): """Create a CoverageData. `warn` is the warning function to use. @@ -125,6 +125,7 @@ def __init__(self, basename=None, warn=None, debug=None): self._warn = warn self._debug = debug self.filename = os.path.abspath(basename or ".coverage") + self.suffix = suffix # A map from canonical Python source file name to a dictionary in # which there's an entry for each line number that has been @@ -434,7 +435,7 @@ def touch_file(self, filename, plugin_name=""): self._validate() - def write(self, suffix=None): + def write(self): """Write the collected coverage data to a file. `suffix` is a suffix to append to the base file name. This can be used @@ -444,6 +445,7 @@ def write(self, suffix=None): """ filename = self.filename + suffix = self.suffix if suffix is True: # If data_suffix was a simple true value, then make a suffix with # plenty of distinguishing information. We do this here in diff --git a/tests/test_data.py b/tests/test_data.py index 424e1c155..ad4dc84a0 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -576,9 +576,9 @@ def test_debug_output_without_debug_option(self): def test_explicit_suffix(self): self.assert_doesnt_exist(".coverage.SUFFIX") - covdata = CoverageData() + covdata = CoverageData(suffix='SUFFIX') covdata.add_lines(LINES_1) - covdata.write(suffix='SUFFIX') + covdata.write() self.assert_exists(".coverage.SUFFIX") self.assert_doesnt_exist(".coverage") @@ -586,17 +586,17 @@ def test_true_suffix(self): self.assert_file_count(".coverage.*", 0) # suffix=True will make a randomly named data file. - covdata1 = CoverageData() + covdata1 = CoverageData(suffix=True) covdata1.add_lines(LINES_1) - covdata1.write(suffix=True) + covdata1.write() self.assert_doesnt_exist(".coverage") data_files1 = glob.glob(".coverage.*") self.assertEqual(len(data_files1), 1) # Another suffix=True will choose a different name. - covdata2 = CoverageData() + covdata2 = CoverageData(suffix=True) covdata2.add_lines(LINES_1) - covdata2.write(suffix=True) + covdata2.write() self.assert_doesnt_exist(".coverage") data_files2 = glob.glob(".coverage.*") self.assertEqual(len(data_files2), 2) @@ -607,15 +607,15 @@ def test_true_suffix(self): def test_combining(self): self.assert_file_count(".coverage.*", 0) - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_lines(LINES_1) - covdata1.write(suffix='1') + covdata1.write() self.assert_exists(".coverage.1") self.assert_file_count(".coverage.*", 1) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_lines(LINES_2) - covdata2.write(suffix='2') + covdata2.write() self.assert_exists(".coverage.2") self.assert_file_count(".coverage.*", 2) @@ -689,6 +689,7 @@ def test_file_format_with_arcs(self): self.assertNotIn('file_tracers', data) def test_writing_to_other_file(self): + self.skipTest("This will be deleted!") # TODO covdata = CoverageData(".otherfile") covdata.add_lines(LINES_1) covdata.write() @@ -700,7 +701,7 @@ def test_writing_to_other_file(self): self.assert_doesnt_exist(".coverage") def test_combining_with_aliases(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_lines({ '/home/ned/proj/src/a.py': {1: None, 2: None}, '/home/ned/proj/src/sub/b.py': {3: None}, @@ -709,14 +710,14 @@ def test_combining_with_aliases(self): covdata1.add_file_tracers({ '/home/ned/proj/src/template.html': 'html.plugin', }) - covdata1.write(suffix='1') + covdata1.write() - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_lines({ r'c:\ned\test\a.py': {4: None, 5: None}, r'c:\ned\test\sub\b.py': {3: None, 6: None}, }) - covdata2.write(suffix='2') + covdata2.write() self.assert_file_count(".coverage.*", 2) From b355058f6cb4e3c9aa9c88f8e60af97872a969c6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 9 Aug 2018 21:15:24 -0400 Subject: [PATCH 159/952] Simplify how run --append works. I don't know why it was using combine after, when .load before seems like the obvious way to do it. (cherry picked from commit 85725034b429fe46cf26429ce3bad0d53db82f3e) --- coverage/cmdline.py | 10 ++++------ tests/test_cmdline.py | 18 +++++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index c21acda64..2b8e8fb9c 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -392,7 +392,7 @@ class CoverageScript(object): def __init__( self, _covpkg=None, _run_python_file=None, - _run_python_module=None, _help_fn=None, _path_exists=None, + _run_python_module=None, _help_fn=None, ): # _covpkg is for dependency injection, so we can test this code. if _covpkg: @@ -405,7 +405,6 @@ def __init__( self.run_python_file = _run_python_file or run_python_file self.run_python_module = _run_python_module or run_python_module self.help_fn = _help_fn or self.help - self.path_exists = _path_exists or os.path.exists self.global_option = False self.coverage = None @@ -619,6 +618,9 @@ def do_run(self, options, args): ) return ERR + if options.append: + self.coverage.load() + # Run the script. self.coverage.start() code_ran = True @@ -634,10 +636,6 @@ def do_run(self, options, args): finally: self.coverage.stop() if code_ran: - if options.append: - data_file = self.coverage.get_option("run:data_file") - if self.path_exists(data_file): - self.coverage.combine(data_paths=[data_file]) self.coverage.save() return OK diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index b8a659f17..f4fbdbbb7 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -70,34 +70,31 @@ def model_object(self): return mk - def mock_command_line(self, args, path_exists=None): + def mock_command_line(self, args): """Run `args` through the command line, with a Mock. Returns the Mock it used and the status code returned. """ m = self.model_object() - m.path_exists.return_value = path_exists 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, ) return m, ret - def cmd_executes(self, args, code, ret=OK, path_exists=None): + def cmd_executes(self, args, code, ret=OK): """Assert that the `args` end up executing the sequence in `code`.""" - m1, r1 = self.mock_command_line(args, path_exists=path_exists) + m1, r1 = self.mock_command_line(args) self.assertEqual(r1, ret, "Wrong status: got %r, wanted %r" % (r1, ret)) # Remove all indentation, and change ".foo()" to "m2.foo()". code = re.sub(r"(?m)^\s+", "", code) code = re.sub(r"(?m)^\.", "m2.", code) m2 = self.model_object() - m2.path_exists.return_value = path_exists code_obj = compile(code, "", "exec") eval(code_obj, globals(), {'m2': m2}) # pylint: disable=eval-used @@ -366,22 +363,21 @@ def test_run(self): # run -a combines with an existing data file before saving. self.cmd_executes("run -a foo.py", """\ .Coverage() + .load() .start() .run_python_file('foo.py', ['foo.py']) .stop() - .path_exists('.coverage') - .combine(data_paths=['.coverage']) .save() - """, path_exists=True) + """) # run -a doesn't combine anything if the data file doesn't exist. self.cmd_executes("run -a foo.py", """\ .Coverage() + .load() .start() .run_python_file('foo.py', ['foo.py']) .stop() - .path_exists('.coverage') .save() - """, 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) From 8a337f91e6444c027771741a56636a56389706e3 Mon Sep 17 00:00:00 2001 From: Jakub Wilk Date: Mon, 13 Aug 2018 19:23:00 +0200 Subject: [PATCH 160/952] Fix typo --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0e80b26c7..4deb575dd 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -606,7 +606,7 @@ Version 4.1b2 --- 2016-01-23 - Class docstrings were considered executable. Now they no longer are. - ``yield from`` and ``await`` were considered returns from functions, since - they could tranfer control to the caller. This produced unhelpful "missing + they could transfer control to the caller. This produced unhelpful "missing branch" reports in a number of circumstances. Now they no longer are considered returns. From 0341a891a22f29466fd525bc5aa010c5d85bed52 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 07:32:08 -0400 Subject: [PATCH 161/952] Refactor initialization We need the data file suffix when the data file is created, not when write() is called. This required separating how different pieces were initialized. The old way was dumb anyway, since it (for example) created a Collector when reporting. --- coverage/control.py | 192 ++++++++++++++++++++++++------------------ tests/test_plugins.py | 6 +- 2 files changed, 112 insertions(+), 86 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 46c2ece1f..c83432afe 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -163,8 +163,11 @@ def __init__( # State machine variables: # Have we initialized everything? self._inited = False + self._inited_for_start = False # Have we started collecting and not stopped it? self._started = False + # Have we written --debug output? + self._wrote_debug = False # If we have sub-process measurement happening automatically, then we # want any explicit creation of a Coverage object to mean, this process @@ -214,73 +217,11 @@ def _init(self): # this is a bit childish. :) plugin.configure([self, self.config][int(time.time()) % 2]) - concurrency = self.config.concurrency or [] - if "multiprocessing" in concurrency: - if not patch_multiprocessing: - raise CoverageException( # pragma: only jython - "multiprocessing is not supported on this Python" - ) - patch_multiprocessing(rcfile=self.config.config_file) - # Multi-processing uses parallel for the subprocesses, so also use - # it for the main process. - self.config.parallel = True - - self._collector = Collector( - should_trace=self._should_trace, - check_include=self._check_include_omit_etc, - timid=self.config.timid, - branch=self.config.branch, - warn=self._warn, - concurrency=concurrency, - ) - - # Early warning if we aren't going to be able to support plugins. - if self._plugins.file_tracers and not self._collector.supports_plugins: - self._warn( - "Plugin file tracers (%s) aren't supported with %s" % ( - ", ".join( - plugin._coverage_plugin_name - for plugin in self._plugins.file_tracers - ), - self._collector.tracer_name(), - ) - ) - for plugin in self._plugins.file_tracers: - plugin._coverage_enabled = False - - # Create the file classifying substructure. - self._inorout = self._inorout_class(warn=self._warn) - self._inorout.configure(self.config) - self._inorout.plugins = self._plugins - self._inorout.disp_class = self._collector.file_disposition_class - - # Suffixes are a bit tricky. We want to use the data suffix only when - # collecting data, not when combining data. So we save it as - # `self._run_suffix` now, and promote it to `self._data_suffix` if we - # find that we are collecting data later. - if self._data_suffix_specified or self.config.parallel: - if not isinstance(self._data_suffix_specified, string_class): - # if data_suffix=True, use .machinename.pid.random - self._data_suffix_specified = True - else: - self._data_suffix_specified = None - self._data_suffix = None - self._run_suffix = self._data_suffix_specified - - # Create the data file. We do this at construction time so that the - # data file will be written into the directory where the process - # started rather than wherever the process eventually chdir'd to. - self._data = CoverageData( - basename=self.config.data_file, warn=self._warn, debug=self._debug, - ) - - # Set the reporting precision. - Numbers.set_precision(self.config.precision) - - atexit.register(self._atexit) - - # The user may want to debug things, show info if desired. - self._write_startup_debug() + def _post_init(self): + """Stuff to do after everything is initialized.""" + if not self._wrote_debug: + self._wrote_debug = True + self._write_startup_debug() def _write_startup_debug(self): """Write out debug info at startup if needed.""" @@ -387,9 +328,79 @@ def set_option(self, option_name, value): def load(self): """Load previously-collected coverage data from the data file.""" self._init() - self._collector.reset() + if self._collector: + self._collector.reset() + self._init_data(suffix=None) + self._post_init() self._data.read() + def _init_for_start(self): + """Initialization for start()""" + concurrency = self.config.concurrency or [] + if "multiprocessing" in concurrency: + if not patch_multiprocessing: + raise CoverageException( # pragma: only jython + "multiprocessing is not supported on this Python" + ) + patch_multiprocessing(rcfile=self.config.config_file) + # Multi-processing uses parallel for the subprocesses, so also use + # it for the main process. + self.config.parallel = True + + self._collector = Collector( + should_trace=self._should_trace, + check_include=self._check_include_omit_etc, + timid=self.config.timid, + branch=self.config.branch, + warn=self._warn, + concurrency=concurrency, + ) + + suffix = self._data_suffix_specified + if suffix or self.config.parallel: + if not isinstance(suffix, string_class): + # if data_suffix=True, use .machinename.pid.random + suffix = True + else: + suffix = None + + self._init_data(suffix) + + # Early warning if we aren't going to be able to support plugins. + if self._plugins.file_tracers and not self._collector.supports_plugins: + self._warn( + "Plugin file tracers (%s) aren't supported with %s" % ( + ", ".join( + plugin._coverage_plugin_name + for plugin in self._plugins.file_tracers + ), + self._collector.tracer_name(), + ) + ) + for plugin in self._plugins.file_tracers: + plugin._coverage_enabled = False + + # Create the file classifying substructure. + self._inorout = self._inorout_class(warn=self._warn) + self._inorout.configure(self.config) + self._inorout.plugins = self._plugins + self._inorout.disp_class = self._collector.file_disposition_class + + atexit.register(self._atexit) + + def _init_data(self, suffix): + """Create a data file if we don't have one yet.""" + if self._data is None: + # Create the data file. We do this at construction time so that the + # data file will be written into the directory where the process + # started rather than wherever the process eventually chdir'd to. + self._data = CoverageData( + basename=self.config.data_file, + suffix=suffix, + warn=self._warn, + debug=self._debug, + ) + def start(self): """Start measuring code coverage. @@ -402,19 +413,22 @@ def start(self): """ self._init() - self._inorout.warn_conflicting_settings() + if not self._inited_for_start: + self._inited_for_start = True + self._init_for_start() + self._post_init() - if self._run_suffix: - # Calling start() means we're running code, so use the run_suffix - # as the data_suffix when we eventually save the data. - self._data_suffix = self._run_suffix - if self._auto_load: - self.load() + # Issue warnings for possible problems. + self._inorout.warn_conflicting_settings() - # See if we think some code that would eventually be measured has already been imported. + # See if we think some code that would eventually be measured has + # already been imported. if self._warn_preimported_source: self._inorout.warn_already_imported_files() + if self._auto_load: + self.load() + self._collector.start() self._started = True @@ -441,7 +455,10 @@ def erase(self): """ self._init() - self._collector.reset() + self._post_init() + if self._collector: + self._collector.reset() + self._init_data(suffix=None) self._data.erase(parallel=self.config.parallel) def clear_exclude(self, which='exclude'): @@ -493,9 +510,8 @@ def get_exclude_list(self, which='exclude'): def save(self): """Save the collected coverage data to the data file.""" - self._init() data = self.get_data() - data.write(suffix=self._data_suffix) + data.write() def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. @@ -520,6 +536,8 @@ def combine(self, data_paths=None, strict=False): """ self._init() + self._init_data(suffix=None) + self._post_init() self.get_data() aliases = None @@ -544,7 +562,7 @@ def get_data(self): """ self._init() - if self._collector.save_data(self._data): + if self._collector and self._collector.save_data(self._data): self._post_save_work() return self._data @@ -595,7 +613,6 @@ def analysis2(self, morf): coverage data. """ - self._init() analysis = self._analyze(morf) return ( analysis.filename, @@ -611,6 +628,11 @@ def _analyze(self, it): Returns an `Analysis` object. """ + # All reporting comes through here, so do reporting initialization. + self._init() + Numbers.set_precision(self.config.precision) + self._post_init() + data = self.get_data() if not isinstance(it, FileReporter): it = self._get_file_reporter(it) @@ -797,6 +819,7 @@ def sys_info(self): import coverage as covmod self._init() + self._post_init() def plugin_info(plugins): """Make an entry for the sys_info from a list of plug-ins.""" @@ -811,13 +834,13 @@ def plugin_info(plugins): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('tracer', self._collector.tracer_name()), + ('tracer', self._collector.tracer_name() if self._collector else "-none-"), ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), ('plugins.configurers', plugin_info(self._plugins.configurers)), ('configs_attempted', self.config.attempted_config_files), ('configs_read', self.config.config_files_read), ('config_file', self.config.config_file), - ('data_path', self._data.filename), + ('data_path', self._data.filename if self._data else "-none-"), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), ('implementation', platform.python_implementation()), @@ -832,7 +855,8 @@ def plugin_info(plugins): ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] - info.extend(self._inorout.sys_info()) + if self._inorout: + info.extend(self._inorout.sys_info()) return info diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 2d0f84266..04eea3dff 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -190,7 +190,8 @@ def coverage_init(reg, options): cov = coverage.Coverage(debug=["sys"]) cov._debug_file = debug_out cov.set_option("run:plugins", ["plugin_sys_info"]) - cov.load() + cov.start() + cov.stop() out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] if env.C_TRACER: @@ -219,7 +220,8 @@ def coverage_init(reg, options): cov = coverage.Coverage(debug=["sys"]) cov._debug_file = debug_out cov.set_option("run:plugins", ["plugin_no_sys_info"]) - cov.load() + cov.start() + cov.stop() out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] self.assertIn('plugins.file_tracers: -none-', out_lines) From f087c213dbe2ffb1b4a0661c9d25e67915987a99 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 08:05:57 -0400 Subject: [PATCH 162/952] Remove an unused debugging thing --- coverage/data.py | 8 +------- coverage/debug.py | 3 --- tests/coveragetest.py | 7 ------- tests/test_farm.py | 5 ----- 4 files changed, 1 insertion(+), 22 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 15d0a2731..5e85fc10d 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -15,7 +15,6 @@ from coverage import env from coverage.backward import iitems, string_class -from coverage.debug import _TEST_NAME_FILE from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone, isolate_module @@ -451,13 +450,8 @@ def write(self): # plenty of distinguishing information. We do this here in # `save()` at the last minute so that the pid will be correct even # if the process forks. - extra = "" - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE) as f: - test_name = f.read() - extra = "." + test_name dice = random.Random(os.urandom(8)).randint(0, 999999) - suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) + suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) if suffix: filename += "." + suffix diff --git a/coverage/debug.py b/coverage/debug.py index fd27c7314..f491a0f79 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -24,9 +24,6 @@ # This is a list of forced debugging options. FORCED_DEBUG = [] -# A hack for debugging testing in sub-processes. -_TEST_NAME_FILE = "" # "/tmp/covtest.txt" - class DebugControl(object): """Control and output for debugging.""" diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 94f508528..9814d6484 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -24,7 +24,6 @@ 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 from coverage.misc import StopEverything from tests.helpers import run_command, SuperModuleCleaner @@ -91,12 +90,6 @@ def setUp(self): self.last_command_output = None self.last_module_name = None - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE, "w") as f: - f.write("%s_%s" % ( - self.__class__.__name__, self._testMethodName, - )) - def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. diff --git a/tests/test_farm.py b/tests/test_farm.py index 942bdd5c8..54eeb4992 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -20,7 +20,6 @@ from coverage import env from coverage.backunittest import unittest -from coverage.debug import _TEST_NAME_FILE # Look for files that become tests. @@ -105,10 +104,6 @@ def runTest(self): # pragma: not covered def __call__(self): # pylint: disable=arguments-differ """Execute the test from the runpy file.""" - 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 clean skip From da37af9a65b144ce6b1f26430bcbc9786e055f8b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 08:37:09 -0400 Subject: [PATCH 163/952] Move the suffix parameter, but no implementation yet --- coverage/sqldata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 996a2ae8c..5ae5e64d4 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -60,7 +60,7 @@ def signed_to_unsigned(val): return struct.unpack('>I', struct.pack('>i', val))[0] class CoverageSqliteData(SimpleRepr): - def __init__(self, basename=None, warn=None, debug=None): + def __init__(self, basename=None, suffix=None, warn=None, debug=None): self.filename = os.path.abspath(basename or ".coverage") self._warn = warn self._debug = debug @@ -306,7 +306,7 @@ def read(self): self._connect() # TODO: doesn't look right self._have_read = True - def write(self, suffix=None): + def write(self): """Write the collected coverage data to a file.""" pass From 067d0a60384b5f12cfee622381cfb5905efb8e13 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 20:38:39 -0400 Subject: [PATCH 164/952] Use pid-random suffixes for SQL files --- coverage/data.py | 21 ++++++++++++--------- coverage/sqldata.py | 4 ++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 5e85fc10d..aa23e7d4f 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -21,6 +21,17 @@ os = isolate_module(os) +def filename_suffix(suffix): + if suffix is True: + # If data_suffix was a simple true value, then make a suffix with + # plenty of distinguishing information. We do this here in + # `save()` at the last minute so that the pid will be correct even + # if the process forks. + dice = random.Random(os.urandom(8)).randint(0, 999999) + suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) + return suffix + + class CoverageJsonData(object): """Manages collected coverage data, including file storage. @@ -444,15 +455,7 @@ def write(self): """ filename = self.filename - suffix = self.suffix - if suffix is True: - # If data_suffix was a simple true value, then make a suffix with - # plenty of distinguishing information. We do this here in - # `save()` at the last minute so that the pid will be correct even - # if the process forks. - dice = random.Random(os.urandom(8)).randint(0, 999999) - suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) - + suffix = filename_suffix(self.suffix) if suffix: filename += "." + suffix self._write_file(filename) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 5ae5e64d4..f36a93852 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -13,6 +13,7 @@ import struct from coverage.backward import iitems +from coverage.data import filename_suffix from coverage.debug import SimpleRepr from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone @@ -62,6 +63,9 @@ def signed_to_unsigned(val): class CoverageSqliteData(SimpleRepr): def __init__(self, basename=None, suffix=None, warn=None, debug=None): self.filename = os.path.abspath(basename or ".coverage") + suffix = filename_suffix(suffix) + if suffix: + self.filename += "." + suffix self._warn = warn self._debug = debug From 9b13a1a7d44d991c4c5dd51d5624f5abe84b77f8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 20:39:31 -0400 Subject: [PATCH 165/952] Skip some tests for SQL for now --- coverage/sqldata.py | 3 +++ tests/coveragetest.py | 6 ++++++ tests/test_data.py | 6 ++++-- tests/test_process.py | 4 ++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index f36a93852..c79ad1758 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,9 +3,12 @@ """Sqlite coverage data.""" +# TODO: get rid of skip_unless_data_storage_is_json # TODO: check the schema # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow +# TODO: should writes be batched? +# TODO: settle the os.fork question import glob import os diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 9814d6484..cd6bb9fc1 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -24,6 +24,7 @@ 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.data import STORAGE from coverage.misc import StopEverything from tests.helpers import run_command, SuperModuleCleaner @@ -90,6 +91,11 @@ def setUp(self): self.last_command_output = None self.last_module_name = None + def skip_unless_data_storage_is_json(self): + if STORAGE != "json": + self.skipTest("Not using JSON for data storage") + + def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. diff --git a/tests/test_data.py b/tests/test_data.py index ad4dc84a0..876357eb8 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -187,6 +187,7 @@ def test_no_lines_vs_unmeasured_file(self): self.assertIsNone(covdata.lines('no_such_file.py')) def test_run_info(self): + self.skip_unless_data_storage_is_json() covdata = CoverageData() self.assertEqual(covdata.run_infos(), []) covdata.add_run_info(hello="there") @@ -265,6 +266,7 @@ def test_update_arcs(self): self.assertEqual(covdata3.run_infos(), []) def test_update_run_info(self): + self.skip_unless_data_storage_is_json() covdata1 = CoverageData() covdata1.add_arcs(ARCS_3) covdata1.add_run_info(hello="there", count=17) @@ -478,6 +480,7 @@ def test_read_errors(self): self.assertFalse(covdata) def test_debug_main(self): + self.skip_unless_data_storage_is_json() covdata1 = CoverageData(".coverage") covdata1.add_lines(LINES_1) covdata1.write() @@ -648,8 +651,7 @@ def test_erasing_parallel(self): def read_json_data_file(self, fname): """Read a JSON data file for testing the JSON directly.""" - if STORAGE != "json": - self.skipTest("Not using JSON for data storage") + self.skip_unless_data_storage_is_json() with open(fname, 'r') as fdata: go_away = fdata.read(len(CoverageData._GO_AWAY)) self.assertEqual(go_away, CoverageData._GO_AWAY) diff --git a/tests/test_process.py b/tests/test_process.py index 48083f220..7c7057395 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -504,6 +504,8 @@ def f1(): def test_fork(self): if not hasattr(os, 'fork'): self.skipTest("Can't test os.fork since it doesn't exist.") + # See https://nedbatchelder.com/blog/201808/sqlite_data_storage_for_coveragepy.html + self.skip_unless_data_storage_is_json() self.make_file("fork.py", """\ import os @@ -642,6 +644,8 @@ def test_note(self): if env.PYPY and env.PY3 and env.PYPYVERSION[:3] == (5, 10, 0): # pragma: obscure # https://bitbucket.org/pypy/pypy/issues/2729/pypy3-510-incorrectly-decodes-astral-plane self.skipTest("Avoid incorrect decoding astral plane JSON chars") + self.skip_unless_data_storage_is_json() + self.make_file(".coveragerc", """\ [run] data_file = mydata.dat From 19ec83bde56b6dfecef4ddae275376fdb4262e3a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 14 Aug 2018 20:39:57 -0400 Subject: [PATCH 166/952] Be flexible, and accept either json-sourced or sql-source error messages in some tests --- coverage/sqldata.py | 5 ++++- tests/test_api.py | 4 ++++ tests/test_data.py | 10 ++++++++-- tests/test_debug.py | 13 +++++++++---- tests/test_process.py | 13 ++++++++++++- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index c79ad1758..3abc3af34 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -4,11 +4,14 @@ """Sqlite coverage data.""" # TODO: get rid of skip_unless_data_storage_is_json +# TODO: get rid of "JSON message" and "SQL message" in the tests # TODO: check the schema +# TODO: get rid of the application_id? # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow # TODO: should writes be batched? # TODO: settle the os.fork question +# TODO: run_info import glob import os @@ -323,7 +326,7 @@ def _start_writing(self): self._have_read = True def has_arcs(self): - return self._has_arcs + return bool(self._has_arcs) def measured_files(self): """A list of all files that had been measured.""" diff --git a/tests/test_api.py b/tests/test_api.py index 3e7e2f06b..854f9cc2b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -371,8 +371,12 @@ def test_combining_corrupt_data(self): self.make_bad_data_file() cov = coverage.Coverage() warning_regex = ( + r"(" # JSON message: r"Couldn't read data from '.*\.coverage\.foo': " r"CoverageException: Doesn't seem to be a coverage\.py data file" + r"|" # SQL message: + r"Couldn't use data file '.*\.coverage\.foo': file is encrypted or is not a database" + r")" ) with self.assert_warnings(cov, [warning_regex]): cov.combine() diff --git a/tests/test_data.py b/tests/test_data.py index 876357eb8..317e04da0 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -559,8 +559,14 @@ def test_debug_output_with_debug_option(self): self.assertRegex( debug.get_output(), + r"(" # JSON output: r"^Writing data to '.*\.coverage'\n" r"Reading data from '.*\.coverage'\n$" + r"|" # SQL output: + r"Erasing data file '.*\.coverage'\n" + r"Creating data file '.*\.coverage'\n" + r"Opening data file '.*\.coverage'\n$" + r")" ) def test_debug_output_without_debug_option(self): @@ -741,14 +747,14 @@ def test_combining_with_aliases(self): self.assertEqual(covdata3.file_tracer(template_html), 'html.plugin') def test_combining_from_different_directories(self): + os.makedirs('cov1') covdata1 = CoverageData('cov1/.coverage.1') covdata1.add_lines(LINES_1) - os.makedirs('cov1') covdata1.write() + os.makedirs('cov2') covdata2 = CoverageData('cov2/.coverage.2') covdata2.add_lines(LINES_2) - os.makedirs('cov2') covdata2.write() # This data won't be included. diff --git a/tests/test_debug.py b/tests/test_debug.py index c46e3dae1..c47dd3439 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -128,8 +128,8 @@ def test_debug_trace_pid(self): def test_debug_callers(self): out_lines = self.f1_debug_output(["pid", "dataop", "dataio", "callers"]) 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" + # 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 = re_lines(out_lines, r" @\d+", match=False).splitlines() frame_pattern = r"\s+f1_debug_output : .*tests[/\\]test_debug.py @\d+$" frames = re_lines(out_lines, frame_pattern).splitlines() @@ -137,9 +137,14 @@ def test_debug_callers(self): # The last message should be "Writing data", and the last frame should # be _write_file in data.py. - 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+$") + from coverage.data import STORAGE + if STORAGE == "json": + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") + self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") + else: + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Creating data file") + self.assertRegex(last_line, r"\s+_create_db : .*coverage[/\\]sqldata.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) diff --git a/tests/test_process.py b/tests/test_process.py index 7c7057395..49919b0ff 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -127,8 +127,13 @@ def test_combine_parallel_data_with_a_corrupt_file(self): self.assert_exists(".coverage") self.assert_exists(".coverage.bad") warning_regex = ( + r"(" # JSON message: r"Coverage.py warning: Couldn't read data from '.*\.coverage\.bad': " r"CoverageException: Doesn't seem to be a coverage\.py data file" + r"|" # SQL message: + r"Coverage.py warning: Couldn't use data file '.*\.coverage\.bad': " + r"file is encrypted or is not a database" + r")" ) self.assertRegex(out, warning_regex) @@ -160,8 +165,14 @@ def test_combine_no_usable_files(self): for n in "12": self.assert_exists(".coverage.bad{0}".format(n)) warning_regex = ( + r"(" # JSON message: r"Coverage.py warning: Couldn't read data from '.*\.coverage\.bad{0}': " - r"CoverageException: Doesn't seem to be a coverage\.py data file".format(n) + r"CoverageException: Doesn't seem to be a coverage\.py data file" + r"|" # SQL message: + r"Coverage.py warning: Couldn't use data file '.*\.coverage.bad{0}': " + r"file is encrypted or is not a database" + r")" + .format(n) ) self.assertRegex(out, warning_regex) self.assertRegex(out, r"No usable data files") From f30f591be04a88dac2080505c241457d45f0314b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Aug 2018 11:12:40 -0400 Subject: [PATCH 167/952] Get file_tracer semantics right, whew --- coverage/sqldata.py | 79 +++++++++++++++++++++++++++++---------------- tests/test_data.py | 4 +-- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 3abc3af34..01082a9b5 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -40,7 +40,6 @@ create table file ( id integer primary key, path text, - tracer text, unique(path) ); @@ -56,6 +55,11 @@ tono integer, unique(file_id, fromno, tono) ); + +create table tracer ( + file_id integer primary key, + tracer text +); """ APP_ID = 0xc07e8a6e # "coverage", kind of. @@ -78,7 +82,7 @@ def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._file_map = {} self._db = None # Are we in sync with the data file? - self._have_read = False + self._have_used = False self._has_lines = False self._has_arcs = False @@ -88,7 +92,7 @@ def _reset(self): if self._db is not None: self._db.close() self._db = None - self._have_read = False + self._have_used = False def _create_db(self): if self._debug and self._debug.should('dataio'): @@ -129,7 +133,6 @@ def _connect(self): self._open_db() else: self._create_db() - self._have_read = True return self._db def __nonzero__(self): @@ -153,7 +156,6 @@ def _file_id(self, filename, add=False): """ if filename not in self._file_map: if add: - self._start_writing() with self._connect() as con: cur = con.execute("insert into file (path) values (?)", (filename,)) self._file_map[filename] = cur.lastrowid @@ -171,7 +173,7 @@ def add_lines(self, line_data): self._debug.write("Adding lines: %d files, %d lines total" % ( len(line_data), sum(len(lines) for lines in line_data.values()) )) - self._start_writing() + self._start_using() self._choose_lines_or_arcs(lines=True) with self._connect() as con: for filename, linenos in iitems(line_data): @@ -194,7 +196,7 @@ def add_arcs(self, arc_data): self._debug.write("Adding arcs: %d files, %d arcs total" % ( len(arc_data), sum(len(arcs) for arcs in arc_data.values()) )) - self._start_writing() + self._start_using() self._choose_lines_or_arcs(arcs=True) with self._connect() as con: for filename, arcs in iitems(arc_data): @@ -222,7 +224,7 @@ def add_file_tracers(self, file_tracers): `file_tracers` is { filename: plugin_name, ... } """ - self._start_writing() + self._start_using() with self._connect() as con: for filename, plugin_name in iitems(file_tracers): file_id = self._file_id(filename) @@ -231,26 +233,27 @@ def add_file_tracers(self, file_tracers): "Can't add file tracer data for unmeasured file '%s'" % (filename,) ) - cur = con.execute("select tracer from file where id = ?", (file_id,)) - [existing_plugin] = cur.fetchone() - if existing_plugin is not None and existing_plugin != plugin_name: - raise CoverageException( - "Conflicting file tracer name for '%s': %r vs %r" % ( - filename, existing_plugin, plugin_name, + existing_plugin = self.file_tracer(filename) + if existing_plugin: + if existing_plugin != plugin_name: + raise CoverageException( + "Conflicting file tracer name for '%s': %r vs %r" % ( + filename, existing_plugin, plugin_name, + ) ) + elif plugin_name: + con.execute( + "insert into tracer (file_id, tracer) values (?, ?)", + (file_id, plugin_name) ) - con.execute( - "update file set tracer = ? where path = ?", - (plugin_name, filename) - ) - def touch_file(self, filename, plugin_name=""): """Ensure that `filename` appears in the data, empty if needed. `plugin_name` is the name of the plugin resposible for this file. It is used to associate the right filereporter, etc. """ + self._start_using() if self._debug and self._debug.should('dataop'): self._debug.write("Touching %r" % (filename,)) if not self._has_arcs and not self._has_lines: @@ -269,6 +272,9 @@ def update(self, other_data, aliases=None): aliases = aliases or PathAliases() + # See what we had already measured, for accurate conflict reporting. + this_measured = set(self.measured_files()) + # lines if other_data._has_lines: for filename in other_data.measured_files(): @@ -289,8 +295,18 @@ def update(self, other_data, aliases=None): for filename in other_data.measured_files(): other_plugin = other_data.file_tracer(filename) filename = aliases.map(filename) - self.add_file_tracers({filename: other_plugin}) - + if filename in this_measured: + this_plugin = self.file_tracer(filename) + else: + this_plugin = None + if this_plugin is None: + self.add_file_tracers({filename: other_plugin}) + elif this_plugin != other_plugin: + raise CoverageException( + "Conflicting file tracer name for '%s': %r vs %r" % ( + filename, this_plugin, other_plugin, + ) + ) def erase(self, parallel=False): """Erase the data in this object. @@ -314,16 +330,16 @@ def erase(self, parallel=False): def read(self): self._connect() # TODO: doesn't look right - self._have_read = True + self._have_used = True def write(self): """Write the collected coverage data to a file.""" pass - def _start_writing(self): - if not self._have_read: + def _start_using(self): + if not self._have_used: self.erase() - self._have_read = True + self._have_used = True def has_arcs(self): return bool(self._has_arcs) @@ -340,12 +356,18 @@ def file_tracer(self, filename): was not measured, then None is returned. """ + self._start_using() with self._connect() as con: - for tracer, in con.execute("select tracer from file where path = ?", (filename,)): - return tracer or "" - return None + file_id = self._file_id(filename) + if file_id is None: + return None + row = con.execute("select tracer from tracer where file_id = ?", (file_id,)).fetchone() + if row is not None: + return row[0] or "" + return "" # File was measured, but no tracer associated. def lines(self, filename): + self._start_using() if self.has_arcs(): arcs = self.arcs(filename) if arcs is not None: @@ -362,6 +384,7 @@ def lines(self, filename): return [lineno for lineno, in linenos] def arcs(self, filename): + self._start_using() with self._connect() as con: file_id = self._file_id(filename) if file_id is None: diff --git a/tests/test_data.py b/tests/test_data.py index 317e04da0..ad8b805be 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -348,11 +348,11 @@ def test_update_conflicting_file_tracers(self): covdata2.update(covdata1) def test_update_file_tracer_vs_no_file_tracer(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix="1") covdata1.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) covdata1.add_file_tracers({"p1.html": "html.plugin"}) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix="2") covdata2.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) msg = "Conflicting file tracer name for 'p1.html': 'html.plugin' vs ''" From 6ee0473a77c8bd8c91681fa86e58acb55a6e44f4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Aug 2018 11:26:14 -0400 Subject: [PATCH 168/952] A better more accurate bool(data) --- coverage/sqldata.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 01082a9b5..f53561e76 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -138,10 +138,7 @@ def _connect(self): def __nonzero__(self): try: with self._connect() as con: - if self.has_arcs(): - rows = con.execute("select * from arc limit 1") - else: - rows = con.execute("select * from line limit 1") + rows = con.execute("select * from file limit 1") return bool(list(rows)) except CoverageException: return False From fd3dd69cc10026cf6d69925267134c11b281a803 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Aug 2018 12:17:25 -0400 Subject: [PATCH 169/952] Cop out for a json/sql difference in data types --- tests/test_data.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index ad8b805be..48df81fdd 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -231,7 +231,7 @@ def test_cant_change_file_tracer_name(self): covdata.add_lines({"p1.foo": dict.fromkeys([1, 2, 3])}) covdata.add_file_tracers({"p1.foo": "p1.plugin"}) - msg = "Conflicting file tracer name for 'p1.foo': 'p1.plugin' vs 'p1.plugin.foo'" + msg = "Conflicting file tracer name for 'p1.foo': u?'p1.plugin' vs u?'p1.plugin.foo'" with self.assertRaisesRegex(CoverageException, msg): covdata.add_file_tracers({"p1.foo": "p1.plugin.foo"}) @@ -339,11 +339,11 @@ def test_update_conflicting_file_tracers(self): covdata2.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) covdata2.add_file_tracers({"p1.html": "html.other_plugin"}) - msg = "Conflicting file tracer name for 'p1.html': 'html.plugin' vs 'html.other_plugin'" + msg = "Conflicting file tracer name for 'p1.html': u?'html.plugin' vs u?'html.other_plugin'" with self.assertRaisesRegex(CoverageException, msg): covdata1.update(covdata2) - msg = "Conflicting file tracer name for 'p1.html': 'html.other_plugin' vs 'html.plugin'" + msg = "Conflicting file tracer name for 'p1.html': u?'html.other_plugin' vs u?'html.plugin'" with self.assertRaisesRegex(CoverageException, msg): covdata2.update(covdata1) @@ -355,11 +355,11 @@ def test_update_file_tracer_vs_no_file_tracer(self): covdata2 = CoverageData(suffix="2") covdata2.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) - msg = "Conflicting file tracer name for 'p1.html': 'html.plugin' vs ''" + msg = "Conflicting file tracer name for 'p1.html': u?'html.plugin' vs u?''" with self.assertRaisesRegex(CoverageException, msg): covdata1.update(covdata2) - msg = "Conflicting file tracer name for 'p1.html': '' vs 'html.plugin'" + msg = "Conflicting file tracer name for 'p1.html': u?'' vs u?'html.plugin'" with self.assertRaisesRegex(CoverageException, msg): covdata2.update(covdata1) From b282c54ebaaae13aa8b81f2380cdc20acaa9fc69 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 18 Aug 2018 19:57:25 -0400 Subject: [PATCH 170/952] Make it run on PyPy for time tests there --- lab/gendata.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lab/gendata.py b/lab/gendata.py index 0e9c6b6fa..27ad4fda2 100644 --- a/lab/gendata.py +++ b/lab/gendata.py @@ -1,3 +1,5 @@ +# Run some timing tests of JsonData vs SqliteData. + import random import time @@ -16,7 +18,7 @@ def linenos(num_lines, prob): start = time.time() for i in range(NUM_FILES): - filename = f"/src/foo/project/file{i}.py" + filename = "/src/foo/project/file{i}.py".format(i=i) line_data = { filename: dict.fromkeys(linenos(NUM_LINES, .6)) } cdata.add_lines(line_data) @@ -34,7 +36,7 @@ def write(self): overhead = gen_data(DummyData()) jtime = gen_data(CoverageJsonData("gendata.json")) - overhead stime = gen_data(CoverageSqliteData("gendata.db")) - overhead -print(f"Overhead: {overhead:.3f}s") -print(f"JSON: {jtime:.3f}s") -print(f"SQLite: {stime:.3f}s") -print(f"{stime / jtime:.3f}x slower") +print("Overhead: {overhead:.3f}s".format(overhead=overhead)) +print("JSON: {jtime:.3f}s".format(jtime=jtime)) +print("SQLite: {stime:.3f}s".format(stime=stime)) +print("{slower:.3f}x slower".format(slower=stime/jtime)) From 948c307c89c9f61256bd96b770fa5b14ee4fe383 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 19 Aug 2018 07:47:54 -0400 Subject: [PATCH 171/952] PyPy needs to close cursors from pragmas --- coverage/sqldata.py | 7 +++++-- tests/coveragetest.py | 1 - tests/test_data.py | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index f53561e76..f92e245b8 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,6 +3,7 @@ """Sqlite coverage data.""" +# TODO: get sys_info for data class, so we can see sqlite version etc # TODO: get rid of skip_unless_data_storage_is_json # TODO: get rid of "JSON message" and "SQL message" in the tests # TODO: check the schema @@ -403,9 +404,11 @@ def __init__(self, filename, debug): self.con = sqlite3.connect(self.filename) # This pragma makes writing faster. It disables rollbacks, but we never need them. - self.execute("pragma journal_mode=off") + # PyPy needs the .close() calls here, or sqlite gets twisted up: + # https://bitbucket.org/pypy/pypy/issues/2872/default-isolation-mode-is-different-on + self.execute("pragma journal_mode=off").close() # This pragma makes writing faster. - self.execute("pragma synchronous=off") + self.execute("pragma synchronous=off").close() def close(self): self.con.close() diff --git a/tests/coveragetest.py b/tests/coveragetest.py index cd6bb9fc1..b804a782c 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -95,7 +95,6 @@ def skip_unless_data_storage_is_json(self): if STORAGE != "json": self.skipTest("Not using JSON for data storage") - def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. diff --git a/tests/test_data.py b/tests/test_data.py index 48df81fdd..00d5d2948 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -105,6 +105,8 @@ def assert_arcs3_data(self, covdata): class CoverageDataTest(DataTestHelpers, CoverageTest): """Test cases for CoverageData.""" + # SQL data storage always has files on disk, even without .write(). + # We need to separate the tests so they don't clobber each other. run_in_temp_dir = STORAGE == "sql" def test_empty_data_is_false(self): From cc8421fa8fb78443c73d3b7d1e7e47ffd0c8d298 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 19 Aug 2018 20:30:01 -0400 Subject: [PATCH 172/952] Change XML gold tests to not use a common source directory --- tests/test_xml.py | 114 ++++++++++++++++++++++++++++++---------------- 1 file changed, 74 insertions(+), 40 deletions(-) diff --git a/tests/test_xml.py b/tests/test_xml.py index acb82a483..3e219a487 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -12,8 +12,7 @@ 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.coveragetest import CoverageTest, TESTS_DIR from tests.goldtest import change_dir, compare from tests.helpers import re_line, re_lines @@ -310,25 +309,34 @@ def clean(text, scrub=None): return text -class XmlGoldTest(CoverageGoldTest): - """Tests of XML reporting that use gold files.""" +def farm_dir(path): + return os.path.join(TESTS_DIR, "farm", path) - # TODO: this should move out of html. - root_dir = 'tests/farm/html' +class XmlGoldTest(CoverageTest): + """Tests of XML reporting that use gold files.""" def test_a_xml_1(self): - self.output_dir("out/xml_1") + self.make_file("a.py", """\ + # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - with change_dir("src"): - # pylint: disable=import-error - cov = coverage.Coverage() - cov.start() - import a # pragma: nested - cov.stop() # pragma: nested - cov.xml_report(a, outfile="../out/xml_1/coverage.xml") - source_path = coverage.files.relative_directory().rstrip(r"\/") + # A test file for HTML reporting by coverage.py. + + if 1 < 2: + # Needed a < to look at HTML entities. + a = 3 + else: + a = 4 + """) + + cov = coverage.Coverage() + cov.start() + import a # pragma: nested # pylint: disable=import-error + cov.stop() # pragma: nested + cov.xml_report(a, outfile="coverage.xml") + source_path = coverage.files.relative_directory().rstrip(r"\/") - compare("gold_x_xml", "out/xml_1", scrubs=[ + compare(".", farm_dir("html/gold_x_xml"), left_extra=True, scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), (r'\s*.*?\s*', '%s' % source_path), @@ -336,18 +344,33 @@ def test_a_xml_1(self): ]) def test_a_xml_2(self): - self.output_dir("out/xml_2") - - with change_dir("src"): - # pylint: disable=import-error - cov = coverage.Coverage(config_file="run_a_xml_2.ini") - cov.start() - import a # pragma: nested - cov.stop() # pragma: nested - cov.xml_report(a) - source_path = coverage.files.relative_directory().rstrip(r"\/") - - compare("gold_x_xml", "out/xml_2", scrubs=[ + self.make_file("a.py", """\ + # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + + # A test file for HTML reporting by coverage.py. + + if 1 < 2: + # Needed a < to look at HTML entities. + a = 3 + else: + a = 4 + """) + + self.make_file("run_a_xml_2.ini", """\ + # Put all the XML output in xml_2 + [xml] + output = xml_2/coverage.xml + """) + + cov = coverage.Coverage(config_file="run_a_xml_2.ini") + cov.start() + import a # pragma: nested # pylint: disable=import-error + cov.stop() # pragma: nested + cov.xml_report(a) + source_path = coverage.files.relative_directory().rstrip(r"\/") + + compare("xml_2", farm_dir("html/gold_x_xml"), scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), (r'\s*.*?\s*', '%s' % source_path), @@ -355,18 +378,29 @@ def test_a_xml_2(self): ]) def test_y_xml_branch(self): - self.output_dir("out/y_xml_branch") - - with change_dir("src"): - # pylint: disable=import-error - cov = coverage.Coverage(branch=True) - cov.start() - import y # pragma: nested - cov.stop() # pragma: nested - cov.xml_report(y, outfile="../out/y_xml_branch/coverage.xml") - source_path = coverage.files.relative_directory().rstrip(r"\/") - - compare("gold_y_xml_branch", "out/y_xml_branch", scrubs=[ + self.make_file("y.py", """\ + # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 + # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + + # A test file for XML reporting by coverage.py. + + def choice(x): + if x < 2: + return 3 + else: + return 4 + + assert choice(1) == 3 + """) + + cov = coverage.Coverage(branch=True) + cov.start() + import y # pragma: nested # pylint: disable=import-error + cov.stop() # pragma: nested + cov.xml_report(y, outfile="y_xml_branch/coverage.xml") + source_path = coverage.files.relative_directory().rstrip(r"\/") + + compare("y_xml_branch", farm_dir("html/gold_y_xml_branch"), scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), (r'\s*.*?\s*', '%s' % source_path), From e248080707eb0d350f2f4bb08b555f4f3670b601 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 20 Aug 2018 07:28:51 -0400 Subject: [PATCH 173/952] Stop TempDirTest from complaining that no files were made --- tests/test_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_data.py b/tests/test_data.py index 00d5d2948..1e6ce0278 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -108,6 +108,7 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): # SQL data storage always has files on disk, even without .write(). # We need to separate the tests so they don't clobber each other. run_in_temp_dir = STORAGE == "sql" + no_files_in_temp_dir = True def test_empty_data_is_false(self): covdata = CoverageData() From 074d8843c0d7909bbc6692f20cc056725d26041c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 21 Aug 2018 06:56:40 -0400 Subject: [PATCH 174/952] Enable keeping test-created temp dirs --- doc/contributing.rst | 11 ++++++++--- tests/coveragetest.py | 6 ++++++ tox.ini | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/doc/contributing.rst b/doc/contributing.rst index 71fa6937e..24a2636d8 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -122,13 +122,14 @@ To limit tox to just a few versions of Python, use the ``-e`` switch:: To run just a few tests, you can use `pytest test selectors`_:: $ tox tests/test_misc.py - $ tox tests/test_misc.py::SetupPyTest - $ tox tests/test_misc.py::SetupPyTest::test_metadata + $ tox tests/test_misc.py::HasherTest + $ tox tests/test_misc.py::HasherTest::test_string_hashing These command run the tests in one file, one class, and just one test, respectively. -You can also affect the test runs with environment variables: +You can also affect the test runs with environment variables. Define any of +these as 1 to use them: - COVERAGE_NO_PYTRACER disables the Python tracer if you only want to run the CTracer tests. @@ -142,6 +143,10 @@ You can also affect the test runs with environment variables: - COVERAGE_KEEP_OUTPUT will save the output files that were generated by the gold-file tests, ones that compare output files to saved gold files. +- COVERAGE_KEEP_TMP keeps the temporary directories in which tests are run. + This makes debugging tests easier. The temporary directories are at + ``$TMPDIR/coverage_test/*``, and are named for the test that made them. + Of course, run all the tests on every version of Python you have, before submitting a change. diff --git a/tests/coveragetest.py b/tests/coveragetest.py index b804a782c..6e3087183 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -81,6 +81,12 @@ class CoverageTest( # Let stderr go to stderr, pytest will capture it for us. show_stderr = True + # Temp dirs go to $TMPDIR/coverage_test/* + temp_dir_prefix = "coverage_test/" + + # Keep the temp directories if the env says to. + keep_temp_dir = bool(int(os.getenv("COVERAGE_KEEP_TMP", 0))) + def setUp(self): super(CoverageTest, self).setUp() diff --git a/tox.ini b/tox.ini index 0f81c200b..bbc00f3a7 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ deps = setuptools==40.0.0 mock==2.0.0 PyContracts==1.8.3 - unittest-mixins==1.4 + unittest-mixins==1.5 #-e/Users/ned/unittest_mixins # gevent 1.3 causes a failure: https://bitbucket.org/ned/coveragepy/issues/663/gevent-132-on-windows-fails py{27,34,35,36}: gevent==1.2.2 From aca3454584e7711a787f7f611837cca9c7d7c996 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 21 Aug 2018 07:05:22 -0400 Subject: [PATCH 175/952] Remove some unneeded lines from the code-in-code xml tests --- tests/farm/html/gold_x_xml/coverage.xml | 12 ++++++------ tests/farm/html/gold_y_xml_branch/coverage.xml | 16 ++++++++-------- tests/test_xml.py | 15 --------------- 3 files changed, 14 insertions(+), 29 deletions(-) diff --git a/tests/farm/html/gold_x_xml/coverage.xml b/tests/farm/html/gold_x_xml/coverage.xml index 162824a00..1030f9f8b 100644 --- a/tests/farm/html/gold_x_xml/coverage.xml +++ b/tests/farm/html/gold_x_xml/coverage.xml @@ -1,9 +1,9 @@ - - + + - /Users/ned/coverage/trunk/tests/farm/html/src + /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_xml_XmlGoldTest_test_a_xml_1_43316963 @@ -11,9 +11,9 @@ - - - + + + diff --git a/tests/farm/html/gold_y_xml_branch/coverage.xml b/tests/farm/html/gold_y_xml_branch/coverage.xml index bcf1137b2..71e08bb0a 100644 --- a/tests/farm/html/gold_y_xml_branch/coverage.xml +++ b/tests/farm/html/gold_y_xml_branch/coverage.xml @@ -1,9 +1,9 @@ - - + + - /Users/ned/coverage/trunk/tests/farm/html/src + /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_xml_XmlGoldTest_test_y_xml_branch_93378757 @@ -11,11 +11,11 @@ - - - - - + + + + + diff --git a/tests/test_xml.py b/tests/test_xml.py index 3e219a487..dd2de007d 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -317,11 +317,6 @@ class XmlGoldTest(CoverageTest): def test_a_xml_1(self): self.make_file("a.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - if 1 < 2: # Needed a < to look at HTML entities. a = 3 @@ -345,11 +340,6 @@ def test_a_xml_1(self): def test_a_xml_2(self): self.make_file("a.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - if 1 < 2: # Needed a < to look at HTML entities. a = 3 @@ -379,11 +369,6 @@ def test_a_xml_2(self): def test_y_xml_branch(self): self.make_file("y.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for XML reporting by coverage.py. - def choice(x): if x < 2: return 3 From 6df4275aa5e15e0f9033946837c1168a7dec00d5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 21 Aug 2018 07:24:00 -0400 Subject: [PATCH 176/952] Smoke should be quiet, and run failed tests first --- Makefile | 6 +++--- setup.cfg | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4512ad477..9ef522a08 100644 --- a/Makefile +++ b/Makefile @@ -48,13 +48,13 @@ pep8: test: tox -e py27,py35 $(ARGS) -TOX_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) +PYTEST_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) smoke: - COVERAGE_NO_PYTRACER=1 tox -e py27,py34 -- $(TOX_SMOKE_ARGS) + COVERAGE_NO_PYTRACER=1 tox -q -e py27,py34 -- $(PYTEST_SMOKE_ARGS) pysmoke: - COVERAGE_NO_CTRACER=1 tox -e py27,py34 -- $(TOX_SMOKE_ARGS) + COVERAGE_NO_CTRACER=1 tox -q -e py27,py34 -- $(PYTEST_SMOKE_ARGS) metacov: COVERAGE_COVERAGE=yes tox $(ARGS) diff --git a/setup.cfg b/setup.cfg index 69c64e7ed..0ab65b0e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = -q -n3 --strict --no-flaky-report -rfe +addopts = -q -n3 --strict --no-flaky-report -rfe --failed-first markers = expensive: too slow to run during "make smoke" From 7f5fb57e3e264f134c162dfb25c92e2b2d0e79e0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 22 Aug 2018 10:03:33 -0400 Subject: [PATCH 177/952] Stop using farm/src for HTML tests. --- coverage/html.py | 2 +- .../html/gold_other/blah_blah_other_py.html | 4 +- tests/farm/html/gold_other/index.html | 2 +- tests/goldtest.py | 6 +- tests/test_html.py | 484 ++++++++++++------ tests/test_xml.py | 13 +- tox.ini | 2 +- 7 files changed, 352 insertions(+), 161 deletions(-) diff --git a/coverage/html.py b/coverage/html.py index 2acc2656e..5c835684f 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -68,7 +68,7 @@ def read_data(fname): def write_html(fname, html): """Write `html` to `fname`, properly encoded.""" - html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) #+ "\n" with open(fname, "wb") as fout: fout.write(html.encode('ascii', 'xmlcharrefreplace')) diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index 4beb2a074..17b7ed3d0 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -3,7 +3,7 @@ - Coverage for /Users/ned/coverage/trunk/tests/farm/html/othersrc/other.py: 100% + Coverage for TEST_TMPDIR/othersrc/other.py: 100% @@ -16,7 +16,7 @@ diff --git a/tests/farm/html/gold_b_branch/b_py.html b/tests/farm/html/gold_b_branch/b_py.html index 6c3f75a84..07250e153 100644 --- a/tests/farm/html/gold_b_branch/b_py.html +++ b/tests/farm/html/gold_b_branch/b_py.html @@ -55,72 +55,62 @@

-

1

+

1

2

-

3

-

4

+

3

+

4

5

-

6

+

6

7

-

8

-

9

-

10

-

11

-

12

+

8

+

9

+

10

+

11

+

12

13

14

15

16

-

17

+

17

18

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

+

20

+

21

+

22

+

23

+

24

+

25

+

26

+

27

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

# A test file for HTML reporting by coverage.py. 

-

 

-

def one(x): 

-

# This will be a branch that misses the else. 

-

8 ↛ 11line 8 didn't jump to line 11, because the condition on line 8 was never false if x < 2: 

-

a = 3 

-

else: 

-

a = 4 

-

 

-

one(1) 

+

def one(x): 

+

# This will be a branch that misses the else. 

+

3 ↛ 6line 3 didn't jump to line 6, because the condition on line 3 was never false if x < 2: 

+

a = 3 

+

else: 

+

a = 4 

+

 

+

one(1) 

+

 

+

def two(x): 

+

# A missed else that branches to "exit" 

+

12 ↛ exitline 12 didn't return from function 'two', because the condition on line 12 was never false if x: 

+

a = 5 

 

-

def two(x): 

-

# A missed else that branches to "exit" 

-

17 ↛ exitline 17 didn't return from function 'two', because the condition on line 17 was never false if x: 

-

a = 5 

-

 

-

two(1) 

-

 

-

def three(): 

-

try: 

-

# This if has two branches, *neither* one taken. 

-

25 ↛ 26,   25 ↛ 282 missed branches: 1) line 25 didn't jump to line 26, because the condition on line 25 was never true, 2) line 25 didn't jump to line 28, because the condition on line 25 was never false if name_error_this_variable_doesnt_exist: 

-

a = 1 

-

else: 

-

a = 2 

-

except: 

-

pass 

-

 

-

three() 

+

two(1) 

+

 

+

def three(): 

+

try: 

+

# This if has two branches, *neither* one taken. 

+

20 ↛ 21,   20 ↛ 232 missed branches: 1) line 20 didn't jump to line 21, because the condition on line 20 was never true, 2) line 20 didn't jump to line 23, because the condition on line 20 was never false if name_error_this_variable_doesnt_exist: 

+

a = 1 

+

else: 

+

a = 2 

+

except: 

+

pass 

+

 

+

three() 

@@ -129,7 +119,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:40 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_b_branch/index.html b/tests/farm/html/gold_b_branch/index.html index 844f79e16..05f882bfd 100644 --- a/tests/farm/html/gold_b_branch/index.html +++ b/tests/farm/html/gold_b_branch/index.html @@ -84,7 +84,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:40 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_bom/bom_py.html b/tests/farm/html/gold_bom/bom_py.html index 472f655bc..92e609f82 100644 --- a/tests/farm/html/gold_bom/bom_py.html +++ b/tests/farm/html/gold_bom/bom_py.html @@ -55,35 +55,29 @@

1

-

2

+

2

3

-

4

-

5

-

6

+

4

+

5

+

6

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

+

8

+

9

+

10

+

11

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

+

# A Python source file in utf-8, with BOM. 

+

math = "3×4 = 12, ÷2 = 6±0" 

 

-

# A Python source file in utf-8, with BOM. 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

 

-

import sys 

-

 

-

if sys.version_info >= (3, 0): 

-

assert len(math) == 18 

-

assert len(math.encode('utf-8')) == 21 

-

else: 

-

assert len(math) == 21 

-

assert len(math.decode('utf-8')) == 18 

+

import sys 

+

 

+

if sys.version_info >= (3, 0): 

+

assert len(math) == 18 

+

assert len(math.encode('utf-8')) == 21 

+

else: 

+

assert len(math) == 21 

+

assert len(math.decode('utf-8')) == 18 

@@ -92,7 +86,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:44 + created at 2018-08-22 20:00

diff --git a/tests/farm/html/gold_bom/index.html b/tests/farm/html/gold_bom/index.html index 0341c0d07..13c55bf69 100644 --- a/tests/farm/html/gold_bom/index.html +++ b/tests/farm/html/gold_bom/index.html @@ -76,7 +76,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:44 + created at 2018-08-22 20:00

diff --git a/tests/farm/html/gold_isolatin1/index.html b/tests/farm/html/gold_isolatin1/index.html index ec1253641..160efcb6a 100644 --- a/tests/farm/html/gold_isolatin1/index.html +++ b/tests/farm/html/gold_isolatin1/index.html @@ -76,7 +76,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:40 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_isolatin1/isolatin1_py.html b/tests/farm/html/gold_isolatin1/isolatin1_py.html index 45d13f429..02e0ac0a9 100644 --- a/tests/farm/html/gold_isolatin1/isolatin1_py.html +++ b/tests/farm/html/gold_isolatin1/isolatin1_py.html @@ -57,21 +57,15 @@

1

2

3

-

4

-

5

-

6

-

7

-

8

+

4

+

5

# -*- coding: iso8859-1 -*- 

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

# A Python source file in another encoding. 

-

 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

assert len(math) == 18 

+

# A Python source file in another encoding. 

+

 

+

math = "3×4 = 12, ÷2 = 6±0" 

+

assert len(math) == 18 

@@ -80,7 +74,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:40 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/index.html b/tests/farm/html/gold_omit_1/index.html index 9ea591a47..95356e06b 100644 --- a/tests/farm/html/gold_omit_1/index.html +++ b/tests/farm/html/gold_omit_1/index.html @@ -97,7 +97,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/m1_py.html b/tests/farm/html/gold_omit_1/m1_py.html index 1557156cb..9ea4648ea 100644 --- a/tests/farm/html/gold_omit_1/m1_py.html +++ b/tests/farm/html/gold_omit_1/m1_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/m2_py.html b/tests/farm/html/gold_omit_1/m2_py.html index 8f3102d11..d6647ac00 100644 --- a/tests/farm/html/gold_omit_1/m2_py.html +++ b/tests/farm/html/gold_omit_1/m2_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m2a = 1 

-

m2b = 2 

+

m2a = 1 

+

m2b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/m3_py.html b/tests/farm/html/gold_omit_1/m3_py.html index 2d1e1d4c2..e5a9ebf75 100644 --- a/tests/farm/html/gold_omit_1/m3_py.html +++ b/tests/farm/html/gold_omit_1/m3_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_1/main_py.html b/tests/farm/html/gold_omit_1/main_py.html index bc93b1a0c..cc7630947 100644 --- a/tests/farm/html/gold_omit_1/main_py.html +++ b/tests/farm/html/gold_omit_1/main_py.html @@ -54,34 +54,28 @@

-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_2/index.html b/tests/farm/html/gold_omit_2/index.html index 8c2576f27..5e78cd3a6 100644 --- a/tests/farm/html/gold_omit_2/index.html +++ b/tests/farm/html/gold_omit_2/index.html @@ -90,7 +90,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_2/m2_py.html b/tests/farm/html/gold_omit_2/m2_py.html index 8f3102d11..d6647ac00 100644 --- a/tests/farm/html/gold_omit_2/m2_py.html +++ b/tests/farm/html/gold_omit_2/m2_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m2a = 1 

-

m2b = 2 

+

m2a = 1 

+

m2b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_2/m3_py.html b/tests/farm/html/gold_omit_2/m3_py.html index 2d1e1d4c2..e5a9ebf75 100644 --- a/tests/farm/html/gold_omit_2/m3_py.html +++ b/tests/farm/html/gold_omit_2/m3_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_2/main_py.html b/tests/farm/html/gold_omit_2/main_py.html index bc93b1a0c..cc7630947 100644 --- a/tests/farm/html/gold_omit_2/main_py.html +++ b/tests/farm/html/gold_omit_2/main_py.html @@ -54,34 +54,28 @@

-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_3/index.html b/tests/farm/html/gold_omit_3/index.html index f0b32cc4f..5c03fb8f5 100644 --- a/tests/farm/html/gold_omit_3/index.html +++ b/tests/farm/html/gold_omit_3/index.html @@ -83,7 +83,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_3/m3_py.html b/tests/farm/html/gold_omit_3/m3_py.html index 2d1e1d4c2..e5a9ebf75 100644 --- a/tests/farm/html/gold_omit_3/m3_py.html +++ b/tests/farm/html/gold_omit_3/m3_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_3/main_py.html b/tests/farm/html/gold_omit_3/main_py.html index bc93b1a0c..cc7630947 100644 --- a/tests/farm/html/gold_omit_3/main_py.html +++ b/tests/farm/html/gold_omit_3/main_py.html @@ -54,34 +54,28 @@

-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_4/index.html b/tests/farm/html/gold_omit_4/index.html index 7dadd229f..13c4ab692 100644 --- a/tests/farm/html/gold_omit_4/index.html +++ b/tests/farm/html/gold_omit_4/index.html @@ -90,7 +90,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_4/m1_py.html b/tests/farm/html/gold_omit_4/m1_py.html index 1557156cb..9ea4648ea 100644 --- a/tests/farm/html/gold_omit_4/m1_py.html +++ b/tests/farm/html/gold_omit_4/m1_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_4/m3_py.html b/tests/farm/html/gold_omit_4/m3_py.html index 2d1e1d4c2..e5a9ebf75 100644 --- a/tests/farm/html/gold_omit_4/m3_py.html +++ b/tests/farm/html/gold_omit_4/m3_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_4/main_py.html b/tests/farm/html/gold_omit_4/main_py.html index bc93b1a0c..cc7630947 100644 --- a/tests/farm/html/gold_omit_4/main_py.html +++ b/tests/farm/html/gold_omit_4/main_py.html @@ -54,34 +54,28 @@

-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_5/index.html b/tests/farm/html/gold_omit_5/index.html index b9912d249..366b1b8bd 100644 --- a/tests/farm/html/gold_omit_5/index.html +++ b/tests/farm/html/gold_omit_5/index.html @@ -83,7 +83,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_5/m1_py.html b/tests/farm/html/gold_omit_5/m1_py.html index 1557156cb..9ea4648ea 100644 --- a/tests/farm/html/gold_omit_5/m1_py.html +++ b/tests/farm/html/gold_omit_5/m1_py.html @@ -54,18 +54,12 @@

-

1

-

2

-

3

-

4

-

5

+

1

+

2

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

@@ -74,7 +68,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_omit_5/main_py.html b/tests/farm/html/gold_omit_5/main_py.html index bc93b1a0c..cc7630947 100644 --- a/tests/farm/html/gold_omit_5/main_py.html +++ b/tests/farm/html/gold_omit_5/main_py.html @@ -54,34 +54,28 @@

-

1

-

2

-

3

-

4

+

1

+

2

+

3

+

4

5

6

7

8

9

-

10

-

11

-

12

-

13

+

10

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

+

 

+

a = 5 

+

b = 6 

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

@@ -90,7 +84,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index fb45d0bfd..4d083808b 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -3,7 +3,7 @@ - Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_81055852/othersrc/other.py: 100% + Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_42705243/othersrc/other.py: 100% @@ -16,7 +16,7 @@ diff --git a/tests/farm/html/gold_other/index.html b/tests/farm/html/gold_other/index.html index a9c3d4cb1..18edab714 100644 --- a/tests/farm/html/gold_other/index.html +++ b/tests/farm/html/gold_other/index.html @@ -60,7 +60,7 @@

Coverage report: - /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_81055852/othersrc/other.py + /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_42705243/othersrc/other.py 1 0 0 @@ -83,7 +83,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:08 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_partial/index.html b/tests/farm/html/gold_partial/index.html index ca8919d78..b0addf9ce 100644 --- a/tests/farm/html/gold_partial/index.html +++ b/tests/farm/html/gold_partial/index.html @@ -84,7 +84,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_partial/partial_py.html b/tests/farm/html/gold_partial/partial_py.html index 7f4ed31ad..c792ff757 100644 --- a/tests/farm/html/gold_partial/partial_py.html +++ b/tests/farm/html/gold_partial/partial_py.html @@ -56,55 +56,47 @@

1

-

2

+

2

3

-

4

-

5

-

6

-

7

+

4

+

5

+

6

+

7

8

-

9

-

10

+

9

+

10

11

-

12

+

12

13

-

14

-

15

+

14

+

15

16

-

17

+

17

18

-

19

-

20

-

21

-

22

-

23

-

24

+

19

+

20

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

+

# partial branches and excluded lines 

+

a = 6 

 

-

# partial branches and excluded lines 

-

 

-

a = 6 

-

 

-

while True: 

-

break 

-

 

-

while 1: 

-

break 

-

 

-

while a: # pragma: no branch 

-

break 

-

 

-

if 0: 

-

never_happen() 

-

 

-

if 1: 

-

a = 21 

-

 

-

if a == 23: 

-

raise AssertionError("Can't") 

+

while True: 

+

break 

+

 

+

while 1: 

+

break 

+

 

+

while a: # pragma: no branch 

+

break 

+

 

+

if 0: 

+

never_happen() 

+

 

+

if 1: 

+

a = 21 

+

 

+

if a == 23: 

+

raise AssertionError("Can't") 

@@ -113,7 +105,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:43 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_styled/a_py.html b/tests/farm/html/gold_styled/a_py.html index 6e6f317ca..65f35d6e2 100644 --- a/tests/farm/html/gold_styled/a_py.html +++ b/tests/farm/html/gold_styled/a_py.html @@ -55,28 +55,18 @@

-

1

+

1

2

-

3

+

3

4

-

5

-

6

-

7

-

8

-

9

-

10

+

5

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

# A test file for HTML reporting by coverage.py. 

-

 

-

if 1 < 2: 

-

# Needed a < to look at HTML entities. 

-

a = 3 

-

else: 

-

a = 4 

+

if 1 < 2: 

+

# Needed a < to look at HTML entities. 

+

a = 3 

+

else: 

+

a = 4 

@@ -85,7 +75,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:42 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_styled/index.html b/tests/farm/html/gold_styled/index.html index c08815928..e5c36b959 100644 --- a/tests/farm/html/gold_styled/index.html +++ b/tests/farm/html/gold_styled/index.html @@ -77,7 +77,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:42 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_unicode/index.html b/tests/farm/html/gold_unicode/index.html index c1f2fb673..ff722dc9a 100644 --- a/tests/farm/html/gold_unicode/index.html +++ b/tests/farm/html/gold_unicode/index.html @@ -76,7 +76,7 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:42 + created at 2018-08-22 19:42

diff --git a/tests/farm/html/gold_unicode/unicode_py.html b/tests/farm/html/gold_unicode/unicode_py.html index 7708f22e8..8207d798c 100644 --- a/tests/farm/html/gold_unicode/unicode_py.html +++ b/tests/farm/html/gold_unicode/unicode_py.html @@ -57,21 +57,15 @@

1

2

3

-

4

-

5

-

6

-

7

-

8

+

4

+

5

# -*- coding: utf-8 -*- 

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

-

 

-

# A Python source file with exotic characters. 

-

 

-

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

-

surrogate = "db40,dd00: x󠄀" 

+

# A Python source file with exotic characters. 

+

 

+

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

+

surrogate = "db40,dd00: x󠄀" 

@@ -80,7 +74,7 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:42 + created at 2018-08-22 19:42

diff --git a/tests/test_html.py b/tests/test_html.py index 07ddba824..b4dd4606c 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -596,11 +596,6 @@ class HtmlGoldTests(CoverageTest): def test_a(self): self.make_file("a.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - if 1 < 2: # Needed a < to look at HTML entities. a = 3 @@ -632,11 +627,6 @@ def test_a(self): def test_b_branch(self): self.make_file("b.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - def one(x): # This will be a branch that misses the else. if x < 2: @@ -680,19 +670,20 @@ def three(): (' a = ' '3'), '70%', - ('8 ↛ 11' - 'line 8 didn\'t jump to line 11, ' - 'because the condition on line 8 was never false'), - ('17 ↛ exit' - 'line 17 didn\'t return from function \'two\', ' - 'because the condition on line 17 was never false'), - ('25 ↛ 26,   ' - '25 ↛ 28' + + ('3 ↛ 6' + 'line 3 didn\'t jump to line 6, ' + 'because the condition on line 3 was never false'), + ('12 ↛ exit' + 'line 12 didn\'t return from function \'two\', ' + 'because the condition on line 12 was never false'), + ('20 ↛ 21,   ' + '20 ↛ 23' '2 missed branches: ' - '1) line 25 didn\'t jump to line 26, ' - 'because the condition on line 25 was never true, ' - '2) line 25 didn\'t jump to line 28, ' - 'because the condition on line 25 was never false'), + '1) line 20 didn\'t jump to line 21, ' + 'because the condition on line 20 was never true, ' + '2) line 20 didn\'t jump to line 23, ' + 'because the condition on line 20 was never false'), ) contains( "out/index.html", @@ -703,10 +694,7 @@ def three(): def test_bom(self): self.make_file("bom.py", bytes=b"""\ -\xef\xbb\xbf# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - -# A Python source file in utf-8, with BOM. +\xef\xbb\xbf# A Python source file in utf-8, with BOM. math = "3\xc3\x974 = 12, \xc3\xb72 = 6\xc2\xb10" import sys @@ -725,7 +713,7 @@ def test_bom(self): with open("bom.py", "rb") as f: data = f.read() assert data[:3] == b"\xef\xbb\xbf" - assert data.count(b"\r\n") == 14 + assert data.count(b"\r\n") == 11 cov = coverage.Coverage() cov.start() @@ -742,9 +730,6 @@ def test_bom(self): def test_isolatin1(self): self.make_file("isolatin1.py", bytes=b"""\ # -*- coding: iso8859-1 -*- -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - # A Python source file in another encoding. math = "3\xd74 = 12, \xf72 = 6\xb10" @@ -765,9 +750,6 @@ def test_isolatin1(self): def make_main_etc(self): self.make_file("main.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - import m1 import m2 import m3 @@ -780,23 +762,14 @@ def make_main_etc(self): assert m3.m3a == 1 """) self.make_file("m1.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - m1a = 1 m1b = 2 """) self.make_file("m2.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - m2a = 1 m2b = 2 """) self.make_file("m3.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - m3a = 1 m3b = 2 """) @@ -872,11 +845,6 @@ def test_omit_5(self): def test_other(self): self.make_file("src/here.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - import other if 1 < 2: @@ -885,9 +853,6 @@ def test_other(self): h = 4 """) self.make_file("othersrc/other.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - # A file in another directory. We're checking that it ends up in the # HTML report. @@ -916,11 +881,7 @@ def test_other(self): def test_partial(self): self.make_file("partial.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - # partial branches and excluded lines - a = 6 while True: @@ -942,9 +903,6 @@ def test_partial(self): raise AssertionError("Can't") """) self.make_file("partial.ini", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - [run] branch = True @@ -962,13 +920,13 @@ def test_partial(self): compare_html("out", gold_path("html/gold_partial")) contains( "out/partial_py.html", - '

', - '

', - '

', + '

', + '

', + '

', # The "if 0" and "if 1" statements are optimized away. - '

', + '

', # The "raise AssertionError" is excluded by regex in the .ini. - '

', + '

', ) contains( "out/index.html", @@ -981,11 +939,6 @@ def test_partial(self): def test_styled(self): self.make_file("a.py", """\ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - - # A test file for HTML reporting by coverage.py. - if 1 < 2: # Needed a < to look at HTML entities. a = 3 @@ -1058,9 +1011,6 @@ def test_tabbed(self): def test_unicode(self): self.make_file("unicode.py", """\ # -*- coding: utf-8 -*- - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - # A Python source file with exotic characters. upside_down = "ʎd˙ǝbɐɹǝʌoɔ" From 9bd3b005d08ee78edbd684ed0706b23843b0460e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 22 Aug 2018 20:11:29 -0400 Subject: [PATCH 183/952] When we trimmed trailing whitespace, we lost the last newline. Put it back. --- coverage/html.py | 2 +- tests/farm/html/gold_a/a_py.html | 4 ++-- tests/farm/html/gold_a/index.html | 4 ++-- tests/farm/html/gold_b_branch/b_py.html | 4 ++-- tests/farm/html/gold_b_branch/index.html | 4 ++-- tests/farm/html/gold_bom/bom_py.html | 4 ++-- tests/farm/html/gold_bom/index.html | 4 ++-- tests/farm/html/gold_isolatin1/index.html | 4 ++-- tests/farm/html/gold_isolatin1/isolatin1_py.html | 4 ++-- tests/farm/html/gold_omit_1/index.html | 4 ++-- tests/farm/html/gold_omit_1/m1_py.html | 4 ++-- tests/farm/html/gold_omit_1/m2_py.html | 4 ++-- tests/farm/html/gold_omit_1/m3_py.html | 4 ++-- tests/farm/html/gold_omit_1/main_py.html | 4 ++-- tests/farm/html/gold_omit_2/index.html | 4 ++-- tests/farm/html/gold_omit_2/m2_py.html | 4 ++-- tests/farm/html/gold_omit_2/m3_py.html | 4 ++-- tests/farm/html/gold_omit_2/main_py.html | 4 ++-- tests/farm/html/gold_omit_3/index.html | 4 ++-- tests/farm/html/gold_omit_3/m3_py.html | 4 ++-- tests/farm/html/gold_omit_3/main_py.html | 4 ++-- tests/farm/html/gold_omit_4/index.html | 4 ++-- tests/farm/html/gold_omit_4/m1_py.html | 4 ++-- tests/farm/html/gold_omit_4/m3_py.html | 4 ++-- tests/farm/html/gold_omit_4/main_py.html | 4 ++-- tests/farm/html/gold_omit_5/index.html | 4 ++-- tests/farm/html/gold_omit_5/m1_py.html | 4 ++-- tests/farm/html/gold_omit_5/main_py.html | 4 ++-- tests/farm/html/gold_other/blah_blah_other_py.html | 8 ++++---- tests/farm/html/gold_other/here_py.html | 4 ++-- tests/farm/html/gold_other/index.html | 6 +++--- tests/farm/html/gold_partial/index.html | 4 ++-- tests/farm/html/gold_partial/partial_py.html | 4 ++-- tests/farm/html/gold_styled/a_py.html | 4 ++-- tests/farm/html/gold_styled/index.html | 4 ++-- tests/farm/html/gold_unicode/index.html | 4 ++-- tests/farm/html/gold_unicode/unicode_py.html | 4 ++-- 37 files changed, 76 insertions(+), 76 deletions(-) diff --git a/coverage/html.py b/coverage/html.py index 5c835684f..bb5192540 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -68,7 +68,7 @@ def read_data(fname): def write_html(fname, html): """Write `html` to `fname`, properly encoded.""" - html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) #+ "\n" + html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + "\n" with open(fname, "wb") as fout: fout.write(html.encode('ascii', 'xmlcharrefreplace')) diff --git a/tests/farm/html/gold_a/a_py.html b/tests/farm/html/gold_a/a_py.html index b90583bc9..119ad4a33 100644 --- a/tests/farm/html/gold_a/a_py.html +++ b/tests/farm/html/gold_a/a_py.html @@ -74,9 +74,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_a/index.html b/tests/farm/html/gold_a/index.html index adcda2d63..b839af1e3 100644 --- a/tests/farm/html/gold_a/index.html +++ b/tests/farm/html/gold_a/index.html @@ -76,9 +76,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_b_branch/b_py.html b/tests/farm/html/gold_b_branch/b_py.html index 07250e153..a21175eba 100644 --- a/tests/farm/html/gold_b_branch/b_py.html +++ b/tests/farm/html/gold_b_branch/b_py.html @@ -119,9 +119,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_b_branch/index.html b/tests/farm/html/gold_b_branch/index.html index 05f882bfd..a0346e86f 100644 --- a/tests/farm/html/gold_b_branch/index.html +++ b/tests/farm/html/gold_b_branch/index.html @@ -84,9 +84,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_bom/bom_py.html b/tests/farm/html/gold_bom/bom_py.html index 92e609f82..78d7f7b71 100644 --- a/tests/farm/html/gold_bom/bom_py.html +++ b/tests/farm/html/gold_bom/bom_py.html @@ -86,9 +86,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 20:00 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_bom/index.html b/tests/farm/html/gold_bom/index.html index 13c55bf69..4c4d98970 100644 --- a/tests/farm/html/gold_bom/index.html +++ b/tests/farm/html/gold_bom/index.html @@ -76,9 +76,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 20:00 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_isolatin1/index.html b/tests/farm/html/gold_isolatin1/index.html index 160efcb6a..c648ae7dc 100644 --- a/tests/farm/html/gold_isolatin1/index.html +++ b/tests/farm/html/gold_isolatin1/index.html @@ -76,9 +76,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_isolatin1/isolatin1_py.html b/tests/farm/html/gold_isolatin1/isolatin1_py.html index 02e0ac0a9..e8ad244b6 100644 --- a/tests/farm/html/gold_isolatin1/isolatin1_py.html +++ b/tests/farm/html/gold_isolatin1/isolatin1_py.html @@ -74,9 +74,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/index.html b/tests/farm/html/gold_omit_1/index.html index 95356e06b..289c6f107 100644 --- a/tests/farm/html/gold_omit_1/index.html +++ b/tests/farm/html/gold_omit_1/index.html @@ -97,9 +97,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/m1_py.html b/tests/farm/html/gold_omit_1/m1_py.html index 9ea4648ea..05b2bd494 100644 --- a/tests/farm/html/gold_omit_1/m1_py.html +++ b/tests/farm/html/gold_omit_1/m1_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/m2_py.html b/tests/farm/html/gold_omit_1/m2_py.html index d6647ac00..056e7af1f 100644 --- a/tests/farm/html/gold_omit_1/m2_py.html +++ b/tests/farm/html/gold_omit_1/m2_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/m3_py.html b/tests/farm/html/gold_omit_1/m3_py.html index e5a9ebf75..428527b2a 100644 --- a/tests/farm/html/gold_omit_1/m3_py.html +++ b/tests/farm/html/gold_omit_1/m3_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_1/main_py.html b/tests/farm/html/gold_omit_1/main_py.html index cc7630947..3fbc4af76 100644 --- a/tests/farm/html/gold_omit_1/main_py.html +++ b/tests/farm/html/gold_omit_1/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_2/index.html b/tests/farm/html/gold_omit_2/index.html index 5e78cd3a6..5813c0dcb 100644 --- a/tests/farm/html/gold_omit_2/index.html +++ b/tests/farm/html/gold_omit_2/index.html @@ -90,9 +90,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_2/m2_py.html b/tests/farm/html/gold_omit_2/m2_py.html index d6647ac00..056e7af1f 100644 --- a/tests/farm/html/gold_omit_2/m2_py.html +++ b/tests/farm/html/gold_omit_2/m2_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_2/m3_py.html b/tests/farm/html/gold_omit_2/m3_py.html index e5a9ebf75..428527b2a 100644 --- a/tests/farm/html/gold_omit_2/m3_py.html +++ b/tests/farm/html/gold_omit_2/m3_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_2/main_py.html b/tests/farm/html/gold_omit_2/main_py.html index cc7630947..3fbc4af76 100644 --- a/tests/farm/html/gold_omit_2/main_py.html +++ b/tests/farm/html/gold_omit_2/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_3/index.html b/tests/farm/html/gold_omit_3/index.html index 5c03fb8f5..4ebcf4a46 100644 --- a/tests/farm/html/gold_omit_3/index.html +++ b/tests/farm/html/gold_omit_3/index.html @@ -83,9 +83,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_3/m3_py.html b/tests/farm/html/gold_omit_3/m3_py.html index e5a9ebf75..428527b2a 100644 --- a/tests/farm/html/gold_omit_3/m3_py.html +++ b/tests/farm/html/gold_omit_3/m3_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_3/main_py.html b/tests/farm/html/gold_omit_3/main_py.html index cc7630947..3fbc4af76 100644 --- a/tests/farm/html/gold_omit_3/main_py.html +++ b/tests/farm/html/gold_omit_3/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_4/index.html b/tests/farm/html/gold_omit_4/index.html index 13c4ab692..e75887142 100644 --- a/tests/farm/html/gold_omit_4/index.html +++ b/tests/farm/html/gold_omit_4/index.html @@ -90,9 +90,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_4/m1_py.html b/tests/farm/html/gold_omit_4/m1_py.html index 9ea4648ea..05b2bd494 100644 --- a/tests/farm/html/gold_omit_4/m1_py.html +++ b/tests/farm/html/gold_omit_4/m1_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_4/m3_py.html b/tests/farm/html/gold_omit_4/m3_py.html index e5a9ebf75..428527b2a 100644 --- a/tests/farm/html/gold_omit_4/m3_py.html +++ b/tests/farm/html/gold_omit_4/m3_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_4/main_py.html b/tests/farm/html/gold_omit_4/main_py.html index cc7630947..3fbc4af76 100644 --- a/tests/farm/html/gold_omit_4/main_py.html +++ b/tests/farm/html/gold_omit_4/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_5/index.html b/tests/farm/html/gold_omit_5/index.html index 366b1b8bd..e2c1a1327 100644 --- a/tests/farm/html/gold_omit_5/index.html +++ b/tests/farm/html/gold_omit_5/index.html @@ -83,9 +83,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_5/m1_py.html b/tests/farm/html/gold_omit_5/m1_py.html index 9ea4648ea..05b2bd494 100644 --- a/tests/farm/html/gold_omit_5/m1_py.html +++ b/tests/farm/html/gold_omit_5/m1_py.html @@ -68,9 +68,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_omit_5/main_py.html b/tests/farm/html/gold_omit_5/main_py.html index cc7630947..3fbc4af76 100644 --- a/tests/farm/html/gold_omit_5/main_py.html +++ b/tests/farm/html/gold_omit_5/main_py.html @@ -84,9 +84,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html index 4d083808b..36e3653d7 100644 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ b/tests/farm/html/gold_other/blah_blah_other_py.html @@ -3,7 +3,7 @@ - Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_42705243/othersrc/other.py: 100% + Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_95946649/othersrc/other.py: 100% @@ -16,7 +16,7 @@ - \ No newline at end of file + diff --git a/tests/farm/html/gold_other/index.html b/tests/farm/html/gold_other/index.html index 18edab714..10d4ae9a4 100644 --- a/tests/farm/html/gold_other/index.html +++ b/tests/farm/html/gold_other/index.html @@ -60,7 +60,7 @@

Coverage report: - /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_42705243/othersrc/other.py + /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_95946649/othersrc/other.py 1 0 0 @@ -83,9 +83,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_partial/index.html b/tests/farm/html/gold_partial/index.html index b0addf9ce..1948615cd 100644 --- a/tests/farm/html/gold_partial/index.html +++ b/tests/farm/html/gold_partial/index.html @@ -84,9 +84,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_partial/partial_py.html b/tests/farm/html/gold_partial/partial_py.html index c792ff757..44238f683 100644 --- a/tests/farm/html/gold_partial/partial_py.html +++ b/tests/farm/html/gold_partial/partial_py.html @@ -105,9 +105,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_styled/a_py.html b/tests/farm/html/gold_styled/a_py.html index 65f35d6e2..dd569b1ba 100644 --- a/tests/farm/html/gold_styled/a_py.html +++ b/tests/farm/html/gold_styled/a_py.html @@ -75,9 +75,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_styled/index.html b/tests/farm/html/gold_styled/index.html index e5c36b959..1f86b7723 100644 --- a/tests/farm/html/gold_styled/index.html +++ b/tests/farm/html/gold_styled/index.html @@ -77,9 +77,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_unicode/index.html b/tests/farm/html/gold_unicode/index.html index ff722dc9a..35a98a9ee 100644 --- a/tests/farm/html/gold_unicode/index.html +++ b/tests/farm/html/gold_unicode/index.html @@ -76,9 +76,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + diff --git a/tests/farm/html/gold_unicode/unicode_py.html b/tests/farm/html/gold_unicode/unicode_py.html index 8207d798c..174a9a270 100644 --- a/tests/farm/html/gold_unicode/unicode_py.html +++ b/tests/farm/html/gold_unicode/unicode_py.html @@ -74,9 +74,9 @@

« index     coverage.py v5.0a2, - created at 2018-08-22 19:42 + created at 2018-08-22 20:12

- \ No newline at end of file + From 53d5da251b441c1896be707cf6c8bce2ce7d2cfe Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 06:38:55 -0400 Subject: [PATCH 184/952] Py2-specific gold files --- tests/farm/html/gold_bom/2/bom_py.html | 48 +++++++++++--------------- tests/farm/html/gold_bom/2/index.html | 4 +-- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/tests/farm/html/gold_bom/2/bom_py.html b/tests/farm/html/gold_bom/2/bom_py.html index 78c498fd1..14f25413b 100644 --- a/tests/farm/html/gold_bom/2/bom_py.html +++ b/tests/farm/html/gold_bom/2/bom_py.html @@ -55,35 +55,29 @@

1

-

2

+

2

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt 

+

# A Python source file in utf-8, with BOM. 

+

math = "3×4 = 12, ÷2 = 6±0" 

 

-

# A Python source file in utf-8, with BOM. 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

 

-

import sys 

-

 

-

if sys.version_info >= (3, 0): 

-

assert len(math) == 18 

-

assert len(math.encode('utf-8')) == 21 

-

else: 

-

assert len(math) == 21 

-

assert len(math.decode('utf-8')) == 18 

+

import sys 

+

 

+

if sys.version_info >= (3, 0): 

+

assert len(math) == 18 

+

assert len(math.encode('utf-8')) == 21 

+

else: 

+

assert len(math) == 21 

+

assert len(math.decode('utf-8')) == 18 

@@ -92,9 +86,9 @@

« index     coverage.py v5.0a2, - created at 2018-06-29 15:46 + created at 2018-08-23 06:35

- \ No newline at end of file + diff --git a/tests/farm/html/gold_bom/2/index.html b/tests/farm/html/gold_bom/2/index.html index 2d285ab15..bde4bb462 100644 --- a/tests/farm/html/gold_bom/2/index.html +++ b/tests/farm/html/gold_bom/2/index.html @@ -76,9 +76,9 @@

Coverage report:

coverage.py v5.0a2, - created at 2018-06-29 15:46 + created at 2018-08-23 06:35

- \ No newline at end of file + From b701a0c3088f917e3fc5feb081a5b5166126d4f1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 06:39:29 -0400 Subject: [PATCH 185/952] SQL storage means more tests need temp directories --- tests/test_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 854f9cc2b..88da3468b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -512,8 +512,6 @@ def test_bug_572(self): class OmitIncludeTestsMixin(UsingModulesMixin, CoverageTestMethodsMixin): """Test methods for coverage methods taking include and omit.""" - run_in_temp_dir = False - def filenames_in(self, summary, filenames): """Assert the `filenames` are in the keys of `summary`.""" for filename in filenames.split(): From 9f502b230c8c4b48334c0846cc9c50f9783c1a06 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 07:08:21 -0400 Subject: [PATCH 186/952] Remove now-unused CoverageGoldTest class --- doc/contributing.rst | 3 --- tests/goldtest.py | 30 +----------------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/doc/contributing.rst b/doc/contributing.rst index 24a2636d8..90d730975 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -140,9 +140,6 @@ these as 1 to use them: - COVEGE_AST_DUMP will dump the AST tree as it is being used during code parsing. -- COVERAGE_KEEP_OUTPUT will save the output files that were generated by the - gold-file tests, ones that compare output files to saved gold files. - - COVERAGE_KEEP_TMP keeps the temporary directories in which tests are run. This makes debugging tests easier. The temporary directories are at ``$TMPDIR/coverage_test/*``, and are named for the test that made them. diff --git a/tests/goldtest.py b/tests/goldtest.py index af471a14e..48842f0c1 100644 --- a/tests/goldtest.py +++ b/tests/goldtest.py @@ -4,12 +4,10 @@ """A test base class for tests based on gold file comparison.""" import os -import sys from unittest_mixins import change_dir # pylint: disable=unused-import -from tests.coveragetest import CoverageTest, TESTS_DIR -from tests.test_farm import clean +from tests.coveragetest import TESTS_DIR # Import helpers, eventually test_farm.py will go away. from tests.test_farm import ( # pylint: disable=unused-import compare, contains, doesnt_contain, contains_any, @@ -18,29 +16,3 @@ def gold_path(path): """Get a path to a gold file for comparison.""" return os.path.join(TESTS_DIR, "farm", path) - - -class CoverageGoldTest(CoverageTest): - """A test based on gold files.""" - - run_in_temp_dir = False - - def setUp(self): - super(CoverageGoldTest, self).setUp() - self.chdir(self.root_dir) - # Modules should be importable from the current directory. - sys.path.insert(0, '') - - def output_dir(self, the_dir): - """Declare where the output directory is. - - The output directory is deleted at the end of the test, unless the - COVERAGE_KEEP_OUTPUT environment variable is set. - - """ - # To make sure tests are isolated, we always clean the directory at the - # beginning of the test. - clean(the_dir) - - if not os.environ.get("COVERAGE_KEEP_OUTPUT"): # pragma: part covered - self.addCleanup(clean, the_dir) From a6097893ac54e6332a7c7b4b3667fc3064d9fb1b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 08:36:47 -0400 Subject: [PATCH 187/952] Make SQLite the default storage --- coverage/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/data.py b/coverage/data.py index aa23e7d4f..f03e90caa 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -634,7 +634,7 @@ def _has_arcs(self): return self._arcs is not None -STORAGE = os.environ.get("COVERAGE_STORAGE", "json") +STORAGE = os.environ.get("COVERAGE_STORAGE", "sql") if STORAGE == "json": CoverageData = CoverageJsonData elif STORAGE == "sql": From 7ef5a0fa170dd96aa257924554473cedfb3ceae7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 20:11:54 -0400 Subject: [PATCH 188/952] Add a test emulating pytest-cov --- tests/test_api.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 88da3468b..05bde67c2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -704,8 +704,6 @@ class TestRunnerPluginTest(CoverageTest): """ def pretend_to_be_nose_with_cover(self, erase): """This is what the nose --with-cover plugin does.""" - cov = coverage.Coverage() - self.make_file("no_biggie.py", """\ a = 1 b = 2 @@ -713,6 +711,7 @@ def pretend_to_be_nose_with_cover(self, erase): c = 4 """) + cov = coverage.Coverage() if erase: cov.combine() cov.erase() @@ -733,6 +732,34 @@ def test_nose_plugin(self): def test_nose_plugin_with_erase(self): self.pretend_to_be_nose_with_cover(erase=True) + def test_pytestcov_parallel(self): + self.make_file("prog.py", """\ + a = 1 + b = 2 + if b == 1: + c = 4 + """) + self.make_file(".coveragerc", """\ + [run] + parallel = True + source = . + """) + + cov = coverage.Coverage(source=None, branch=None, config_file='.coveragerc') + cov.erase() + self.start_import_stop(cov, "prog") + cov.combine() + cov.save() + report = StringIO() + cov.report(show_missing=None, ignore_errors=True, file=report, skip_covered=None) + self.assertEqual(report.getvalue(), textwrap.dedent("""\ + Name Stmts Miss Cover + ----------------------------- + prog.py 4 1 75% + """)) + self.assert_file_count(".coverage", 0) + self.assert_file_count(".coverage.*", 1) + class ReporterDeprecatedAttributeTest(CoverageTest): """Test that Reporter.file_reporters has been deprecated.""" From ad58ff0db4eeb40794e3cf87c2ee9365aedc7bd6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 23 Aug 2018 20:17:50 -0400 Subject: [PATCH 189/952] Fix the pytest-cov test --- coverage/control.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/coverage/control.py b/coverage/control.py index c83432afe..4dd62e10d 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -460,6 +460,7 @@ def erase(self): self._collector.reset() self._init_data(suffix=None) self._data.erase(parallel=self.config.parallel) + self._data = None def clear_exclude(self, which='exclude'): """Clear the exclude list.""" @@ -561,6 +562,8 @@ def get_data(self): """ self._init() + self._init_data(suffix=None) + self._post_init() if self._collector and self._collector.save_data(self._data): self._post_save_work() From c315b91653fcedc55f6ef1ff5e199c700b63398e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 24 Aug 2018 06:53:43 -0400 Subject: [PATCH 190/952] Disable travis lint until we can clean up the code --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 06782dc97..0eb9f6983 100644 --- a/tox.ini +++ b/tox.ini @@ -85,8 +85,9 @@ commands = python -m pylint --notes= {env:LINTABLE} [travis] +#2.7: py27, lint python = - 2.7: py27, lint + 2.7: py27 3.4: py34 3.5: py35 3.6: py36 From dd5b0cc88ebe4528abaa7cdf0b3fd516fb1f7e01 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 24 Aug 2018 07:13:24 -0400 Subject: [PATCH 191/952] CHANGES: data format changed to SQLite --- CHANGES.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 0e80b26c7..7ec728113 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,15 @@ Change history for Coverage.py Unreleased ---------- +- Coverage's data storage has changed. In version 4.x, .coverage files were + basically JSON. Now, they are SQLite databases. This means the data file + can be created earlier than it used to. A large amount of code was + refactored to support this change. + +- The old data format is still available (for now) by setting the environment + variable COVERAGE_STORAGE=json. Please tell me if you think you need to keep + the JSON format. + - Development moved from `Bitbucket`_ to `GitHub`_. - HTML files no longer have trailing and extra whitespace. From 0dbc2577b16e08df5ed6552e72b09c6849f8cb21 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 24 Aug 2018 07:22:22 -0400 Subject: [PATCH 192/952] Obsolete --- MANIFEST.in | 1 - TODO.txt | 285 ---------------------------------------------------- 2 files changed, 286 deletions(-) delete mode 100644 TODO.txt diff --git a/MANIFEST.in b/MANIFEST.in index f79021f7e..c9b345e10 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -10,7 +10,6 @@ include MANIFEST.in include Makefile include NOTICE.txt include README.rst -include TODO.txt include __main__.py include .travis.yml include appveyor.yml diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index f6036d2e5..000000000 --- a/TODO.txt +++ /dev/null @@ -1,285 +0,0 @@ -Coverage.py TODO - -Key: - * Heading - - Not done yet. - + Done. - x Not going to do. - -* 4.0 - -- What defaults should change? - x --source = . ? - x --branch = True ? - -- Remove 2.3, 2.4, 2.5 limitations - + set, sorted, reversed, rpartition - + generator expressions - + decorators - + collections.defaultdict - + .startswith((,)) - + "with" statements - - .format() ? - + try/except/finally - + with assertRaises - + addCleanup instead of tearDown - + exec statement can look like a function in py2 (since when?) - - runpy ? - + we can use "except ExcClass as e:" - -- Plugins - + Clean up - + implement plugin support in CTracer - + remove plugin support from PyTracer - x add services: - - filelocator - - warning - - dynamic_source_filename: return should be a canonical path - - update the omit test to use "quux*" instead of "*quux*" - + docs -+ Make reports use filenames, not module names -- documentation - - test helpers - + cov.config["run:branch"] api (well, coverage.get_option etc) - + "added in 4.0" - - tweaks to theme? - - Plugins! - Once per process - Once per file - - create a file tracer - - call its has_dynamic_source_file() - Once per call - Once per line -- build process - - don't publish to nedbat.com any more (but still need the sample html reports) - + don't need .px tooling - - write a new nedbat.com/code/coverage page. - - all doc links should point to rtfd -+ Remove code only run on <2.6 -+ Change data file to json -+ Create data api -+ gevent, etc. -+ Remove the old command-line syntax - + A pain, b/c of the structure of the tests. - + BTW: make an easier way to write those tests. - -- tests - - test the kit has the right contents - - test the kit installs the right stuff - - -* --source stuff: - + warn if a package is never found. - + warn if no data was collected - - tie --source into reporting - -* Soon - -+ Better omit handling that ignores files during measurement. - - Deal with ~ in specified paths correctly. -+ while TRUE claims to be partial. - + A way to mark lines as partial branches, with a regex? - + Default to "while True:", "while 1:" -+ HTML keyboard short cuts - - -* 3.2 - -+ Some kind of indication in the HTML where yellow lines aren't going. -- Profile the reporting code: it's REALLY slow. - - parser is doing some redundant work. -+ Analysis class should do rolling up of stats also (actually Numbers) -+ Update docs for --branch. -x self.coverage.data.has_arcs is ugly. -+ Branches that never jump to nocover lines shouldn't be marked as partial. - (see top of test_cogapp for examples) -+ Maybe turning off yellow lines should make those lines green? -+ A missing branch to leave the function shows an annotation of -1. Now "exit". -+ XML report needs to get branch information. -+ Add branch info to "coverage debug data" -+ Polish up the help, and double-check the docs. - - -* Speed - -+ C extension collector -- bitvector in trace extension. -- Ignore certain modules -+ Record linenos rather than (file,lineno) pairs in tracer. -x Tricky swapping of collector like figleaf, pycov, et al. (Don't need to do - this with C collector). -- Seems like there should be a faster way to manage all the line number sets in - CodeParser.raw_parse. -- If tracing, canonical_filename_cache overlaps with should_trace_cache. Skip - canonical_filename_cache. Maybe it isn't even worth it... -- Would pre-allocating line number integers make the C tracer faster? It would - use less memory. - - -* Accuracy - -- Record magic number of module to ensure code hasn't changed -- Record version of coverage data file, so we can update what's stored there. -- Record options in coverage data file, so multiple runs are certain to make - sense together. -- Do I still need the lines in annotate_file that deal specially with "else"? - - -* Power - -+ Branch coverage - Titus' idea: - 1: if a: - 2: b = 2 - 3: c = 3 - if the coverage data shows 1,2,3, it was if-then. if it's 1,3, then the - missing else was executed. -+ API for getting coverage data. -- Instruction tracing instead of line tracing. -- Path tracing (how does this even work?) -- Count execution of lines -- Track callers of functions (ala std module trace) -- Method/Class/Module coverage reporting. -- .coverage files that can be kept separate, rather than accumulated. -- test/coverage map: http://rbtcollins.wordpress.com/2009/09/16/back-from-hiatus/ - - Similar to figleaf's sections. - - -* Convenience - -- Command line modules should also be directories, meaning all the modules in that - directory. -- Why can't a morf also be a string, the name of a module? -- ignore by module as well as file? -+ Use a .coveragerc file to control coverage.py without the programmatic API. -- Add a --data switch to explicitly control the data file on the command line. -x Why can't you specify execute (-x) and report (-r) in the same invocation? - Maybe just because -x needs the rest of the command line? -+ Support 2.3 - 3.1! - http://pythonology.blogspot.com/2009/02/making-code-run-on-python-20-through-30.html - http://www.rfk.id.au/blog/entry/preparing-pyenchant-for-python-3 - http://pydev.blogspot.com/2008/11/making-code-work-in-python-2-and-3.html - + Explicitly set pickle protocol to 2. -- An inference mode that marks lines as executed if they "must have been" executed: - class definitions, etc, when coverage is started after the class is defined. -- Different categories of exclude pragma? So you can enable and disable them - from the command line, to reconsider exclusions. -+ Reporting on files never touched by coverage.py (package completeness) -- A setup.py command? http://jeetworks.org/node/50 -- Deltas: indicate the change in coverage percentage from the last run. -+ Show lines missing rather than lines run in the reporting, since that's what - you need to focus on. - - -* Beauty - -+ HTML report - - Colored bars indicating coverage per file. - - Package navigation. - - Rolled-up statistics. - - Some way to focus in on red and yellow - - Show only lines near highlights? - + Jump to next highlight? - + Keyboard navigation: j and k. - - Cookie for changes to pyfile.html state. - + Clickable column headers on the index page. - + Syntax coloring in HTML report. - + Dynamic effects in HTML report. - + Footer in reports pointing to coverage.py home page. - + Baseline grid for linenumber font. - + Separate out css and HTML. - + Does it work right with utf-8 source files? http://www.python.org/dev/peps/pep-0263/ - - Use vim modeline to determine tab width: http://vimdoc.sourceforge.net/htmldoc/options.html#modeline - - -* Community - -+ New docs, rather than pointing to Gareth's - + Min python version is 2.3. - - Three phases of work: - - Collection - - Analysis - - Reporting - - Distinction between: - - ignore (files not to collect) - - exclude (lines not to report as missed) - - omit (files not to report) - - Changes from coverage.py 2.x: - - Bare "except:" lines now count as executable code. - - Double function decorators: all decorator lines count as executable code. - x Document the .coverage file format. - + HTML reporting. - - Much more detail about what's in the report. - - References between pages are off: - - They have tags around them. - - They use #anchors that don't survive the px->html conversion. -+ Be sure --help text is complete (-i is missing). -+ Host the project somewhere with a real bug tracker: bitbucket.org -+ Point discussion to TIP -- PEP 8 compliance? - - -* Programmability - -+ Don't use sys.exit in CoverageScript. -+ Remove singleton - + Initialization of instance variables in the class. - - -* Installation - -x How will coverage.py package install over coverage.py module? -x pip can't install it: it reads the coverage.py html page, and finds the kit link, - but then can't handle the root-relative link. - - -* Modernization - -+ Decide on minimum supported version - + 2.3 - + Get rid of the basestring protection - + Use enumerate - + Use sets instead of dicts -+ Switch from getopt to optparse. -+ Get rid of the recursive nonsense. -+ Docstrings. -+ Remove huge document-style comments. -- Better names: - + self.cache -> self.cache_filename -> CoverageData.filename - + self.usecache -> CoverageData.use_file -- More classes: - - Module munging - + Coverage data files -+ Why are some imports at the top of the file, and some in functions? -+ Get rid of sys.exitfunc use. -+ True and False (with no backward adaptation: the constants are new in 2.2.1) -+ Get rid of compiler module - + In analyzing code - + In test_coverage.py -+ Style: - + lineno - + filename - - -* Correctness - -- What does -p (parallel mode) mean with -e (erase data)? - - -* Tests - -+ Switch to a real test runner, like nose. -+ Test both the C trace function and the Python trace function. -+ parser.py has no direct tests. -+ Tests about the .coverage file. -+ Tests about the --long-form of arguments. -+ Tests about overriding the .coverage filename. -- Tests about parallel mode. -+ Tests about assigning a multi-line string. -- Tests about tricky docstrings. -+ Coverage test coverage.py! -- Tests that tracing stops after calling stop() -- More intensive thread testing. -x Tests about the "import __main__" in cmdline.py -+ What happens if the -x script raises an exception? -- Test that the kit has all the proper contents. From 5d35fd2661eebe91b6a55b7d43117488c217c587 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 24 Aug 2018 20:11:18 -0400 Subject: [PATCH 193/952] Another pytest-cov test, for --append --- tests/test_api.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 05bde67c2..4d6ba9296 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -733,6 +733,12 @@ def test_nose_plugin_with_erase(self): self.pretend_to_be_nose_with_cover(erase=True) def test_pytestcov_parallel(self): + self.pretend_to_be_pytestcov(append=False) + + def test_pytestcov_parallel_append(self): + self.pretend_to_be_pytestcov(append=True) + + def pretend_to_be_pytestcov(self, append): self.make_file("prog.py", """\ a = 1 b = 2 @@ -746,7 +752,10 @@ def test_pytestcov_parallel(self): """) cov = coverage.Coverage(source=None, branch=None, config_file='.coveragerc') - cov.erase() + if append: + cov.load() + else: + cov.erase() self.start_import_stop(cov, "prog") cov.combine() cov.save() From 6cab43c5cacb79ef69bd603a2b148011aedf0f01 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 25 Aug 2018 08:38:45 -0400 Subject: [PATCH 194/952] A good way to solve the load-parallel pytestcov situation? --- coverage/control.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 4dd62e10d..6329441f4 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -330,9 +330,12 @@ def load(self): self._init() if self._collector: self._collector.reset() - self._init_data(suffix=None) + should_skip = self.config.parallel and not os.path.exists(self.config.data_file) + if not should_skip: + self._init_data(suffix=None) self._post_init() - self._data.read() + if not should_skip: + self._data.read() def _init_for_start(self): """Initialization for start()""" From 86508459edcf8f85a394413aec4cf909e709981f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 26 Aug 2018 08:50:54 -0400 Subject: [PATCH 195/952] Check the schema version, no more app_id --- coverage/sqldata.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index f92e245b8..e102f2949 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -6,8 +6,6 @@ # TODO: get sys_info for data class, so we can see sqlite version etc # TODO: get rid of skip_unless_data_storage_is_json # TODO: get rid of "JSON message" and "SQL message" in the tests -# TODO: check the schema -# TODO: get rid of the application_id? # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow # TODO: should writes be batched? @@ -17,7 +15,6 @@ import glob import os import sqlite3 -import struct from coverage.backward import iitems from coverage.data import filename_suffix @@ -26,13 +23,13 @@ from coverage.misc import CoverageException, file_be_gone +SCHEMA_VERSION = 1 + SCHEMA = """ -create table schema ( +create table coverage_schema ( version integer ); -insert into schema (version) values (1); - create table meta ( has_lines boolean, has_arcs boolean @@ -63,13 +60,6 @@ ); """ -APP_ID = 0xc07e8a6e # "coverage", kind of. - -def unsigned_to_signed(val): - return struct.unpack('>i', struct.pack('>I', val))[0] - -def signed_to_unsigned(val): - return struct.unpack('>I', struct.pack('>i', val))[0] class CoverageSqliteData(SimpleRepr): def __init__(self, basename=None, suffix=None, warn=None, debug=None): @@ -100,11 +90,11 @@ def _create_db(self): self._debug.write("Creating data file {!r}".format(self.filename)) self._db = Sqlite(self.filename, self._debug) with self._db: - self._db.execute("pragma application_id = {}".format(unsigned_to_signed(APP_ID))) for stmt in SCHEMA.split(';'): stmt = stmt.strip() if stmt: self._db.execute(stmt) + self._db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) self._db.execute( "insert into meta (has_lines, has_arcs) values (?, ?)", (self._has_lines, self._has_arcs) @@ -115,12 +105,20 @@ def _open_db(self): self._debug.write("Opening data file {!r}".format(self.filename)) self._db = Sqlite(self.filename, self._debug) with self._db: - for app_id, in self._db.execute("pragma application_id"): - app_id = signed_to_unsigned(int(app_id)) - if app_id != APP_ID: + try: + schema_version, = self._db.execute("select version from coverage_schema").fetchone() + except (TypeError, sqlite3.Error) as exc: + raise CoverageException( + "Data file {!r} doesn't seem to be a coverage data file: {}".format( + self.filename, exc + ) + ) + else: + if schema_version != SCHEMA_VERSION: raise CoverageException( - "Couldn't use {!r}: wrong application_id: " - "0x{:08x} != 0x{:08x}".format(self.filename, app_id, APP_ID) + "Data file {!r} is wrong schema: {} instead of {}".format( + self.filename, schema_version, SCHEMA_VERSION + ) ) for row in self._db.execute("select has_lines, has_arcs from meta"): self._has_lines, self._has_arcs = row From 505a31090814a674dc697ef93ab76f8f2b183ff9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 26 Aug 2018 09:13:52 -0400 Subject: [PATCH 196/952] Clean up lingering test results when we clean --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 4c29c3688..6c83b5627 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ clean: -rm -rf doc/_build doc/_spell doc/sample_html_beta -rm -rf .tox_kits -rm -rf .cache .pytest_cache + -rm -rf $$TMPDIR/coverage_test sterile: clean -rm -rf .tox* From 3ab1c50f23c47721bd2ccb025c0f0a8b3d2b16c9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 26 Aug 2018 09:44:22 -0400 Subject: [PATCH 197/952] Tests of the schema checking --- coverage/sqldata.py | 6 +++--- tests/coveragetest.py | 6 +++--- tests/test_data.py | 42 ++++++++++++++++++++++++++++++++---------- tests/test_process.py | 4 ++-- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index e102f2949..0037d76d8 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -4,7 +4,7 @@ """Sqlite coverage data.""" # TODO: get sys_info for data class, so we can see sqlite version etc -# TODO: get rid of skip_unless_data_storage_is_json +# TODO: get rid of skip_unless_data_storage_is # TODO: get rid of "JSON message" and "SQL message" in the tests # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow @@ -107,7 +107,7 @@ def _open_db(self): with self._db: try: schema_version, = self._db.execute("select version from coverage_schema").fetchone() - except (TypeError, sqlite3.Error) as exc: + except Exception as exc: raise CoverageException( "Data file {!r} doesn't seem to be a coverage data file: {}".format( self.filename, exc @@ -116,7 +116,7 @@ def _open_db(self): else: if schema_version != SCHEMA_VERSION: raise CoverageException( - "Data file {!r} is wrong schema: {} instead of {}".format( + "Couldn't use data file {!r}: wrong schema: {} instead of {}".format( self.filename, schema_version, SCHEMA_VERSION ) ) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 6e3087183..15c61ece6 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -97,9 +97,9 @@ def setUp(self): self.last_command_output = None self.last_module_name = None - def skip_unless_data_storage_is_json(self): - if STORAGE != "json": - self.skipTest("Not using JSON for data storage") + def skip_unless_data_storage_is(self, storage): + if STORAGE != storage: + self.skipTest("Not using {} for data storage".format(storage)) def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. diff --git a/tests/test_data.py b/tests/test_data.py index 1e6ce0278..15da32e57 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -8,6 +8,7 @@ import os import os.path import re +import sqlite3 import mock @@ -190,7 +191,7 @@ def test_no_lines_vs_unmeasured_file(self): self.assertIsNone(covdata.lines('no_such_file.py')) def test_run_info(self): - self.skip_unless_data_storage_is_json() + self.skip_unless_data_storage_is("json") covdata = CoverageData() self.assertEqual(covdata.run_infos(), []) covdata.add_run_info(hello="there") @@ -269,7 +270,7 @@ def test_update_arcs(self): self.assertEqual(covdata3.run_infos(), []) def test_update_run_info(self): - self.skip_unless_data_storage_is_json() + self.skip_unless_data_storage_is("json") covdata1 = CoverageData() covdata1.add_arcs(ARCS_3) covdata1.add_run_info(hello="there", count=17) @@ -475,15 +476,36 @@ def test_read_errors(self): covdata.read() self.assertFalse(covdata) - if STORAGE == "json": - self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") - with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): - covdata = CoverageData("misleading.dat") - covdata.read() - self.assertFalse(covdata) + def test_read_json_errors(self): + self.skip_unless_data_storage_is("json") + self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") + msg = r"Couldn't .* '.*[/\\]{0}': \S+" + with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): + covdata = CoverageData("misleading.dat") + covdata.read() + self.assertFalse(covdata) + + def test_read_sql_errors(self): + self.skip_unless_data_storage_is("sql") + with sqlite3.connect("wrong_schema.db") as con: + con.execute("create table coverage_schema (version integer)") + con.execute("insert into coverage_schema (version) values (99)") + msg = r"Couldn't .* '.*[/\\]{0}': wrong schema: 99 instead of \d+".format("wrong_schema.db") + with self.assertRaisesRegex(CoverageException, msg): + covdata = CoverageData("wrong_schema.db") + covdata.read() + self.assertFalse(covdata) + + with sqlite3.connect("no_schema.db") as con: + con.execute("create table foobar (baz text)") + msg = r"Couldn't .* '.*[/\\]{0}': \S+".format("no_schema.db") + with self.assertRaisesRegex(CoverageException, msg): + covdata = CoverageData("no_schema.db") + covdata.read() + self.assertFalse(covdata) def test_debug_main(self): - self.skip_unless_data_storage_is_json() + self.skip_unless_data_storage_is("json") covdata1 = CoverageData(".coverage") covdata1.add_lines(LINES_1) covdata1.write() @@ -660,7 +682,7 @@ def test_erasing_parallel(self): def read_json_data_file(self, fname): """Read a JSON data file for testing the JSON directly.""" - self.skip_unless_data_storage_is_json() + self.skip_unless_data_storage_is("json") with open(fname, 'r') as fdata: go_away = fdata.read(len(CoverageData._GO_AWAY)) self.assertEqual(go_away, CoverageData._GO_AWAY) diff --git a/tests/test_process.py b/tests/test_process.py index 49919b0ff..1ac18ffe5 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -516,7 +516,7 @@ def test_fork(self): if not hasattr(os, 'fork'): self.skipTest("Can't test os.fork since it doesn't exist.") # See https://nedbatchelder.com/blog/201808/sqlite_data_storage_for_coveragepy.html - self.skip_unless_data_storage_is_json() + self.skip_unless_data_storage_is("json") self.make_file("fork.py", """\ import os @@ -655,7 +655,7 @@ def test_note(self): if env.PYPY and env.PY3 and env.PYPYVERSION[:3] == (5, 10, 0): # pragma: obscure # https://bitbucket.org/pypy/pypy/issues/2729/pypy3-510-incorrectly-decodes-astral-plane self.skipTest("Avoid incorrect decoding astral plane JSON chars") - self.skip_unless_data_storage_is_json() + self.skip_unless_data_storage_is("json") self.make_file(".coveragerc", """\ [run] From 8295f50fa3dfebb12b94f9745385a3227ec7d53c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 25 Aug 2018 19:25:03 -0400 Subject: [PATCH 198/952] Better sys_info: pid, and a more understandable name. --- coverage/control.py | 5 +++-- tests/test_cmdline.py | 2 +- tests/test_debug.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 6329441f4..37ebada60 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -846,17 +846,18 @@ def plugin_info(plugins): ('configs_attempted', self.config.attempted_config_files), ('configs_read', self.config.config_files_read), ('config_file', self.config.config_file), - ('data_path', self._data.filename if self._data else "-none-"), + ('data_file', self._data.filename if self._data else "-none-"), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), ('implementation', platform.python_implementation()), ('executable', sys.executable), + ('pid', os.getpid()), ('cwd', os.getcwd()), ('path', sys.path), ('environment', sorted( ("%s = %s" % (k, v)) for k, v in iitems(os.environ) - if k.startswith(("COV", "PY")) + if any(slug in k for slug in ("COV", "PY")) )), ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index b12f92ea6..a4d018806 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -232,7 +232,7 @@ def test_debug_sys(self): self.command_line("debug sys") out = self.stdout() self.assertIn("version:", out) - self.assertIn("data_path:", out) + self.assertIn("data_file:", out) def test_debug_config(self): self.command_line("debug config") diff --git a/tests/test_debug.py b/tests/test_debug.py index c47dd3439..26ddc0df4 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -168,8 +168,8 @@ def test_debug_sys(self): labels = """ version coverage cover_paths pylib_paths tracer configs_attempted config_file - configs_read data_path python platform implementation executable - cwd path environment command_line cover_match pylib_match + configs_read data_file python platform implementation executable + pid cwd path environment command_line cover_match pylib_match """.split() for label in labels: label_pat = r"^\s*%s: " % label From ecb9039daac71f6cf7f83ea3046659ac924188d4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 26 Aug 2018 10:37:01 -0400 Subject: [PATCH 199/952] Formalize some more debugging tools I've been keeping to the side --- coverage/control.py | 4 +++ coverage/debug.py | 74 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/coverage/control.py b/coverage/control.py index 37ebada60..2ee2c9337 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -39,6 +39,10 @@ os = isolate_module(os) +if 1: + from coverage.debug import decorate_methods, break_in_pudb, show_calls + +#@decorate_methods(show_calls(show_args=True), butnot=['get_data']) class Coverage(object): """Programmatic access to coverage.py. diff --git a/coverage/debug.py b/coverage/debug.py index f491a0f79..67a45ce12 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -4,7 +4,9 @@ """Control of and utilities for debugging.""" import contextlib +import functools import inspect +import itertools import os import re import sys @@ -173,7 +175,10 @@ def add_pid_and_tid(text): class SimpleRepr(object): """A mixin implementing a simple __repr__.""" def __repr__(self): - show_attrs = ((k, v) for k, v in self.__dict__.items() if getattr(v, "show_repr_attr", True)) + show_attrs = ( + (k, v) for k, v in self.__dict__.items() + if getattr(v, "show_repr_attr", True) + ) return "<{klass} @0x{id:x} {attrs}>".format( klass=self.__class__.__name__, id=id(self), @@ -248,6 +253,8 @@ def the_one(cls, fileobj=None, show_process=True, filters=()): # on a class attribute. Yes, this is aggressively gross. the_one = sys.modules.get(cls.SYS_MOD_NAME) if the_one is None: + with open("/tmp/where.txt", "a") as f: + f.write("Starting with {}\n".format(fileobj)) if fileobj is None: fileobj = open("/tmp/debug_log.txt", "a") sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters) @@ -271,6 +278,71 @@ def log(msg, stack=False): # pragma: debugging dump_stack_frames(out=out, skip=1) +def decorate_methods(decorator, butnot=()): # pragma: debugging + """A class decorator to apply a decorator to public methods.""" + def _decorator(cls): + for name, meth in inspect.getmembers(cls, inspect.isroutine): + public = name == '__init__' or not name.startswith("_") + decorate_it = public and name not in butnot + if decorate_it: + setattr(cls, name, decorator(meth)) + return cls + return _decorator + + +def break_in_pudb(func): # pragma: debugging + """A function decorator to stop in the debugger for each call.""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + import pudb # pylint: disable=import-error + sys.stdout = sys.__stdout__ + pudb.set_trace() + return func(*args, **kwargs) + return wrapper + + +OBJ_IDS = itertools.count() +CALLS = itertools.count() +OBJ_ID_ATTR = "$coverage.object_id" + +def show_calls(show_args=True, show_stack=False): # pragma: debugging + """A method decorator to debug-log each call to the function.""" + def _decorator(func): + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + oid = getattr(self, OBJ_ID_ATTR, None) + if oid is None: + oid = "{:08d} {:04d}".format(os.getpid(), next(OBJ_IDS)) + setattr(self, OBJ_ID_ATTR, oid) + extra = "" + if show_args: + eargs = ", ".join(map(repr, args)) + ekwargs = ", ".join("{}={!r}".format(*item) for item in kwargs.items()) + extra += "(" + extra += eargs + if eargs and ekwargs: + extra += ", " + extra += ekwargs + extra += ")" + if show_stack: + extra += " @ " + extra += "; ".join(_clean_stack_line(l) for l in short_stack().splitlines()) + msg = "{} {:04d} {}{}\n".format(oid, next(CALLS), func.__name__, extra) + DebugOutputFile.the_one().write(msg) + return func(self, *args, **kwargs) + return wrapper + return _decorator + + +def _clean_stack_line(s): # pragma: debugging + """Simplify some paths in a stack trace, for compactness.""" + s = s.strip() + s = s.replace(os.path.dirname(__file__) + '/', '') + s = s.replace(os.path.dirname(os.__file__) + '/', '') + s = s.replace(sys.prefix + '/', '') + return s + + def filter_aspectlib_frames(text): # pragma: debugging """Aspectlib prints stack traces, but includes its own frames. Scrub those out.""" # <<< aspectlib/__init__.py:257:function_wrapper < igor.py:143:run_tests < ... From 3c54d8407173fc9c06577fc85950422387f412df Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 26 Aug 2018 11:23:30 -0400 Subject: [PATCH 200/952] Create the debug file more centrally This helps ensure that all debugging output goes into a single file. --- coverage/control.py | 6 ------ coverage/debug.py | 14 ++++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 2ee2c9337..2130de39e 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -197,12 +197,6 @@ def _init(self): # Create and configure the debugging controller. COVERAGE_DEBUG_FILE # is an environment variable, the name of a file to append debug logs # to. - if self._debug_file is None: - debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE") - if debug_file_name: - self._debug_file = open(debug_file_name, "a") - else: - self._debug_file = sys.stderr self._debug = DebugControl(self.config.debug, self._debug_file) # _exclude_re is a dict that maps exclusion list names to compiled regexes. diff --git a/coverage/debug.py b/coverage/debug.py index 67a45ce12..442fb1de1 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -35,17 +35,17 @@ class DebugControl(object): def __init__(self, options, output): """Configure the options and output file for debugging.""" self.options = list(options) + FORCED_DEBUG - self.raw_output = output self.suppress_callers = False filters = [] if self.should('pid'): filters.append(add_pid_and_tid) - self.output = DebugOutputFile( - self.raw_output, + self.output = DebugOutputFile.the_one( + output, show_process=self.should('process'), filters=filters, ) + self.raw_output = self.output.outfile def __repr__(self): return "" % (self.options, self.raw_output) @@ -253,10 +253,12 @@ def the_one(cls, fileobj=None, show_process=True, filters=()): # on a class attribute. Yes, this is aggressively gross. the_one = sys.modules.get(cls.SYS_MOD_NAME) if the_one is None: - with open("/tmp/where.txt", "a") as f: - f.write("Starting with {}\n".format(fileobj)) if fileobj is None: - fileobj = open("/tmp/debug_log.txt", "a") + debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE") + if debug_file_name: + fileobj = open(debug_file_name, "a") + else: + fileobj = sys.stderr sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters) return the_one From c977402a9c209a9c3ab07e58bcff409c673edd98 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 26 Aug 2018 11:26:29 -0400 Subject: [PATCH 201/952] An environment-controllable way to turn on call debugging --- coverage/control.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 2130de39e..a246bb7a2 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -39,10 +39,6 @@ os = isolate_module(os) -if 1: - from coverage.debug import decorate_methods, break_in_pudb, show_calls - -#@decorate_methods(show_calls(show_args=True), butnot=['get_data']) class Coverage(object): """Programmatic access to coverage.py. @@ -866,6 +862,13 @@ def plugin_info(plugins): return info +# Mega debugging... +if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): + from coverage.debug import decorate_methods, show_calls + + Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage) + + def process_startup(): """Call this at Python start-up to perhaps measure coverage. From 032923e64a98277b13670382cd4324cf1a22438e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 27 Aug 2018 10:26:34 -0400 Subject: [PATCH 202/952] SQLite storage detects forks --- coverage/sqldata.py | 21 ++++++++++++++++----- tests/test_process.py | 2 -- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 0037d76d8..87c157391 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -9,7 +9,6 @@ # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow # TODO: should writes be batched? -# TODO: settle the os.fork question # TODO: run_info import glob @@ -63,21 +62,28 @@ class CoverageSqliteData(SimpleRepr): def __init__(self, basename=None, suffix=None, warn=None, debug=None): - self.filename = os.path.abspath(basename or ".coverage") - suffix = filename_suffix(suffix) - if suffix: - self.filename += "." + suffix + self._basename = os.path.abspath(basename or ".coverage") + self._suffix = suffix self._warn = warn self._debug = debug + self._choose_filename() self._file_map = {} self._db = None + self._pid = os.getpid() + # Are we in sync with the data file? self._have_used = False self._has_lines = False self._has_arcs = False + def _choose_filename(self): + self.filename = self._basename + suffix = filename_suffix(self._suffix) + if suffix: + self.filename += "." + suffix + def _reset(self): self._file_map = {} if self._db is not None: @@ -333,6 +339,11 @@ def write(self): pass def _start_using(self): + if self._pid != os.getpid(): + # Looks like we forked! Have to start a new data file. + self._reset() + self._choose_filename() + self._pid = os.getpid() if not self._have_used: self.erase() self._have_used = True diff --git a/tests/test_process.py b/tests/test_process.py index 1ac18ffe5..6174b9f7c 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -515,8 +515,6 @@ def f1(): def test_fork(self): if not hasattr(os, 'fork'): self.skipTest("Can't test os.fork since it doesn't exist.") - # See https://nedbatchelder.com/blog/201808/sqlite_data_storage_for_coveragepy.html - self.skip_unless_data_storage_is("json") self.make_file("fork.py", """\ import os From 94213347541d8138db07811c496d6db138b23700 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 27 Aug 2018 19:20:06 -0400 Subject: [PATCH 203/952] question marks aren't as good as -none- --- coverage/control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/control.py b/coverage/control.py index a246bb7a2..1e23dcd56 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -853,7 +853,7 @@ def plugin_info(plugins): for k, v in iitems(os.environ) if any(slug in k for slug in ("COV", "PY")) )), - ('command_line', " ".join(getattr(sys, 'argv', ['???']))), + ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))), ] if self._inorout: From e82f5d884e1f08275f0343f280ed3500b4b6d47a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 30 Aug 2018 16:45:45 -0400 Subject: [PATCH 204/952] Fix the aggressive singleton that broke the test suite in 3c54d840 --- coverage/debug.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index 442fb1de1..c2d7b5174 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -40,7 +40,7 @@ def __init__(self, options, output): filters = [] if self.should('pid'): filters.append(add_pid_and_tid) - self.output = DebugOutputFile.the_one( + self.output = DebugOutputFile.get_one( output, show_process=self.should('process'), filters=filters, @@ -239,14 +239,23 @@ def __init__(self, outfile, show_process, filters): SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' @classmethod - def the_one(cls, fileobj=None, show_process=True, filters=()): - """Get the process-wide singleton DebugOutputFile. + def get_one(cls, fileobj=None, show_process=True, filters=()): + """Get a DebugOutputFile. - If it doesn't exist yet, then create it as a wrapper around the file - object `fileobj`. `show_process` controls whether the debug file adds - process-level information. + If `fileobj` is provided, then a new DebugOutputFile is made with it. + + If `fileobj` isn't provided, then a file is chosen + (COVERAGE_DEBUG_FILE, or stderr), and a process-wide singleton + DebugOutputFile is made. + + `show_process` controls whether the debug file adds process-level + information, and filters is a list of other message filters to apply. """ + if fileobj is not None: + # Make DebugOutputFile around the fileobj passed. + return cls(fileobj, show_process, filters) + # Because of the way igor.py deletes and re-imports modules, # this class can be defined more than once. But we really want # a process-wide singleton. So stash it in sys.modules instead of @@ -274,7 +283,7 @@ def flush(self): def log(msg, stack=False): # pragma: debugging """Write a log message as forcefully as possible.""" - out = DebugOutputFile.the_one() + out = DebugOutputFile.get_one() out.write(msg+"\n") if stack: dump_stack_frames(out=out, skip=1) @@ -330,7 +339,7 @@ def wrapper(self, *args, **kwargs): extra += " @ " extra += "; ".join(_clean_stack_line(l) for l in short_stack().splitlines()) msg = "{} {:04d} {}{}\n".format(oid, next(CALLS), func.__name__, extra) - DebugOutputFile.the_one().write(msg) + DebugOutputFile.get_one().write(msg) return func(self, *args, **kwargs) return wrapper return _decorator @@ -358,7 +367,6 @@ def enable_aspectlib_maybe(): # pragma: debugging Define COVERAGE_ASPECTLIB to enable and configure aspectlib to trace execution:: - $ export COVERAGE_LOG=covaspect.txt $ export COVERAGE_ASPECTLIB=coverage.Coverage:coverage.data.CoverageData $ coverage run blah.py ... @@ -373,9 +381,8 @@ def enable_aspectlib_maybe(): # pragma: debugging import aspectlib # pylint: disable=import-error import aspectlib.debug # pylint: disable=import-error - filename = os.environ.get("COVERAGE_LOG", "/tmp/covlog.txt") filters = [add_pid_and_tid, filter_aspectlib_frames] - aspects_file = DebugOutputFile.the_one(open(filename, "a"), show_process=True, filters=filters) + aspects_file = DebugOutputFile.get_one(None, show_process=True, filters=filters) aspect_log = aspectlib.debug.log( print_to=aspects_file, attributes=['id'], stacktrace=30, use_logging=False ) From b764393571dbb84996f26b7f52cb02599b44b5a9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 30 Aug 2018 08:55:49 -0400 Subject: [PATCH 205/952] Show the contents of the config file --- coverage/config.py | 3 +++ coverage/control.py | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/coverage/config.py b/coverage/config.py index a0d7d06bc..061fa304e 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -181,6 +181,7 @@ def __init__(self): self.config_files_read = [] # The file that gave us our configuration. self.config_file = None + self._config_contents = None # Defaults for [run] and [report] self._include = None @@ -315,6 +316,8 @@ def from_file(self, filename, our_file): if used: self.config_file = filename + with open(filename) as f: + self._config_contents = f.read() return used diff --git a/coverage/control.py b/coverage/control.py index 1e23dcd56..580fc9dae 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -223,6 +223,7 @@ def _write_startup_debug(self): with self._debug.without_callers(): if self._debug.should('config'): config_info = sorted(self.config.__dict__.items()) + config_info = [(k, v) for k, v in config_info if not k.startswith('_')] write_formatted_info(self._debug, "config", config_info) wrote_any = True @@ -840,6 +841,11 @@ def plugin_info(plugins): ('configs_attempted', self.config.attempted_config_files), ('configs_read', self.config.config_files_read), ('config_file', self.config.config_file), + ('config_contents', + repr(self.config._config_contents) + if self.config._config_contents + else '-none-' + ), ('data_file', self._data.filename if self._data else "-none-"), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), From a9bce6d2be9d51d0a449411cf4325a965140ce0c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 30 Aug 2018 14:33:04 -0400 Subject: [PATCH 206/952] Put back an always-on indication of the tracer available --- coverage/control.py | 3 ++- tests/test_debug.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 580fc9dae..c4022f1fb 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -13,7 +13,7 @@ from coverage import env from coverage.annotate import AnnotateReporter from coverage.backward import string_class, iitems -from coverage.collector import Collector +from coverage.collector import Collector, CTracer from coverage.config import read_coverage_config from coverage.data import CoverageData, combine_parallel_data from coverage.debug import DebugControl, write_formatted_info @@ -836,6 +836,7 @@ def plugin_info(plugins): ('version', covmod.__version__), ('coverage', covmod.__file__), ('tracer', self._collector.tracer_name() if self._collector else "-none-"), + ('CTracer', 'available' if CTracer else "unavailable"), ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), ('plugins.configurers', plugin_info(self._plugins.configurers)), ('configs_attempted', self.config.attempted_config_files), diff --git a/tests/test_debug.py b/tests/test_debug.py index 26ddc0df4..284d9567d 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -10,9 +10,10 @@ import coverage from coverage.backward import StringIO from coverage.debug import filter_text, info_formatter, info_header, short_id, short_stack +from coverage.env import C_TRACER from tests.coveragetest import CoverageTest -from tests.helpers import re_lines +from tests.helpers import re_line, re_lines class InfoFormatterTest(CoverageTest): @@ -160,7 +161,8 @@ def test_debug_config(self): label_pat = r"^\s*%s: " % label self.assertEqual( len(re_lines(out_lines, label_pat).splitlines()), - 1 + 1, + msg="Incorrect lines for %r" % label, ) def test_debug_sys(self): @@ -179,6 +181,15 @@ def test_debug_sys(self): msg="Incorrect lines for %r" % label, ) + def test_debug_sys_ctracer(self): + out_lines = self.f1_debug_output(["sys"]) + tracer_line = re_line(out_lines, r"CTracer:").strip() + if C_TRACER: + expected = "CTracer: available" + else: + expected = "CTracer: unavailable" + self.assertEqual(tracer_line, expected) + def f_one(*args, **kwargs): """First of the chain of functions for testing `short_stack`.""" From 17724b9930224f772f3d2a2ebdbf154f15014be1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 30 Aug 2018 17:31:00 -0400 Subject: [PATCH 207/952] Fix a line length --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ce6455435..14b51cfcc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -615,9 +615,9 @@ Version 4.1b2 --- 2016-01-23 - Class docstrings were considered executable. Now they no longer are. - ``yield from`` and ``await`` were considered returns from functions, since - they could transfer control to the caller. This produced unhelpful "missing - branch" reports in a number of circumstances. Now they no longer are - considered returns. + they could transfer control to the caller. This produced unhelpful + "missing branch" reports in a number of circumstances. Now they no longer + are considered returns. - In unusual situations, a missing branch to a negative number was reported. This has been fixed, closing `issue 466`_. From e60e9e0e6f3c2c74a4bae8d3ff10de070e350a98 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 8 Aug 2018 11:45:14 -0300 Subject: [PATCH 208/952] Persist html sort order in localStorage instead of cookie --- coverage/htmlfiles/coverage_html.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index 7fc2963c6..2b32c3910 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -169,22 +169,13 @@ coverage.wire_up_filter = function () { // Loaded on index.html coverage.index_ready = function ($) { - // Look for a cookie containing previous sort settings: + // Look for a localStorage item containing previous sort settings: var sort_list = []; - var cookie_name = "COVERAGE_INDEX_SORT"; - var i; + var storage_name = "COVERAGE_INDEX_SORT"; + var stored_list = localStorage.getItem(storage_name); - // This almost makes it worth installing the jQuery cookie plugin: - if (document.cookie.indexOf(cookie_name) > -1) { - var cookies = document.cookie.split(";"); - for (i = 0; i < cookies.length; i++) { - var parts = cookies[i].split("="); - - if ($.trim(parts[0]) === cookie_name && parts[1]) { - sort_list = eval("[[" + parts[1] + "]]"); - break; - } - } + if (stored_list) { + sort_list = JSON.parse('[[' + stored_list + ']]'); } // Create a new widget which exists only to save and restore @@ -231,7 +222,7 @@ coverage.index_ready = function ($) { // Watch for page unload events so we can save the final sort settings: $(window).unload(function () { - document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; + localStorage.setItem(storage_name, sort_list.toString()) }); }; From 04fd64ed12954b12fb0d3be219a562d3354bb4ed Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 30 Aug 2018 18:48:11 -0400 Subject: [PATCH 209/952] Federico did the local storage change --- CHANGES.rst | 3 +++ CONTRIBUTORS.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 14b51cfcc..1a10a7d58 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,6 +30,9 @@ Unreleased - HTML files no longer have trailing and extra whitespace. +- The sort order in the HTML report is stored in local storage rather than + cookies. Thanks, Federico Bond. + - pickle2json, for converting v3 data files to v4 data files, has been removed. .. _Bitbucket: https://bitbucket.org/ned/coveragepy diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 549a83dcf..011e234c7 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -47,6 +47,7 @@ Dmitry Trofimov Eduardo Schettino Emil Madsen Edward Loper +Federico Bond Geoff Bache George Paci George Song From e734f26c0cf619e57a51fe9442df09e754945139 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 30 Aug 2018 19:23:25 -0400 Subject: [PATCH 210/952] Have to escape data going into a regex sub --- tests/test_xml.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_xml.py b/tests/test_xml.py index 65c1a48ba..f3a9e70b7 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -331,7 +331,7 @@ def test_a_xml_1(self): compare(".", gold_path("html/gold_x_xml"), left_extra=True, scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), - (r'\s*.*?\s*', '%s' % source_path), + (r'\s*.*?\s*', '%s' % re.escape(source_path)), (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), ]) @@ -360,7 +360,7 @@ def test_a_xml_2(self): compare("xml_2", gold_path("html/gold_x_xml"), scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), - (r'\s*.*?\s*', '%s' % source_path), + (r'\s*.*?\s*', '%s' % re.escape(source_path)), (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), ]) @@ -385,6 +385,6 @@ def choice(x): compare("y_xml_branch", gold_path("html/gold_y_xml_branch"), scrubs=[ (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), (r' version="[-.\w]+"', ' version="VERSION"'), - (r'\s*.*?\s*', '%s' % source_path), + (r'\s*.*?\s*', '%s' % re.escape(source_path)), (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), ]) From 595029f5b7c9923d080e61be599e0aa1799936f3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 30 Aug 2018 20:18:35 -0400 Subject: [PATCH 211/952] Don't keep the sqlite db open for long --- coverage/sqldata.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 87c157391..c120d82e7 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -85,10 +85,10 @@ def _choose_filename(self): self.filename += "." + suffix def _reset(self): - self._file_map = {} if self._db is not None: self._db.close() self._db = None + self._file_map = {} self._have_used = False def _create_db(self): @@ -126,11 +126,12 @@ def _open_db(self): self.filename, schema_version, SCHEMA_VERSION ) ) - for row in self._db.execute("select has_lines, has_arcs from meta"): - self._has_lines, self._has_arcs = row - for path, id in self._db.execute("select path, id from file"): - self._file_map[path] = id + for row in self._db.execute("select has_lines, has_arcs from meta"): + self._has_lines, self._has_arcs = row + + for path, id in self._db.execute("select path, id from file"): + self._file_map[path] = id def _connect(self): if self._db is None: @@ -331,8 +332,8 @@ def erase(self, parallel=False): file_be_gone(filename) def read(self): - self._connect() # TODO: doesn't look right - self._have_used = True + with self._connect(): # TODO: doesn't look right + self._have_used = True def write(self): """Write the collected coverage data to a file.""" @@ -410,6 +411,9 @@ def __init__(self, filename, debug): if self.debug: self.debug.write("Connecting to {!r}".format(filename)) self.filename = filename + self.nest = 0 + + def connect(self): self.con = sqlite3.connect(self.filename) # This pragma makes writing faster. It disables rollbacks, but we never need them. @@ -423,11 +427,17 @@ def close(self): self.con.close() def __enter__(self): - self.con.__enter__() + if self.nest == 0: + self.connect() + self.con.__enter__() + self.nest += 1 return self def __exit__(self, exc_type, exc_value, traceback): - return self.con.__exit__(exc_type, exc_value, traceback) + self.nest -= 1 + if self.nest == 0: + self.con.__exit__(exc_type, exc_value, traceback) + self.close() def execute(self, sql, parameters=()): if self.debug: From 206f13aa721ed166bdb0f9e2f91e640e27201d71 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 31 Aug 2018 15:45:43 -0400 Subject: [PATCH 212/952] More tests need clear separation of databases --- tests/test_data.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_data.py b/tests/test_data.py index 15da32e57..60fd341e5 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -240,13 +240,13 @@ def test_cant_change_file_tracer_name(self): covdata.add_file_tracers({"p1.foo": "p1.plugin.foo"}) def test_update_lines(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_lines(LINES_1) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_lines(LINES_2) - covdata3 = CoverageData() + covdata3 = CoverageData(suffix='3') covdata3.update(covdata1) covdata3.update(covdata2) @@ -255,13 +255,13 @@ def test_update_lines(self): self.assertEqual(covdata3.run_infos(), []) def test_update_arcs(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_arcs(ARCS_3) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_arcs(ARCS_4) - covdata3 = CoverageData() + covdata3 = CoverageData(suffix='3') covdata3.update(covdata1) covdata3.update(covdata2) @@ -289,10 +289,10 @@ def test_update_run_info(self): ]) def test_update_cant_mix_lines_and_arcs(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_lines(LINES_1) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_arcs(ARCS_3) with self.assertRaisesRegex(CoverageException, "Can't combine arc data with line data"): @@ -302,7 +302,7 @@ def test_update_cant_mix_lines_and_arcs(self): covdata2.update(covdata1) def test_update_file_tracers(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_lines({ "p1.html": dict.fromkeys([1, 2, 3, 4]), "p2.html": dict.fromkeys([5, 6, 7]), @@ -313,7 +313,7 @@ def test_update_file_tracers(self): "p2.html": "html.plugin2", }) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_lines({ "p1.html": dict.fromkeys([3, 4, 5, 6]), "p2.html": dict.fromkeys([7, 8, 9]), @@ -326,7 +326,7 @@ def test_update_file_tracers(self): "p3.foo": "foo_plugin", }) - covdata3 = CoverageData() + covdata3 = CoverageData(suffix='3') covdata3.update(covdata1) covdata3.update(covdata2) self.assertEqual(covdata3.file_tracer("p1.html"), "html.plugin") @@ -335,11 +335,11 @@ def test_update_file_tracers(self): self.assertEqual(covdata3.file_tracer("main.py"), "") def test_update_conflicting_file_tracers(self): - covdata1 = CoverageData() + covdata1 = CoverageData(suffix='1') covdata1.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) covdata1.add_file_tracers({"p1.html": "html.plugin"}) - covdata2 = CoverageData() + covdata2 = CoverageData(suffix='2') covdata2.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) covdata2.add_file_tracers({"p1.html": "html.other_plugin"}) From fcf665bb4db940dc36d036bb09cbda0bb1ac39af Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 31 Aug 2018 16:10:11 -0400 Subject: [PATCH 213/952] SQLite on windows has slightly different messages --- tests/test_api.py | 2 +- tests/test_process.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 4d6ba9296..b44a5d340 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -375,7 +375,7 @@ def test_combining_corrupt_data(self): r"Couldn't read data from '.*\.coverage\.foo': " r"CoverageException: Doesn't seem to be a coverage\.py data file" r"|" # SQL message: - r"Couldn't use data file '.*\.coverage\.foo': file is encrypted or is not a database" + r"Couldn't use data file '.*\.coverage\.foo': file (is encrypted or )?is not a database" r")" ) with self.assert_warnings(cov, [warning_regex]): diff --git a/tests/test_process.py b/tests/test_process.py index 6174b9f7c..626c8094a 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -132,7 +132,7 @@ def test_combine_parallel_data_with_a_corrupt_file(self): r"CoverageException: Doesn't seem to be a coverage\.py data file" r"|" # SQL message: r"Coverage.py warning: Couldn't use data file '.*\.coverage\.bad': " - r"file is encrypted or is not a database" + r"file (is encrypted or )?is not a database" r")" ) self.assertRegex(out, warning_regex) @@ -170,7 +170,7 @@ def test_combine_no_usable_files(self): r"CoverageException: Doesn't seem to be a coverage\.py data file" r"|" # SQL message: r"Coverage.py warning: Couldn't use data file '.*\.coverage.bad{0}': " - r"file is encrypted or is not a database" + r"file (is encrypted or )?is not a database" r")" .format(n) ) From 8bb8175d9e14f1a47180ccd356060d5068bc769b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 2 Sep 2018 07:24:58 -0400 Subject: [PATCH 214/952] More tests need temp directories --- tests/test_plugins.py | 2 -- tests/test_summary.py | 4 ---- 2 files changed, 6 deletions(-) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 04eea3dff..b1614832c 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -863,8 +863,6 @@ def coverage_init(reg, options): class ConfigurerPluginTest(CoverageTest): """Test configuring plugins.""" - run_in_temp_dir = False - def test_configurer_plugin(self): cov = coverage.Coverage() cov.set_option("run:plugins", ["tests.plugin_config"]) diff --git a/tests/test_summary.py b/tests/test_summary.py index 980fd3d4d..936df7fa5 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -695,8 +695,6 @@ class SummaryTest2(UsingModulesMixin, CoverageTest): # needs of their setUp, rather than the product features they are testing. # There's probably a better way to organize these. - run_in_temp_dir = False - def test_empty_files(self): # Shows that empty files like __init__.py are listed as having zero # statements, not one statement. @@ -752,8 +750,6 @@ def test_xml(self): class TestSummaryReporterConfiguration(CoverageTest): """Tests of SummaryReporter.""" - run_in_temp_dir = False - LINES_1 = { os.path.join(TESTS_DIR, "test_api.py"): dict.fromkeys(range(400)), os.path.join(TESTS_DIR, "test_backward.py"): dict.fromkeys(range(20)), From 56b9c7e4db40df6515d4ca5d913cb4678da2b753 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 2 Sep 2018 09:04:49 -0400 Subject: [PATCH 215/952] Move fiddly fnmatch logic into its own testable function --- coverage/files.py | 62 +++++++++++++++++++++++++++------------------ tests/test_files.py | 51 ++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/coverage/files.py b/coverage/files.py index 70fde9dba..5beb518d7 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -260,19 +260,8 @@ def match(self, module_name): class FnmatchMatcher(object): """A matcher for files by file name pattern.""" def __init__(self, pats): - self.pats = pats[:] - # fnmatch is platform-specific. On Windows, it does the Windows thing - # of treating / and \ as equivalent. But on other platforms, we need to - # take care of that ourselves. - fnpats = (fnmatch.translate(p) for p in pats) - # Python3.7 fnmatch translates "/" as "/", before that, it translates as "\/", - # so we have to deal with maybe a backslash. - fnpats = (re.sub(r"\\?/", r"[\\\\/]", p) for p in fnpats) - flags = 0 - if env.WINDOWS: - # Windows is also case-insensitive, so make the regex case-insensitive. - flags |= re.IGNORECASE - self.re = re.compile(join_regex(fnpats), flags=flags) + self.pats = list(pats) + self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS) def __repr__(self): return "" % self.pats @@ -296,6 +285,39 @@ def sep(s): return the_sep +def fnmatches_to_regex(patterns, case_insensitive=False, partial=False): + """Convert fnmatch patterns to a compiled regex that matches any of them. + + Slashes are always converted to match either slash or backslash, for + Windows support, even when running elsewhere. + + If `partial` is true, then the pattern will match if the target string + starts with the pattern. Otherwise, it must match the entire string. + + Returns: a compiled regex object. Use the .match method to compare target + strings. + + """ + regexes = (fnmatch.translate(pattern) for pattern in patterns) + # Python3.7 fnmatch translates "/" as "/". Before that, it translates as "\/", + # so we have to deal with maybe a backslash. + regexes = (re.sub(r"\\?/", r"[\\\\/]", regex) for regex in regexes) + + if partial: + # fnmatch always adds a \Z to match the whole string, which we don't + # want, so we remove the \Z. While removing it, we only replace \Z if + # followed by paren (introducing flags), or at end, to keep from + # destroying a literal \Z in the pattern. + regexes = (re.sub(r'\\Z(\(\?|$)', r'\1', regex) for regex in regexes) + + flags = 0 + if case_insensitive: + flags |= re.IGNORECASE + compiled = re.compile(join_regex(regexes), flags=flags) + + return compiled + + class PathAliases(object): """A collection of aliases for paths. @@ -343,18 +365,8 @@ def add(self, pattern, result): if not pattern.endswith(pattern_sep): pattern += pattern_sep - # Make a regex from the pattern. fnmatch always adds a \Z to - # match the whole string, which we don't want, so we remove the \Z. - # While removing it, we only replace \Z if followed by paren, or at - # end, to keep from destroying a literal \Z in the pattern. - regex_pat = fnmatch.translate(pattern) - regex_pat = re.sub(r'\\Z(\(|$)', r'\1', regex_pat) - - # We want */a/b.py to match on Windows too, so change slash to match - # either separator. - regex_pat = regex_pat.replace(r"\/", r"[\\/]") - # We want case-insensitive matching, so add that flag. - regex = re.compile(r"(?i)" + regex_pat) + # Make a regex from the pattern. + regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True) # Normalize the result: it must end with a path separator. result_sep = sep(result) diff --git a/tests/test_files.py b/tests/test_files.py index 2e705a1be..b4490ea64 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -12,7 +12,7 @@ from coverage import files from coverage.files import ( TreeMatcher, FnmatchMatcher, ModuleMatcher, PathAliases, - find_python_files, abs_file, actual_path, flat_rootname, + find_python_files, abs_file, actual_path, flat_rootname, fnmatches_to_regex, ) from coverage.misc import CoverageException from coverage import env @@ -77,6 +77,55 @@ def test_flat_rootname(original, flat): assert flat_rootname(original) == flat +@pytest.mark.parametrize( + "patterns, case_insensitive, partial," + "matches," + "nomatches", +[ + ( + ["abc", "xyz"], False, False, + ["abc", "xyz"], + ["ABC", "xYz", "abcx", "xabc", "axyz", "xyza"], + ), + ( + ["abc", "xyz"], True, False, + ["abc", "xyz", "Abc", "XYZ", "AbC"], + ["abcx", "xabc", "axyz", "xyza"], + ), + ( + ["abc/hi.py"], True, False, + ["abc/hi.py", "ABC/hi.py", r"ABC\hi.py"], + ["abc_hi.py", "abc/hi.pyc"], + ), + ( + [r"abc\hi.py"], True, False, + [r"abc\hi.py", r"ABC\hi.py"], + ["abc/hi.py", "ABC/hi.py", "abc_hi.py", "abc/hi.pyc"], + ), + ( + ["abc/*/hi.py"], True, False, + ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], + ["abc/hi.py", "abc/hi.pyc"], + ), + ( + ["abc/[a-f]*/hi.py"], True, False, + ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], + ["abc/zoo/hi.py", "abc/hi.py", "abc/hi.pyc"], + ), + ( + ["abc/"], True, True, + ["abc/foo/hi.py", "ABC/foo/bar/hi.py", r"ABC\foo/bar/hi.py"], + ["abcd/foo.py", "xabc/hi.py"], + ), +]) +def test_fnmatches_to_regex(patterns, case_insensitive, partial, matches, nomatches): + regex = fnmatches_to_regex(patterns, case_insensitive=case_insensitive, partial=partial) + for s in matches: + assert regex.match(s) + for s in nomatches: + assert not regex.match(s) + + class MatcherTest(CoverageTest): """Tests of file matchers.""" From 66fdf66e8bd9e77ef3a4c2616bfa4edf4cbe5cb5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 2 Sep 2018 11:02:02 -0400 Subject: [PATCH 216/952] When scrubbing file names, we don't care about slash direction --- tests/test_html.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/tests/test_html.py b/tests/test_html.py index b4dd4606c..9efc3c233 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -565,6 +565,15 @@ def test_cant_find_static_files(self): with self.assertRaisesRegex(CoverageException, msg): cov.html_report() +def filepath_to_regex(path): + """Create a regex for scrubbing a file path.""" + regex = re.escape(path) + # If there's a backslash, let it match either slash. + regex = regex.replace(r"\\", r"[\\/]") + if env.WINDOWS: + regex = "(?i)" + regex + return regex + def compare_html(dir1, dir2): """Specialized compare function for HTML files.""" @@ -575,13 +584,13 @@ def compare_html(dir1, dir2): # Some words are identifiers in one version, keywords in another. (r'(print|True|False)', r'\2'), # Occasionally an absolute path is in the HTML report. - (re.escape(TESTS_DIR), 'TESTS_DIR'), + (filepath_to_regex(TESTS_DIR), 'TESTS_DIR'), (r'/Users/ned/coverage/trunk/tests', 'TESTS_DIR'), - (flat_rootname(unicode_class(TESTS_DIR)), '_TESTS_DIR'), + (filepath_to_regex(flat_rootname(unicode_class(TESTS_DIR))), '_TESTS_DIR'), (flat_rootname(u'/Users/ned/coverage/trunk/tests'), '_TESTS_DIR'), # The temp dir the tests make. - (re.escape(os.getcwd()), 'TEST_TMPDIR'), - (flat_rootname(unicode_class(os.getcwd())), '_TEST_TMPDIR'), + (filepath_to_regex(os.getcwd()), 'TEST_TMPDIR'), + (filepath_to_regex(flat_rootname(unicode_class(os.getcwd()))), '_TEST_TMPDIR'), (r'/private/var/folders/[\w/]{35}/coverage_test/tests_test_html_\w+_\d{8}', 'TEST_TMPDIR'), (r'_private_var_folders_\w{35}_coverage_test_tests_test_html_\w+_\d{8}', '_TEST_TMPDIR'), ] From d7b20337e091075198c9c1efd726a9ac71957b26 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 2 Sep 2018 16:00:06 -0400 Subject: [PATCH 217/952] SQLite on py2 doesn't like opening files with non-ascii chars in the path --- coverage/sqldata.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index c120d82e7..91508586a 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -414,7 +414,11 @@ def __init__(self, filename, debug): self.nest = 0 def connect(self): - self.con = sqlite3.connect(self.filename) + # SQLite on Windows on py2 won't open a file if the filename argument + # has non-ascii characters in it. Opening a relative file name avoids + # a problem if the current directory has non-ascii. + filename = os.path.relpath(self.filename) + self.con = sqlite3.connect(filename) # This pragma makes writing faster. It disables rollbacks, but we never need them. # PyPy needs the .close() calls here, or sqlite gets twisted up: From 4d00a2f35570a16456f4e3d224d3a22bbc3f5984 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 3 Sep 2018 09:23:45 -0400 Subject: [PATCH 218/952] Polish up the change history for SQLite storage --- CHANGES.rst | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1a10a7d58..521d10ec8 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,9 +22,16 @@ Unreleased can be created earlier than it used to. A large amount of code was refactored to support this change. -- The old data format is still available (for now) by setting the environment - variable COVERAGE_STORAGE=json. Please tell me if you think you need to keep - the JSON format. + - Because the data file is created differently than previous releases, you + may need ``parallel=true`` where you didn't before. + + - The old data format is still available (for now) by setting the environment + variable COVERAGE_STORAGE=json. Please tell me if you think you need to + keep the JSON format. + + - The database schema is guaranteed to change in the future, to support new + features. I'm looking for opinions about making the schema part of the + public API to coverage.py or not. - Development moved from `Bitbucket`_ to `GitHub`_. From 43b7cd8181640ab525e06795ea3ae53d828b6ca6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 3 Sep 2018 09:31:53 -0400 Subject: [PATCH 219/952] Updates for 5.0a2 --- CHANGES.rst | 6 ++++-- README.rst | 2 +- doc/conf.py | 2 +- doc/index.rst | 4 ++-- howto.txt | 6 +++--- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 521d10ec8..f54cecb6c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,8 +14,10 @@ Change history for Coverage.py .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- -Unreleased ----------- +.. _changes_50a2: + +Version 5.0a2 --- 2018-09-03 +---------------------------- - Coverage's data storage has changed. In version 4.x, .coverage files were basically JSON. Now, they are SQLite databases. This means the data file diff --git a/README.rst b/README.rst index 9898276c6..b1b2e0ee9 100644 --- a/README.rst +++ b/README.rst @@ -31,7 +31,7 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on .. _GitHub: https://github.com/nedbat/coveragepy -**New in 5.0:** Dropped support for Python 2.6 and 3.3. +**New in 5.0:** SQLite data storage, dropped support for Python 2.6 and 3.3. New in 4.5: Configurator plug-ins. diff --git a/doc/conf.py b/doc/conf.py index 9936d1b97..a87047ebe 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -58,7 +58,7 @@ # The short X.Y version. version = '5.0' # CHANGEME # The full version, including alpha/beta/rc tags. -release = '5.0a1' # CHANGEME +release = '5.0a2' # CHANGEME # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/index.rst b/doc/index.rst index 78496cdb3..0a7950114 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -77,8 +77,8 @@ not. .. ifconfig:: prerelease - The latest version is coverage.py 5.0a1, released June 5th 2018. It is - supported on: + The latest version is coverage.py 5.0a2, released September 3rd 2018. + It is supported on: * Python versions 2.7, 3.4, 3.5, 3.6, and 3.7. diff --git a/howto.txt b/howto.txt index b23757dcb..36d1cf486 100644 --- a/howto.txt +++ b/howto.txt @@ -19,7 +19,7 @@ - Check that the docs build correctly: $ tox -e doc - Done with changes to source files, check them in. - - hg push + - git push - Generate new sample_html to get the latest, incl footer version number: make clean pip install -e . @@ -62,7 +62,7 @@ - Visit https://pypi.python.org/pypi?:action=pkg_edit&name=coverage : - show/hide the proper versions. - Tag the tree - - hg tag -m "Coverage 3.0.1" coverage-3.0.1 + - git tag coverage-3.0.1 - Bump version: - coverage/version.py - increment version number @@ -70,7 +70,7 @@ - set to alpha-0 if just released - CHANGES.rst - add an "Unreleased" section to the top. -- push hg changes +- push git changes - Update nedbatchelder.com - Blog post? - Update readthedocs From c28e6b999bca96deeb4d9da54985b25605889f5c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 3 Sep 2018 10:55:26 -0400 Subject: [PATCH 220/952] How-to fixes --- howto.txt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/howto.txt b/howto.txt index 36d1cf486..e915c4463 100644 --- a/howto.txt +++ b/howto.txt @@ -58,11 +58,9 @@ - Update PyPi: - upload kits: - $ make kit_upload - - DON'T NEED TO DO THIS ANY MORE? - - Visit https://pypi.python.org/pypi?:action=pkg_edit&name=coverage : - - show/hide the proper versions. - Tag the tree - git tag coverage-3.0.1 + - git push --tags - Bump version: - coverage/version.py - increment version number From 1ed59ddc5f8fef4b4446ba51408b561aa0e0f99a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 3 Sep 2018 10:56:19 -0400 Subject: [PATCH 221/952] Bump version --- CHANGES.rst | 6 ++++++ coverage/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index f54cecb6c..6ed73e595 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,12 @@ Change history for Coverage.py .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- +Unreleased +---------- + +- nothing yet. + + .. _changes_50a2: Version 5.0a2 --- 2018-09-03 diff --git a/coverage/version.py b/coverage/version.py index 2639941a1..b56980376 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 0, 0, 'alpha', 2) +version_info = (5, 0, 0, 'alpha', 3) def _make_version(major, minor, micro, releaselevel, serial): From 18f4e1a2b7f54da3bb661770f8cb3cc9402e974b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 4 Sep 2018 06:26:28 -0400 Subject: [PATCH 222/952] Remove aspectlib debugging --- coverage/control.py | 2 +- coverage/debug.py | 37 ------------------------------------- 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index c4022f1fb..08d219591 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -870,7 +870,7 @@ def plugin_info(plugins): # Mega debugging... -if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): +if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging from coverage.debug import decorate_methods, show_calls Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage) diff --git a/coverage/debug.py b/coverage/debug.py index c2d7b5174..f990ca3b5 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -352,40 +352,3 @@ def _clean_stack_line(s): # pragma: debugging s = s.replace(os.path.dirname(os.__file__) + '/', '') s = s.replace(sys.prefix + '/', '') return s - - -def filter_aspectlib_frames(text): # pragma: debugging - """Aspectlib prints stack traces, but includes its own frames. Scrub those out.""" - # <<< aspectlib/__init__.py:257:function_wrapper < igor.py:143:run_tests < ... - text = re.sub(r"(?<= )aspectlib/[^.]+\.py:\d+:\w+ < ", "", text) - return text - - -def enable_aspectlib_maybe(): # pragma: debugging - """For debugging, we can use aspectlib to trace execution. - - Define COVERAGE_ASPECTLIB to enable and configure aspectlib to trace - execution:: - - $ export COVERAGE_ASPECTLIB=coverage.Coverage:coverage.data.CoverageData - $ coverage run blah.py ... - - This will trace all the public methods on Coverage and CoverageData, - writing the information to covaspect.txt. - - """ - aspects = os.environ.get("COVERAGE_ASPECTLIB", "") - if not aspects: - return - - import aspectlib # pylint: disable=import-error - import aspectlib.debug # pylint: disable=import-error - - filters = [add_pid_and_tid, filter_aspectlib_frames] - aspects_file = DebugOutputFile.get_one(None, show_process=True, filters=filters) - aspect_log = aspectlib.debug.log( - print_to=aspects_file, attributes=['id'], stacktrace=30, use_logging=False - ) - public_methods = re.compile(r'^(__init__|[a-zA-Z].*)$') - for aspect in aspects.split(':'): - aspectlib.weave(aspect, aspect_log, methods=public_methods) From 8051d361f8d0684028b6dc71196fc941dd24ddb6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 6 Sep 2018 19:51:32 -0400 Subject: [PATCH 223/952] Finish removing aspectlib --- coverage/__init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/coverage/__init__.py b/coverage/__init__.py index 0f17c0a1f..5edb75244 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -12,7 +12,6 @@ from coverage.control import Coverage, process_startup from coverage.data import CoverageData -from coverage.debug import enable_aspectlib_maybe from coverage.misc import CoverageException from coverage.plugin import CoveragePlugin, FileTracer, FileReporter from coverage.pytracer import PyTracer @@ -20,9 +19,6 @@ # Backward compatibility. coverage = Coverage -# Possibly enable aspectlib to debug our execution. -enable_aspectlib_maybe() - # On Windows, we encode and decode deep enough that something goes wrong and # the encodings.utf_8 module is loaded and then unloaded, I don't know why. # Adding a reference here prevents it from being unloaded. Yuk. From f12fcc57542ada16ae24fefea7d34d82b9302a03 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 6 Sep 2018 20:23:44 -0400 Subject: [PATCH 224/952] Upgrade testing tools --- requirements/pytest.pip | 4 ++-- requirements/tox.pip | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/pytest.pip b/requirements/pytest.pip index 139c6479a..a946a59af 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -3,9 +3,9 @@ # The pytest specifics used by coverage.py -pytest==3.7.1 +pytest==3.8.0 pluggy>=0.7 # pytest needs this, but pip doesn't understand -pytest-xdist==1.22.5 +pytest-xdist==1.23.0 flaky==3.4.0 mock==2.0.0 PyContracts==1.8.3 diff --git a/requirements/tox.pip b/requirements/tox.pip index a84d77e9e..07d889e6f 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -1,4 +1,4 @@ # The version of tox used by coverage.py -tox==3.1.3 +tox==3.2.1 # Adds env recreation on requirements file changes. tox-battery==0.5.1 From ac7d7d9d0d10e52b4f45eded597ae962f63c4905 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 8 Sep 2018 07:54:51 -0400 Subject: [PATCH 225/952] Clarify report() arguments --- coverage/control.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 08d219591..04cb4b5d1 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -695,16 +695,26 @@ def report( file=None, # pylint: disable=redefined-builtin omit=None, include=None, skip_covered=None, ): - """Write a summary report to `file`. + """Write a textual summary report to `file`. Each module in `morfs` is listed, with counts of statements, executed statements, missing statements, and a list of lines missed. + If `show_missing` is true, then details of which lines or branches are + missing will be included in the report. If `ignore_errors` is true, + then a failure while reporting a single file will not stop the entire + report. + + `file` is a file-like object, suitable for writing. + `include` is a list of file name patterns. Files that match will be included in the report. Files matching `omit` will not be included in the report. - If `skip_covered` is True, don't report on files with 100% coverage. + If `skip_covered` is true, don't report on files with 100% coverage. + + All of the arguments default to the settings read from the + :ref:`configuration file `. Returns a float, the total percentage covered. From f4a99853c8dc38a2feafcd0d575a99633786d22d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 8 Sep 2018 13:38:36 -0400 Subject: [PATCH 226/952] Tidy up --- coverage/control.py | 1 - coverage/debug.py | 1 - 2 files changed, 2 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 04cb4b5d1..5d42af777 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -3,7 +3,6 @@ """Core control stuff for coverage.py.""" - import atexit import os import platform diff --git a/coverage/debug.py b/coverage/debug.py index f990ca3b5..7cbda792b 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -8,7 +8,6 @@ import inspect import itertools import os -import re import sys try: import _thread From dde0a3ef3b88eb79bff8a36943cf934452eb8c26 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 8 Sep 2018 16:17:09 -0400 Subject: [PATCH 227/952] Move variable substitution to be independent --- coverage/config.py | 19 ++----------------- coverage/misc.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_misc.py | 18 +++++++++++++++++- 3 files changed, 56 insertions(+), 18 deletions(-) diff --git a/coverage/config.py b/coverage/config.py index 061fa304e..69c929b4c 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -10,6 +10,7 @@ from coverage import env from coverage.backward import configparser, iitems, string_class from coverage.misc import contract, CoverageException, isolate_module +from coverage.misc import substitute_variables os = isolate_module(os) @@ -85,23 +86,7 @@ def get(self, section, option, *args, **kwargs): # pylint: disable=argume raise configparser.NoOptionError v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs) - def dollar_replace(m): - """Called for each $replacement.""" - # Only one of the groups will have matched, just get its text. - word = next(w for w in m.groups() if w is not None) # pragma: part covered - if word == "$": - return "$" - else: - return os.environ.get(word, '') - - dollar_pattern = r"""(?x) # Use extended regex syntax - \$(?: # A dollar sign, then - (?P\w+) | # a plain word, - {(?P\w+)} | # or a {-wrapped word, - (?P[$]) # or a dollar sign. - ) - """ - v = re.sub(dollar_pattern, dollar_replace, v) + v = substitute_variables(v) return v def getlist(self, section, option): diff --git a/coverage/misc.py b/coverage/misc.py index ab950846b..e2031852e 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -8,6 +8,7 @@ import inspect import locale import os +import re import sys import types @@ -250,6 +251,42 @@ def _needs_to_implement(that, func_name): ) +def substitute_variables(text, variables=os.environ): + """Substitute ``${VAR}`` variables in `text` with their values. + + Variables in the text can take a number of shell-inspired forms:: + + $VAR + ${VAR} + + A dollar can be inserted with ``$$``. + + `variables` is a dictionary of variable values, defaulting to the + environment variables. + + Returns the resulting text with values substituted. + + """ + def dollar_replace(m): + """Called for each $replacement.""" + # Only one of the groups will have matched, just get its text. + word = next(w for w in m.groups() if w is not None) # pragma: part covered + if word == "$": + return "$" + else: + return variables.get(word, '') + + dollar_pattern = r"""(?x) # Use extended regex syntax + \$(?: # A dollar sign, then + (?P\w+) | # a plain word, + {(?P\w+)} | # or a {-wrapped word, + (?P[$]) # or a dollar sign. + ) + """ + text = re.sub(dollar_pattern, dollar_replace, text) + return text + + class BaseCoverageException(Exception): """The base of all Coverage exceptions.""" pass diff --git a/tests/test_misc.py b/tests/test_misc.py index 1d01537ba..77200b3ce 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,7 +6,7 @@ import pytest from coverage.misc import contract, dummy_decorator_with_args, file_be_gone -from coverage.misc import format_lines, Hasher, one_of +from coverage.misc import format_lines, Hasher, one_of, substitute_variables from tests.coveragetest import CoverageTest @@ -135,3 +135,19 @@ def undecorated(a=None, b=None): # pylint: disable=missing-docstr ]) def test_format_lines(statements, lines, result): assert format_lines(statements, lines) == result + + +@pytest.mark.parametrize("before, after", [ + ("Nothing to do", "Nothing to do"), + ("Dollar: $$", "Dollar: $"), + ("Simple: $FOO is fooey", "Simple: fooey is fooey"), + ("Braced: X${FOO}X.", "Braced: XfooeyX."), + ("Missing: x$NOTHING is x", "Missing: x is x"), + ("Multiple: $$ $FOO $BAR ${FOO}", "Multiple: $ fooey xyzzy fooey"), +]) +def test_substitute_variables(before, after): + variables = { + 'FOO': 'fooey', + 'BAR': 'xyzzy', + } + assert substitute_variables(before, variables) == after From 4fe7c61916e9599c459e231245dccc92db2affa8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 8 Sep 2018 16:42:44 -0400 Subject: [PATCH 228/952] Strict variable substitution is now an option --- CHANGES.rst | 4 +++- coverage/misc.py | 13 +++++++++++-- doc/config.rst | 6 ++++-- tests/test_misc.py | 25 +++++++++++++++++++------ 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6ed73e595..23aa0921c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,7 +17,9 @@ Change history for Coverage.py Unreleased ---------- -- nothing yet. +- Environment variable substitution in configuration files can now be strict: + using a question mark suffix like ``${VARNAME?}`` will raise an error if + ``VARNAME`` is not defined as an environment variable. .. _changes_50a2: diff --git a/coverage/misc.py b/coverage/misc.py index e2031852e..c6a7c8cf6 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -258,6 +258,7 @@ def substitute_variables(text, variables=os.environ): $VAR ${VAR} + ${VAR?} strict: an error if VAR isn't defined. A dollar can be inserted with ``$$``. @@ -270,16 +271,24 @@ def substitute_variables(text, variables=os.environ): def dollar_replace(m): """Called for each $replacement.""" # Only one of the groups will have matched, just get its text. - word = next(w for w in m.groups() if w is not None) # pragma: part covered + word = m.expand(r"\g\g\g") if word == "$": return "$" else: + strict = bool(m.group('strict')) + if strict: + if word not in variables: + msg = "Variable {} is undefined: {}".format(word, text) + raise CoverageException(msg) return variables.get(word, '') dollar_pattern = r"""(?x) # Use extended regex syntax \$(?: # A dollar sign, then (?P\w+) | # a plain word, - {(?P\w+)} | # or a {-wrapped word, + { # or a {-wrapped word, + (?P\w+) + (?P\??) # with maybe a strict marker + } | (?P[$]) # or a dollar sign. ) """ diff --git a/doc/config.rst b/doc/config.rst index 3e76e3d00..ab8746193 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -59,8 +59,10 @@ or ``0`` and are case-insensitive. Environment variables can be substituted in by using dollar signs: ``$WORD`` or ``${WORD}`` will be replaced with the value of ``WORD`` in the environment. -A dollar sign can be inserted with ``$$``. Missing environment variables -will result in empty strings with no error. +A dollar sign can be inserted with ``$$``. If you want to raise an error if +an environment variable is undefined, use a question mark suffix: ``${WORD?}``. +Otherwise, missing environment variables will result in empty strings with no +error. Many sections and values correspond roughly to commands and options in the :ref:`command-line interface `. diff --git a/tests/test_misc.py b/tests/test_misc.py index 77200b3ce..65476928f 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -7,6 +7,7 @@ from coverage.misc import contract, dummy_decorator_with_args, file_be_gone from coverage.misc import format_lines, Hasher, one_of, substitute_variables +from coverage.misc import CoverageException from tests.coveragetest import CoverageTest @@ -137,17 +138,29 @@ def test_format_lines(statements, lines, result): assert format_lines(statements, lines) == result +VARS = { + 'FOO': 'fooey', + 'BAR': 'xyzzy', +} + @pytest.mark.parametrize("before, after", [ ("Nothing to do", "Nothing to do"), ("Dollar: $$", "Dollar: $"), ("Simple: $FOO is fooey", "Simple: fooey is fooey"), ("Braced: X${FOO}X.", "Braced: XfooeyX."), - ("Missing: x$NOTHING is x", "Missing: x is x"), + ("Missing: x${NOTHING}y is xy", "Missing: xy is xy"), ("Multiple: $$ $FOO $BAR ${FOO}", "Multiple: $ fooey xyzzy fooey"), + ("Ill-formed: ${%5} ${{HI}} ${", "Ill-formed: ${%5} ${{HI}} ${"), + ("Strict: ${FOO?} is there", "Strict: fooey is there"), ]) def test_substitute_variables(before, after): - variables = { - 'FOO': 'fooey', - 'BAR': 'xyzzy', - } - assert substitute_variables(before, variables) == after + assert substitute_variables(before, VARS) == after + +@pytest.mark.parametrize("text", [ + "Strict: ${NOTHING?} is an error", +]) +def test_substitute_variables_errors(text): + with pytest.raises(CoverageException) as exc_info: + substitute_variables(text, VARS) + assert text in str(exc_info.value) + assert "Variable NOTHING is undefined" in str(exc_info.value) From c001d1676de46aef12f80a21675937756e594acf Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 8 Sep 2018 17:19:56 -0400 Subject: [PATCH 229/952] Py2 doesn't like using unmatched groups in regex results --- coverage/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/misc.py b/coverage/misc.py index c6a7c8cf6..037332f54 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -271,7 +271,7 @@ def substitute_variables(text, variables=os.environ): def dollar_replace(m): """Called for each $replacement.""" # Only one of the groups will have matched, just get its text. - word = m.expand(r"\g\g\g") + word = ''.join(m.group(name) or '' for name in ['v1', 'v2', 'char']) if word == "$": return "$" else: From 69d106fec5ac2e9aba1146c0004d961e8cb903f5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 8 Sep 2018 16:56:08 -0400 Subject: [PATCH 230/952] Defaultable variable substitution --- CHANGES.rst | 7 ++++--- coverage/misc.py | 15 ++++++++++----- doc/config.rst | 15 +++++++++++---- tests/test_misc.py | 1 + 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 23aa0921c..8f8cd32b0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,9 +17,10 @@ Change history for Coverage.py Unreleased ---------- -- Environment variable substitution in configuration files can now be strict: - using a question mark suffix like ``${VARNAME?}`` will raise an error if - ``VARNAME`` is not defined as an environment variable. +- Environment variable substitution in configuration files now supports two + syntaxes for controlling the behavior of undefined variables: if ``VARNAME`` + is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default + value}`` will use "default value". .. _changes_50a2: diff --git a/coverage/misc.py b/coverage/misc.py index 037332f54..7b8fbb934 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -258,7 +258,8 @@ def substitute_variables(text, variables=os.environ): $VAR ${VAR} - ${VAR?} strict: an error if VAR isn't defined. + ${VAR?} strict: an error if VAR isn't defined. + ${VAR-missing} defaulted: "missing" if VAR isn't defined. A dollar can be inserted with ``$$``. @@ -280,16 +281,20 @@ def dollar_replace(m): if word not in variables: msg = "Variable {} is undefined: {}".format(word, text) raise CoverageException(msg) - return variables.get(word, '') + return variables.get(word, m.group('defval') or '') dollar_pattern = r"""(?x) # Use extended regex syntax \$(?: # A dollar sign, then (?P\w+) | # a plain word, + (?P\$) | # or a dollar sign. { # or a {-wrapped word, (?P\w+) - (?P\??) # with maybe a strict marker - } | - (?P[$]) # or a dollar sign. + (?: + (?P\?) # with a strict marker + | + -(?P[^}]*) # or a default value + )? + } ) """ text = re.sub(dollar_pattern, dollar_replace, text) diff --git a/doc/config.rst b/doc/config.rst index ab8746193..666a13213 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -59,10 +59,17 @@ or ``0`` and are case-insensitive. Environment variables can be substituted in by using dollar signs: ``$WORD`` or ``${WORD}`` will be replaced with the value of ``WORD`` in the environment. -A dollar sign can be inserted with ``$$``. If you want to raise an error if -an environment variable is undefined, use a question mark suffix: ``${WORD?}``. -Otherwise, missing environment variables will result in empty strings with no -error. +A dollar sign can be inserted with ``$$``. Special forms can be used to +control what happens if the variable isn't defined in the environment: + +- If you want to raise an error if an environment variable is undefined, use a + question mark suffix: ``${WORD?}``. + +- If you want to provide a default for missing variables, use a dash with a + default value: ``${WORD-default value}``. + +- Otherwise, missing environment variables will result in empty strings with no + error. Many sections and values correspond roughly to commands and options in the :ref:`command-line interface `. diff --git a/tests/test_misc.py b/tests/test_misc.py index 65476928f..c8c2c9e49 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -152,6 +152,7 @@ def test_format_lines(statements, lines, result): ("Multiple: $$ $FOO $BAR ${FOO}", "Multiple: $ fooey xyzzy fooey"), ("Ill-formed: ${%5} ${{HI}} ${", "Ill-formed: ${%5} ${{HI}} ${"), ("Strict: ${FOO?} is there", "Strict: fooey is there"), + ("Defaulted: ${WUT-missing}!", "Defaulted: missing!"), ]) def test_substitute_variables(before, after): assert substitute_variables(before, VARS) == after From 56c6213c89dfbabb21de3264ec0e024ada9fe568 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 8 Sep 2018 20:56:25 -0400 Subject: [PATCH 231/952] Fix flaky tests On Travis, the html delta tests fail randomly, because they were pulling in pytest code, whose coverage results were changing? Not really sure why. --- tests/test_html.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_html.py b/tests/test_html.py index 9efc3c233..955a85375 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -106,6 +106,12 @@ def setUp(self): self.real_coverage_version = coverage.__version__ self.addCleanup(setattr, coverage, "__version__", self.real_coverage_version) + def run_coverage(self, covargs=None, htmlargs=None): + """For the delta tests, we always want source=. """ + covargs = covargs or {} + covargs['source'] = "." + super(HtmlDeltaTest, self).run_coverage(covargs=covargs, htmlargs=htmlargs) + def test_html_created(self): # Test basic HTML generation: files should be created. self.create_initial_files() From d8da2b9a6cf5c127c9ba9393c716d9dc4dd50cbe Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 9 Sep 2018 07:42:14 -0400 Subject: [PATCH 232/952] Keep windows working in the face of bizarre errors --- coverage/files.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/coverage/files.py b/coverage/files.py index 5beb518d7..b328f6538 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -122,7 +122,9 @@ def actual_path(path): else: try: files = os.listdir(head) - except OSError: + except Exception: + # This will raise OSError, or this bizarre TypeError: + # https://bugs.python.org/issue1776160 files = [] _ACTUAL_PATH_LIST_CACHE[head] = files normtail = os.path.normcase(tail) From 9d0f877142f41a9cf8e872fde5aeec004c345587 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 9 Sep 2018 08:55:25 -0400 Subject: [PATCH 233/952] SimpleReprMixin is a better name --- coverage/debug.py | 4 ++-- coverage/results.py | 4 ++-- coverage/sqldata.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index 7cbda792b..cf90f77ac 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -29,7 +29,7 @@ class DebugControl(object): """Control and output for debugging.""" - show_repr_attr = False # For SimpleRepr + show_repr_attr = False # For SimpleReprMixin def __init__(self, options, output): """Configure the options and output file for debugging.""" @@ -171,7 +171,7 @@ def add_pid_and_tid(text): return text -class SimpleRepr(object): +class SimpleReprMixin(object): """A mixin implementing a simple __repr__.""" def __repr__(self): show_attrs = ( diff --git a/coverage/results.py b/coverage/results.py index fb919c9b8..cab8796e2 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -6,7 +6,7 @@ import collections from coverage.backward import iitems -from coverage.debug import SimpleRepr +from coverage.debug import SimpleReprMixin from coverage.misc import contract, format_lines @@ -158,7 +158,7 @@ def branch_stats(self): return stats -class Numbers(SimpleRepr): +class Numbers(SimpleReprMixin): """The numerical results of measuring coverage. This holds the basic statistics from `Analysis`, and is used to roll diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 91508586a..f9598485a 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -17,7 +17,7 @@ from coverage.backward import iitems from coverage.data import filename_suffix -from coverage.debug import SimpleRepr +from coverage.debug import SimpleReprMixin from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone @@ -60,7 +60,7 @@ """ -class CoverageSqliteData(SimpleRepr): +class CoverageSqliteData(SimpleReprMixin): def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._basename = os.path.abspath(basename or ".coverage") self._suffix = suffix @@ -405,7 +405,7 @@ def run_infos(self): return [] # TODO -class Sqlite(SimpleRepr): +class Sqlite(SimpleReprMixin): def __init__(self, filename, debug): self.debug = debug if (debug and debug.should('sql')) else None if self.debug: From c6b688ac89dca7e8ab5d03ead7799c33f8a92785 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 9 Sep 2018 10:59:14 -0400 Subject: [PATCH 234/952] No need for a separate variable --- coverage/cmdline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 23d2aec3e..d7dfa68cd 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -629,8 +629,7 @@ def do_run(self, options, args): if options.module: self.run_python_module(args[0], args) else: - filename = args[0] - self.run_python_file(filename, args) + self.run_python_file(args[0], args) except NoSource: code_ran = False raise From 42c0c58c452f6f10b26986cefe9cd00688081546 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 9 Sep 2018 13:30:08 -0400 Subject: [PATCH 235/952] Name decorator components to avoid docstring requirement --- coverage/debug.py | 8 ++++---- coverage/misc.py | 9 ++++----- pylintrc | 4 ++-- tests/coveragetest.py | 5 ++--- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index cf90f77ac..9077a3af9 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -303,12 +303,12 @@ def _decorator(cls): def break_in_pudb(func): # pragma: debugging """A function decorator to stop in the debugger for each call.""" @functools.wraps(func) - def wrapper(*args, **kwargs): + def _wrapper(*args, **kwargs): import pudb # pylint: disable=import-error sys.stdout = sys.__stdout__ pudb.set_trace() return func(*args, **kwargs) - return wrapper + return _wrapper OBJ_IDS = itertools.count() @@ -319,7 +319,7 @@ def show_calls(show_args=True, show_stack=False): # pragma: debugging """A method decorator to debug-log each call to the function.""" def _decorator(func): @functools.wraps(func) - def wrapper(self, *args, **kwargs): + def _wrapper(self, *args, **kwargs): oid = getattr(self, OBJ_ID_ATTR, None) if oid is None: oid = "{:08d} {:04d}".format(os.getpid(), next(OBJ_IDS)) @@ -340,7 +340,7 @@ def wrapper(self, *args, **kwargs): msg = "{} {:04d} {}{}\n".format(oid, next(CALLS), func.__name__, extra) DebugOutputFile.get_one().write(msg) return func(self, *args, **kwargs) - return wrapper + return _wrapper return _decorator diff --git a/coverage/misc.py b/coverage/misc.py index 7b8fbb934..a923829d0 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -70,11 +70,11 @@ def one_of(argnames): """Ensure that only one of the argnames is non-None.""" def _decorator(func): argnameset = set(name.strip() for name in argnames.split(",")) - def _wrapped(*args, **kwargs): + def _wrapper(*args, **kwargs): vals = [kwargs.get(name) for name in argnameset] assert sum(val is not None for val in vals) == 1 return func(*args, **kwargs) - return _wrapped + return _wrapper return _decorator else: # pragma: not testing # We aren't using real PyContracts, so just define our decorators as @@ -149,13 +149,12 @@ def expensive(fn): if env.TESTING: attr = "_once_" + fn.__name__ - def _wrapped(self): - """Inner function that checks the cache.""" + def _wrapper(self): if hasattr(self, attr): raise AssertionError("Shouldn't have called %s more than once" % fn.__name__) setattr(self, attr, True) return fn(self) - return _wrapped + return _wrapper else: return fn # pragma: not testing diff --git a/pylintrc b/pylintrc index d4ba155c6..93afc3044 100644 --- a/pylintrc +++ b/pylintrc @@ -133,9 +133,9 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme # Special methods don't: __foo__ # Test methods don't: testXXXX # TestCase overrides don't: setUp, tearDown -# Nested decorator implementations: _decorator, _wrapped +# Nested decorator implementations: _decorator, _wrapper # Dispatched methods don't: _xxx__Xxxx -no-docstring-rgx=__.*__|test[A-Z_].*|setUp|tearDown|_decorator|_wrapped|_.*__.* +no-docstring-rgx=__.*__|test[A-Z_].*|setUp|tearDown|_decorator|_wrapper|_.*__.* # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 15c61ece6..378097c80 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -40,14 +40,13 @@ def convert_skip_exceptions(method): """A decorator for test methods to convert StopEverything to SkipTest.""" @functools.wraps(method) - def wrapper(*args, **kwargs): - """Run the test method, and convert exceptions.""" + def _wrapper(*args, **kwargs): try: result = method(*args, **kwargs) except StopEverything: raise unittest.SkipTest("StopEverything!") return result - return wrapper + return _wrapper class SkipConvertingMetaclass(type): From 189504112784fbafc68ee0757754e8806573d279 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 13 Sep 2018 09:42:26 -0400 Subject: [PATCH 236/952] Check for import order --- coverage/__init__.py | 6 +++--- pylintrc | 3 --- setup.py | 8 +++++--- tests/conftest.py | 3 ++- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/coverage/__init__.py b/coverage/__init__.py index 5edb75244..12644a5c4 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -8,13 +8,14 @@ """ -from coverage.version import __version__, __url__, version_info +import sys from coverage.control import Coverage, process_startup from coverage.data import CoverageData from coverage.misc import CoverageException from coverage.plugin import CoveragePlugin, FileTracer, FileReporter from coverage.pytracer import PyTracer +from coverage.version import __version__, __url__, version_info # Backward compatibility. coverage = Coverage @@ -22,13 +23,12 @@ # On Windows, we encode and decode deep enough that something goes wrong and # the encodings.utf_8 module is loaded and then unloaded, I don't know why. # Adding a reference here prevents it from being unloaded. Yuk. -import encodings.utf_8 +import encodings.utf_8 # pylint: disable=wrong-import-position, wrong-import-order # Because of the "from coverage.control import fooey" lines at the top of the # file, there's an entry for coverage.coverage in sys.modules, mapped to None. # This makes some inspection tools (like pydoc) unable to find the class # coverage.coverage. So remove that entry. -import sys try: del sys.modules['coverage.coverage'] except KeyError: diff --git a/pylintrc b/pylintrc index 93afc3044..c9e04b9ed 100644 --- a/pylintrc +++ b/pylintrc @@ -74,9 +74,6 @@ disable= too-many-ancestors, # Formatting stuff superfluous-parens,bad-continuation, -# I'm fine deciding my own import order, - wrong-import-position, - wrong-import-order, # Messages that are noisy for now, eventually maybe we'll turn them on: invalid-name, protected-access, diff --git a/setup.py b/setup.py index accfd18fa..af99b59b3 100644 --- a/setup.py +++ b/setup.py @@ -9,10 +9,12 @@ import os import sys +# Setuptools has to be imported before distutils, or things break. from setuptools import setup -from distutils.core import Extension # pylint: disable=no-name-in-module, import-error -from distutils.command.build_ext import build_ext # pylint: disable=no-name-in-module, import-error -from distutils import errors # pylint: disable=no-name-in-module +from distutils.core import Extension # pylint: disable=no-name-in-module, import-error, wrong-import-order +from distutils.command.build_ext import build_ext # pylint: disable=no-name-in-module, import-error, wrong-import-order +from distutils import errors # pylint: disable=no-name-in-module, wrong-import-order + # Get or massage our metadata. We exec coverage/version.py so we can avoid # importing the product code into setup.py. diff --git a/tests/conftest.py b/tests/conftest.py index aeccec888..e98025174 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,9 +7,10 @@ This module is run automatically by pytest, to define and enable fixtures. """ -import pytest import warnings +import pytest + from coverage import env From 4876f170e5d59b8a45ae0c6732acb73b8ac566d2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 13 Sep 2018 10:29:05 -0400 Subject: [PATCH 237/952] Fix cyclic import --- coverage/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coverage/__init__.py b/coverage/__init__.py index 12644a5c4..331b304b6 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -10,12 +10,13 @@ import sys +from coverage.version import __version__, __url__, version_info + from coverage.control import Coverage, process_startup from coverage.data import CoverageData from coverage.misc import CoverageException from coverage.plugin import CoveragePlugin, FileTracer, FileReporter from coverage.pytracer import PyTracer -from coverage.version import __version__, __url__, version_info # Backward compatibility. coverage = Coverage From 5fff9a625e91600346cfbaa0022060dfbc9e9fb3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sun, 16 Sep 2018 13:28:15 -0700 Subject: [PATCH 238/952] Correct capitalization of PyPI As spelled on https://pypi.org/. --- howto.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/howto.txt b/howto.txt index e915c4463..ae20d0b97 100644 --- a/howto.txt +++ b/howto.txt @@ -55,7 +55,7 @@ - https://ci.appveyor.com/project/nedbat/coveragepy - $ make download_appveyor - examine the dist directory, and remove anything that looks malformed. -- Update PyPi: +- Update PyPI: - upload kits: - $ make kit_upload - Tag the tree From 181f5a78fdbdb7d6f90a478482512297f3a0f845 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 9 Sep 2018 09:38:27 -0400 Subject: [PATCH 239/952] Super-simple contexts added to the schema --- coverage/sqldata.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 91508586a..0c05ae1f2 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -22,7 +22,11 @@ from coverage.misc import CoverageException, file_be_gone -SCHEMA_VERSION = 1 +# Schema versions: +# 1: Released in 5.0a2 +# 2: Added contexts + +SCHEMA_VERSION = 2 SCHEMA = """ create table coverage_schema ( @@ -40,17 +44,25 @@ unique(path) ); +create table context ( + id integer primary key, + context text, + unique(context) +); + create table line ( file_id integer, + context_id integer, lineno integer, - unique(file_id, lineno) + unique(file_id, context_id, lineno) ); create table arc ( file_id integer, + context_id integer, fromno integer, tono integer, - unique(file_id, fromno, tono) + unique(file_id, context_id, fromno, tono) ); create table tracer ( @@ -78,6 +90,8 @@ def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._has_lines = False self._has_arcs = False + self._context_id = 0 + def _choose_filename(self): self.filename = self._basename suffix = filename_suffix(self._suffix) @@ -181,9 +195,9 @@ def add_lines(self, line_data): with self._connect() as con: for filename, linenos in iitems(line_data): file_id = self._file_id(filename, add=True) - data = [(file_id, lineno) for lineno in linenos] + data = [(file_id, self._context_id, lineno) for lineno in linenos] con.executemany( - "insert or ignore into line (file_id, lineno) values (?, ?)", + "insert or ignore into line (file_id, context_id, lineno) values (?, ?, ?)", data, ) @@ -204,9 +218,9 @@ def add_arcs(self, arc_data): with self._connect() as con: for filename, arcs in iitems(arc_data): file_id = self._file_id(filename, add=True) - data = [(file_id, fromno, tono) for fromno, tono in arcs] + data = [(file_id, self._context_id, fromno, tono) for fromno, tono in arcs] con.executemany( - "insert or ignore into arc (file_id, fromno, tono) values (?, ?, ?)", + "insert or ignore into arc (file_id, context_id, fromno, tono) values (?, ?, ?, ?)", data, ) From 5aa95d1edec75c4f30458773894c7f47c1af0edc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 9 Sep 2018 10:58:38 -0400 Subject: [PATCH 240/952] Plumb through context= setting --- coverage/cmdline.py | 7 +++++++ coverage/config.py | 2 ++ coverage/control.py | 6 ++++-- tests/test_cmdline.py | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 23d2aec3e..99b155b2c 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -43,6 +43,10 @@ class Opts(object): "Valid values are: %s." ) % ", ".join(CONCURRENCY_CHOICES), ) + context = optparse.make_option( + '', '--context', action='store', metavar="LABEL", + help="The context label to record for this coverage run", + ) debug = optparse.make_option( '', '--debug', action='store', metavar="OPTS", help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", @@ -160,6 +164,7 @@ def __init__(self, *args, **kwargs): append=None, branch=None, concurrency=None, + context=None, debug=None, directory=None, fail_under=None, @@ -358,6 +363,7 @@ def get_prog_name(self): Opts.append, Opts.branch, Opts.concurrency, + Opts.context, Opts.include, Opts.module, Opts.omit, @@ -482,6 +488,7 @@ def command_line(self, argv): debug=debug, concurrency=options.concurrency, check_preimported=True, + context=options.context, ) if options.action == "debug": diff --git a/coverage/config.py b/coverage/config.py index 69c929b4c..9a11323d8 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -175,6 +175,7 @@ def __init__(self): # Defaults for [run] self.branch = False self.concurrency = None + self.context = None self.cover_pylib = False self.data_file = ".coverage" self.debug = [] @@ -318,6 +319,7 @@ def from_file(self, filename, our_file): # [run] ('branch', 'run:branch', 'boolean'), ('concurrency', 'run:concurrency', 'list'), + ('context', 'run:context'), ('cover_pylib', 'run:cover_pylib', 'boolean'), ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), diff --git a/coverage/control.py b/coverage/control.py index 5d42af777..cdbd721fe 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -57,7 +57,7 @@ def __init__( self, data_file=None, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, source=None, omit=None, include=None, debug=None, - concurrency=None, check_preimported=False, + concurrency=None, check_preimported=False, context=None, ): """ `data_file` is the base name of the data file to use, defaulting to @@ -116,6 +116,8 @@ def __init__( by coverage. Importing measured files before coverage is started can mean that code is missed. + `context` is a string to use as the context label for collected data. + .. versionadded:: 4.0 The `concurrency` parameter. @@ -133,7 +135,7 @@ def __init__( branch=branch, parallel=bool_or_none(data_suffix), source=source, run_omit=omit, run_include=include, debug=debug, report_omit=omit, report_include=include, - concurrency=concurrency, + concurrency=concurrency, context=context, ) # This is injectable by tests. diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index a4d018806..92867a540 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -33,7 +33,7 @@ class BaseCmdLineTest(CoverageTest): 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, check_preimported=True, + concurrency=None, check_preimported=True, context=None, ) defaults.annotate( directory=None, ignore_errors=None, include=None, omit=None, morfs=[], From 3839a376d03f22fad41b869be680ba496147b281 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 9 Sep 2018 13:19:40 -0400 Subject: [PATCH 241/952] Collector has a CoverageData --- coverage/collector.py | 18 +++++++++++++----- coverage/control.py | 6 +++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/coverage/collector.py b/coverage/collector.py index fa3eaaa49..db8373a19 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -99,6 +99,7 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency self.warn = warn self.branch = branch self.threading = None + self.covdata = None self.origin = short_stack() @@ -160,6 +161,10 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency def __repr__(self): return "" % (id(self), self.tracer_name()) + def use_data(self, covdata): + """Use `covdata` for recording data.""" + self.covdata = covdata + def tracer_name(self): """Return the class name of the tracer we're using.""" return self._trace_class.__name__ @@ -378,8 +383,11 @@ def cached_abs_file(self, filename): except KeyError: return self.abs_file_cache.setdefault(key, abs_file(filename)) - def save_data(self, covdata): - """Save the collected data to a `CoverageData`. + def flush_data(self): + """Save the collected data to our associated `CoverageData`. + + Data may have also been saved along the way. This forces the + last of the data to be saved. Returns True if there was data to save, False if not. """ @@ -406,10 +414,10 @@ def abs_file_dict(d): return dict((self.cached_abs_file(k), v) for k, v in items) if self.branch: - covdata.add_arcs(abs_file_dict(self.data)) + self.covdata.add_arcs(abs_file_dict(self.data)) else: - covdata.add_lines(abs_file_dict(self.data)) - covdata.add_file_tracers(abs_file_dict(self.file_tracers)) + self.covdata.add_lines(abs_file_dict(self.data)) + self.covdata.add_file_tracers(abs_file_dict(self.file_tracers)) if self.wtw: # Just a hack, so just hack it. diff --git a/coverage/control.py b/coverage/control.py index cdbd721fe..ca0843d73 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -335,6 +335,7 @@ def load(self): def _init_for_start(self): """Initialization for start()""" + # Construct the collector. concurrency = self.config.concurrency or [] if "multiprocessing" in concurrency: if not patch_multiprocessing: @@ -400,6 +401,9 @@ def _init_data(self, suffix): debug=self._debug, ) + if self._collector is not None: + self._collector.use_data(self._data) + def start(self): """Start measuring code coverage. @@ -564,7 +568,7 @@ def get_data(self): self._init_data(suffix=None) self._post_init() - if self._collector and self._collector.save_data(self._data): + if self._collector and self._collector.flush_data(): self._post_save_work() return self._data From d5d3427c92b78324beaba4babb281ac96eb1ebc1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 10 Sep 2018 15:19:21 -0400 Subject: [PATCH 242/952] SqlData can set_context --- coverage/sqldata.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 0c05ae1f2..641e8ae1b 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -178,6 +178,20 @@ def _file_id(self, filename, add=False): self._file_map[filename] = cur.lastrowid return self._file_map.get(filename) + def set_context(self, context): + """Get the context id for `context`.""" + self._start_using() + if not context: + self._context_id = 0 + else: + with self._connect() as con: + row = con.execute("select id from context where context = ?", (context,)).fetchone() + if row is not None: + self._context_id = row[0] + else: + cur = con.execute("insert into context (context) values (?)", (context,)) + self._context_id = cur.lastrowid + def add_lines(self, line_data): """Add measured line data. From b4b9c0b458325c98a3b3a3fcb6b3daa5bb63f4ec Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 19 Sep 2018 18:05:44 -0400 Subject: [PATCH 243/952] Clarify an error message --- coverage/cmdline.py | 2 +- tests/test_cmdline.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index d7dfa68cd..8f6b0a90c 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -614,7 +614,7 @@ def do_run(self, options, args): # they will be None if they have not been specified. if getattr(options, opt_name) is not None: self.help_fn( - "Options affecting multiprocessing must be specified " + "Options affecting multiprocessing must only be specified " "in a configuration file." ) return ERR diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index a4d018806..59c76c738 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -470,7 +470,7 @@ def test_multiprocessing_needs_config_file(self): # config file. self.command_line("run --concurrency=multiprocessing --branch foo.py", ret=ERR) self.assertIn( - "Options affecting multiprocessing must be specified in a configuration file.", + "Options affecting multiprocessing must only be specified in a configuration file.", self.stderr() ) From 21a9e672336675aa4ad55b3fb946661b6e5616f2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Sep 2018 08:44:56 -0400 Subject: [PATCH 244/952] Turbogears is not worth a mention --- doc/cmd.rst | 3 +-- doc/trouble.rst | 15 --------------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/doc/cmd.rst b/doc/cmd.rst index 908b2ee98..c0b912216 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -124,8 +124,7 @@ code as well as your own, add the ``-L`` (or ``--pylib``) flag. If your coverage results seem to be overlooking code that you know has been executed, try running coverage.py again with the ``--timid`` flag. This uses a -simpler but slower trace method. Projects that use DecoratorTools, including -TurboGears, will need to use ``--timid`` to get correct results. +simpler but slower trace method, and might be needed in rare cases. If you are measuring coverage in a multi-process program, or across a number of machines, you'll want the ``--parallel-mode`` switch to keep the data separate diff --git a/doc/trouble.rst b/doc/trouble.rst index 8f260604e..16483bb2b 100644 --- a/doc/trouble.rst +++ b/doc/trouble.rst @@ -53,21 +53,6 @@ coverage.py from working properly: .. _issue 43: https://bitbucket.org/ned/coveragepy/issues/43/coverage-measurement-fails-on-code -Things that require --timid ---------------------------- - -Some packages interfere with coverage measurement, but you might be able to -make it work by using the ``--timid`` command-line switch, or the ``[run] -timid=True`` configuration option. - -* `DecoratorTools`_, or any package which uses it, notably `TurboGears`_. - DecoratorTools fiddles with the trace function. You will need to use - ``--timid``. - -.. _DecoratorTools: https://pypi.org/project/DecoratorTools/ -.. _TurboGears: http://turbogears.org/ - - Still having trouble? --------------------- From 85f63fef4d3b8e1b1328e3268c3eee3900e1d8a4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Sep 2018 08:47:36 -0400 Subject: [PATCH 245/952] Put these paragraphs in a more reasonable order --- doc/cmd.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/cmd.rst b/doc/cmd.rst index c0b912216..00a9a6c3e 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -118,6 +118,10 @@ configuration file for all options. .. _gevent: http://www.gevent.org/ .. _eventlet: http://eventlet.net/ +If you are measuring coverage in a multi-process program, or across a number of +machines, you'll want the ``--parallel-mode`` switch to keep the data separate +during measurement. See :ref:`cmd_combining` below. + By default, coverage.py does not measure code installed with the Python interpreter, for example, the standard library. If you want to measure that code as well as your own, add the ``-L`` (or ``--pylib``) flag. @@ -126,10 +130,6 @@ If your coverage results seem to be overlooking code that you know has been executed, try running coverage.py again with the ``--timid`` flag. This uses a simpler but slower trace method, and might be needed in rare cases. -If you are measuring coverage in a multi-process program, or across a number of -machines, you'll want the ``--parallel-mode`` switch to keep the data separate -during measurement. See :ref:`cmd_combining` below. - .. _cmd_warnings: From 8c05c99a4782e67c75e361f3fcb06ae32559e5f1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 12 Sep 2018 08:39:37 -0400 Subject: [PATCH 246/952] Set the context in the data --- coverage/collector.py | 6 +++++- coverage/control.py | 5 ++--- coverage/sqldata.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/coverage/collector.py b/coverage/collector.py index db8373a19..e01449798 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -101,6 +101,8 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency self.threading = None self.covdata = None + self.static_context = None + self.origin = short_stack() self.concur_id_func = None @@ -161,9 +163,11 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency def __repr__(self): return "" % (id(self), self.tracer_name()) - def use_data(self, covdata): + def use_data(self, covdata, context): """Use `covdata` for recording data.""" self.covdata = covdata + self.static_context = context + self.covdata.set_context(self.static_context) def tracer_name(self): """Return the class name of the tracer we're using.""" diff --git a/coverage/control.py b/coverage/control.py index ca0843d73..0918a34ea 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -366,6 +366,8 @@ def _init_for_start(self): self._init_data(suffix) + self._collector.use_data(self._data, self.config.context) + # Early warning if we aren't going to be able to support plugins. if self._plugins.file_tracers and not self._collector.supports_plugins: self._warn( @@ -401,9 +403,6 @@ def _init_data(self, suffix): debug=self._debug, ) - if self._collector is not None: - self._collector.use_data(self._data) - def start(self): """Start measuring code coverage. diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 641e8ae1b..6dde9c2e8 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -111,7 +111,7 @@ def _create_db(self): self._db = Sqlite(self.filename, self._debug) with self._db: for stmt in SCHEMA.split(';'): - stmt = stmt.strip() + stmt = " ".join(stmt.strip().split()) if stmt: self._db.execute(stmt) self._db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) From e1d0020d6336d5ad4cdac7c177d38d11ba1ac91d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 13 Sep 2018 09:06:15 -0400 Subject: [PATCH 247/952] Change these tests to not use weird attribute stuffing --- tests/test_summary.py | 58 +++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/test_summary.py b/tests/test_summary.py index 936df7fa5..c3b572d29 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -750,24 +750,31 @@ def test_xml(self): class TestSummaryReporterConfiguration(CoverageTest): """Tests of SummaryReporter.""" - LINES_1 = { - os.path.join(TESTS_DIR, "test_api.py"): dict.fromkeys(range(400)), - os.path.join(TESTS_DIR, "test_backward.py"): dict.fromkeys(range(20)), - os.path.join(TESTS_DIR, "test_coverage.py"): dict.fromkeys(range(15)), - } - - def get_coverage_data(self, lines): - """Get a CoverageData object that includes the requested lines.""" - data = CoverageData() - data.add_lines(lines) - return data - - def get_summary_text(self, coverage_data, options): + def make_rigged_file(self, filename, stmts, miss): + """Create a file that will have specific results. + + `stmts` and `miss` are ints, the number of statements, and + missed statements that should result. + """ + run = stmts - miss - 1 + dont_run = miss + source = "" + source += "a = 1\n" * run + source += "if a == 99:\n" + source += " a = 2\n" * dont_run + self.make_file(filename, source) + + def get_summary_text(self, options): """Get text output from the SummaryReporter.""" - cov = Coverage() + self.make_rigged_file("file1.py", 339, 155) + self.make_rigged_file("file2.py", 13, 3) + self.make_rigged_file("file3.py", 234, 228) + self.make_file("doit.py", "import file1, file2, file3") + + cov = Coverage(source=["."], omit=["doit.py"]) cov.start() + import doit # pragma: nested # pylint: disable=import-error, unused-variable cov.stop() # pragma: nested - cov._data = coverage_data printer = SummaryReporter(cov, options) destination = StringIO() printer.report([], destination) @@ -777,8 +784,7 @@ def test_test_data(self): # We use our own test files as test data. Check that our assumptions # about them are still valid. We want the three columns of numbers to # sort in three different orders. - data = self.get_coverage_data(self.LINES_1) - report = self.get_summary_text(data, CoverageConfig()) + report = self.get_summary_text(CoverageConfig()) print(report) # Name Stmts Miss Cover # -------------------------------------------- @@ -802,18 +808,16 @@ def test_test_data(self): def test_defaults(self): """Run the report with no configuration options.""" - data = self.get_coverage_data(self.LINES_1) opts = CoverageConfig() - report = self.get_summary_text(data, opts) + report = self.get_summary_text(opts) self.assertNotIn('Missing', report) self.assertNotIn('Branch', report) def test_print_missing(self): """Run the report printing the missing lines.""" - data = self.get_coverage_data(self.LINES_1) opts = CoverageConfig() opts.from_args(show_missing=True) - report = self.get_summary_text(data, opts) + report = self.get_summary_text(opts) self.assertIn('Missing', report) self.assertNotIn('Branch', report) @@ -827,33 +831,29 @@ def assert_ordering(self, text, *words): def test_sort_report_by_stmts(self): # Sort the text report by the Stmts column. - data = self.get_coverage_data(self.LINES_1) opts = CoverageConfig() opts.from_args(sort='Stmts') - report = self.get_summary_text(data, opts) + report = self.get_summary_text(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) + report = self.get_summary_text(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) opts = CoverageConfig() opts.from_args(sort='Cover') - report = self.get_summary_text(data, opts) + report = self.get_summary_text(opts) self.assert_ordering(report, "test_coverage.py", "test_api.py", "test_backward.py") def test_sort_report_by_invalid_option(self): # Sort the text report by a nonsense column. - data = self.get_coverage_data(self.LINES_1) opts = CoverageConfig() opts.from_args(sort='Xyzzy') msg = "Invalid sorting option: 'Xyzzy'" with self.assertRaisesRegex(CoverageException, msg): - self.get_summary_text(data, opts) + self.get_summary_text(opts) From 2f1b8cfcfe184a8fd6f3f2f789530bddb233dda8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 14 Sep 2018 07:05:51 -0400 Subject: [PATCH 248/952] Change measured_files to a set --- coverage/control.py | 10 +++------- coverage/data.py | 4 ++-- coverage/sqldata.py | 6 +++--- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 0918a34ea..f7d97cf61 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -683,15 +683,11 @@ def _get_file_reporters(self, morfs=None): if not morfs: morfs = self._data.measured_files() - # Be sure we have a list. - if not isinstance(morfs, (list, tuple)): + # Be sure we have a collection. + if not isinstance(morfs, (list, tuple, set)): morfs = [morfs] - file_reporters = [] - for morf in morfs: - file_reporter = self._get_file_reporter(morf) - file_reporters.append(file_reporter) - + file_reporters = [self._get_file_reporter(morf) for morf in morfs] return file_reporters def report( diff --git a/coverage/data.py b/coverage/data.py index f03e90caa..3250196df 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -252,8 +252,8 @@ def run_infos(self): return self._runs def measured_files(self): - """A list of all files that had been measured.""" - return list(self._arcs or self._lines or {}) + """A set of all files that had been measured.""" + return set(self._arcs or self._lines or {}) def __nonzero__(self): return bool(self._lines or self._arcs) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 6dde9c2e8..b9488557d 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -304,7 +304,7 @@ def update(self, other_data, aliases=None): aliases = aliases or PathAliases() # See what we had already measured, for accurate conflict reporting. - this_measured = set(self.measured_files()) + this_measured = self.measured_files() # lines if other_data._has_lines: @@ -381,8 +381,8 @@ def has_arcs(self): return bool(self._has_arcs) def measured_files(self): - """A list of all files that had been measured.""" - return list(self._file_map) + """A set of all files that had been measured.""" + return set(self._file_map) def file_tracer(self, filename): """Get the plugin name of the file tracer for a file. From d2f77ab2ffc308e616af0207546ee1bef1cb8c75 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 15 Sep 2018 08:07:26 -0400 Subject: [PATCH 249/952] measured_contexts() and two simple tests of the global context --- coverage/data.py | 5 +++++ coverage/sqldata.py | 27 ++++++++++++++++----------- tests/test_context.py | 30 ++++++++++++++++++++++++++++++ tests/test_debug.py | 4 ++-- 4 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 tests/test_context.py diff --git a/coverage/data.py b/coverage/data.py index 3250196df..3a2432b3c 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -445,6 +445,11 @@ def touch_file(self, filename, plugin_name=""): self._validate() + def set_context(self, context): + """Set the context. Not implemented for JSON storage.""" + if context: + raise CoverageException("JSON storage doesn't support contexts") + def write(self): """Write the collected coverage data to a file. diff --git a/coverage/sqldata.py b/coverage/sqldata.py index b9488557d..45c1570c4 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -179,18 +179,16 @@ def _file_id(self, filename, add=False): return self._file_map.get(filename) def set_context(self, context): - """Get the context id for `context`.""" + """Set the current context for future `add_lines` etc.""" self._start_using() - if not context: - self._context_id = 0 - else: - with self._connect() as con: - row = con.execute("select id from context where context = ?", (context,)).fetchone() - if row is not None: - self._context_id = row[0] - else: - cur = con.execute("insert into context (context) values (?)", (context,)) - self._context_id = cur.lastrowid + context = context or "" + with self._connect() as con: + row = con.execute("select id from context where context = ?", (context,)).fetchone() + if row is not None: + self._context_id = row[0] + else: + cur = con.execute("insert into context (context) values (?)", (context,)) + self._context_id = cur.lastrowid def add_lines(self, line_data): """Add measured line data. @@ -384,6 +382,13 @@ def measured_files(self): """A set of all files that had been measured.""" return set(self._file_map) + def measured_contexts(self): + """A set of all contexts that have been measured.""" + self._start_using() + with self._connect() as con: + contexts = set(row[0] for row in con.execute("select distinct(context) from context")) + return contexts + def file_tracer(self, filename): """Get the plugin name of the file tracer for a file. diff --git a/tests/test_context.py b/tests/test_context.py new file mode 100644 index 000000000..ec1e48853 --- /dev/null +++ b/tests/test_context.py @@ -0,0 +1,30 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Tests for context support.""" + +import coverage + +from tests.coveragetest import CoverageTest + + +class GlobalContextTest(CoverageTest): + """Tests of the global context.""" + + def setUp(self): + super(GlobalContextTest, self).setUp() + self.skip_unless_data_storage_is("sql") + + def test_no_context(self): + self.make_file("main.py", "a = 1") + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + data = cov.get_data() + self.assertCountEqual(data.measured_contexts(), [""]) + + def test_global_context(self): + self.make_file("main.py", "a = 1") + cov = coverage.Coverage(context="gooey") + self.start_import_stop(cov, "main") + data = cov.get_data() + self.assertCountEqual(data.measured_contexts(), ["gooey"]) diff --git a/tests/test_debug.py b/tests/test_debug.py index 284d9567d..63edc84ff 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -144,8 +144,8 @@ def test_debug_callers(self): self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") else: - self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Creating data file") - self.assertRegex(last_line, r"\s+_create_db : .*coverage[/\\]sqldata.py @\d+$") + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Adding lines") + self.assertRegex(last_line, r"\s+add_lines : .*coverage[/\\]sqldata.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) From 5ff763737475f8fa1a587f6903de1329b41090ae Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 18 Sep 2018 07:49:54 -0400 Subject: [PATCH 250/952] Combining contexts works --- coverage/sqldata.py | 74 ++++++++++++++++++++++++++++++------------- tests/test_context.py | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 22 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 45c1570c4..224573bec 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -12,6 +12,7 @@ # TODO: run_info import glob +import itertools import os import sqlite3 @@ -90,7 +91,7 @@ def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._has_lines = False self._has_arcs = False - self._context_id = 0 + self._current_context_id = None def _choose_filename(self): self.filename = self._basename @@ -104,6 +105,7 @@ def _reset(self): self._db = None self._file_map = {} self._have_used = False + self._current_context_id = None def _create_db(self): if self._debug and self._debug.should('dataio'): @@ -178,6 +180,17 @@ def _file_id(self, filename, add=False): self._file_map[filename] = cur.lastrowid return self._file_map.get(filename) + def _context_id(self, context): + """Get the id for a context.""" + assert context is not None + self._start_using() + with self._connect() as con: + row = con.execute("select id from context where context = ?", (context,)).fetchone() + if row is not None: + return row[0] + else: + return None + def set_context(self, context): """Set the current context for future `add_lines` etc.""" self._start_using() @@ -185,10 +198,10 @@ def set_context(self, context): with self._connect() as con: row = con.execute("select id from context where context = ?", (context,)).fetchone() if row is not None: - self._context_id = row[0] + self._current_context_id = row[0] else: cur = con.execute("insert into context (context) values (?)", (context,)) - self._context_id = cur.lastrowid + self._current_context_id = cur.lastrowid def add_lines(self, line_data): """Add measured line data. @@ -204,10 +217,12 @@ def add_lines(self, line_data): )) self._start_using() self._choose_lines_or_arcs(lines=True) + if self._current_context_id is None: + self.set_context("") with self._connect() as con: for filename, linenos in iitems(line_data): file_id = self._file_id(filename, add=True) - data = [(file_id, self._context_id, lineno) for lineno in linenos] + data = [(file_id, self._current_context_id, lineno) for lineno in linenos] con.executemany( "insert or ignore into line (file_id, context_id, lineno) values (?, ?, ?)", data, @@ -227,10 +242,12 @@ def add_arcs(self, arc_data): )) self._start_using() self._choose_lines_or_arcs(arcs=True) + if self._current_context_id is None: + self.set_context("") with self._connect() as con: for filename, arcs in iitems(arc_data): file_id = self._file_id(filename, add=True) - data = [(file_id, self._context_id, fromno, tono) for fromno, tono in arcs] + data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] con.executemany( "insert or ignore into arc (file_id, context_id, fromno, tono) values (?, ?, ?, ?)", data, @@ -306,19 +323,23 @@ def update(self, other_data, aliases=None): # lines if other_data._has_lines: - for filename in other_data.measured_files(): - lines = set(other_data.lines(filename)) - filename = aliases.map(filename) - lines.update(self.lines(filename) or ()) - self.add_lines({filename: lines}) + for context in other_data.measured_contexts(): + self.set_context(context) + for filename in other_data.measured_files(): + lines = set(other_data.lines(filename, context=context)) + filename = aliases.map(filename) + lines.update(self.lines(filename, context=context) or ()) + self.add_lines({filename: lines}) # arcs if other_data._has_arcs: - for filename in other_data.measured_files(): - arcs = set(other_data.arcs(filename)) - filename = aliases.map(filename) - arcs.update(self.arcs(filename) or ()) - self.add_arcs({filename: arcs}) + for context in other_data.measured_contexts(): + self.set_context(context) + for filename in other_data.measured_files(): + arcs = set(other_data.arcs(filename, context=context)) + filename = aliases.map(filename) + arcs.update(self.arcs(filename, context=context) or ()) + self.add_arcs({filename: arcs}) # file_tracers for filename in other_data.measured_files(): @@ -407,12 +428,11 @@ def file_tracer(self, filename): return row[0] or "" return "" # File was measured, but no tracer associated. - def lines(self, filename): + def lines(self, filename, context=None): self._start_using() if self.has_arcs(): - arcs = self.arcs(filename) + arcs = self.arcs(filename, context=context) if arcs is not None: - import itertools all_lines = itertools.chain.from_iterable(arcs) return list(set(l for l in all_lines if l > 0)) @@ -421,18 +441,28 @@ def lines(self, filename): if file_id is None: return None else: - linenos = con.execute("select lineno from line where file_id = ?", (file_id,)) + query = "select lineno from line where file_id = ?" + data = [file_id] + if context is not None: + query += " and context_id = ?" + data += [self._context_id(context)] + linenos = con.execute(query, data) return [lineno for lineno, in linenos] - def arcs(self, filename): + def arcs(self, filename, context=None): self._start_using() with self._connect() as con: file_id = self._file_id(filename) if file_id is None: return None else: - arcs = con.execute("select fromno, tono from arc where file_id = ?", (file_id,)) - return [pair for pair in arcs] + query = "select fromno, tono from arc where file_id = ?" + data = [file_id] + if context is not None: + query += " and context_id = ?" + data += [self._context_id(context)] + arcs = con.execute(query, data) + return list(arcs) def run_infos(self): return [] # TODO diff --git a/tests/test_context.py b/tests/test_context.py index ec1e48853..e20eb9bb6 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -3,7 +3,10 @@ """Tests for context support.""" +import os.path + import coverage +from coverage.data import CoverageData, combine_parallel_data from tests.coveragetest import CoverageTest @@ -28,3 +31,59 @@ def test_global_context(self): self.start_import_stop(cov, "main") data = cov.get_data() self.assertCountEqual(data.measured_contexts(), ["gooey"]) + + def run_red_blue(self, **options): + self.make_file("red.py", """\ + a = 1 + if a > 2: + a = 3 + assert a == 1 + """) + red_cov = coverage.Coverage(context="red", data_suffix="r", source=["."], **options) + self.start_import_stop(red_cov, "red") + red_cov.save() + + self.make_file("blue.py", """\ + b = 1 + if b > 2: + b = 3 + assert b == 1 + """) + blue_cov = coverage.Coverage(context="blue", data_suffix="b", source=["."], **options) + self.start_import_stop(blue_cov, "blue") + blue_cov.save() + + def test_combining_line_contexts(self): + self.run_red_blue() + combined = CoverageData() + combine_parallel_data(combined) + + self.assertEqual(combined.measured_contexts(), {'red', 'blue'}) + + full_names = {os.path.basename(f): f for f in combined.measured_files()} + self.assertCountEqual(full_names, ['red.py', 'blue.py']) + + self.assertEqual(combined.lines(full_names['red.py'], context='red'), [1, 2, 4]) + self.assertEqual(combined.lines(full_names['red.py'], context='blue'), []) + self.assertEqual(combined.lines(full_names['blue.py'], context='red'), []) + self.assertEqual(combined.lines(full_names['blue.py'], context='blue'), [1, 2, 4]) + + def test_combining_arc_contexts(self): + self.run_red_blue(branch=True) + combined = CoverageData() + combine_parallel_data(combined) + + self.assertEqual(combined.measured_contexts(), {'red', 'blue'}) + + full_names = {os.path.basename(f): f for f in combined.measured_files()} + self.assertCountEqual(full_names, ['red.py', 'blue.py']) + + self.assertEqual(combined.lines(full_names['red.py'], context='red'), [1, 2, 4]) + self.assertEqual(combined.lines(full_names['red.py'], context='blue'), []) + self.assertEqual(combined.lines(full_names['blue.py'], context='red'), []) + self.assertEqual(combined.lines(full_names['blue.py'], context='blue'), [1, 2, 4]) + + self.assertEqual(combined.arcs(full_names['red.py'], context='red'), [(-1, 1), (1, 2), (2, 4), (4, -1)]) + self.assertEqual(combined.arcs(full_names['red.py'], context='blue'), []) + self.assertEqual(combined.arcs(full_names['blue.py'], context='red'), []) + self.assertEqual(combined.arcs(full_names['blue.py'], context='blue'), [(-1, 1), (1, 2), (2, 4), (4, -1)]) From edc25b9a723272f869c598e929d72e5db341ba0d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Sep 2018 19:45:17 -0400 Subject: [PATCH 251/952] More debugging. --- coverage/data.py | 2 ++ coverage/sqldata.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/coverage/data.py b/coverage/data.py index 3a2432b3c..e6d56d84a 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -727,6 +727,8 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): files_combined = 0 for f in files_to_combine: + if data._debug and data._debug.should('dataio'): + data._debug.write("Combining data file %r" % (f,)) try: new_data = CoverageData(f, debug=data._debug) new_data.read() diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 224573bec..e644ec164 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -167,6 +167,12 @@ def __nonzero__(self): __bool__ = __nonzero__ + def dump(self): # pragma: debugging + """Write a dump of the database.""" + if self._debug: + with self._connect() as con: + self._debug.write(con.dump()) + def _file_id(self, filename, add=False): """Get the file id for `filename`. @@ -519,3 +525,7 @@ def executemany(self, sql, data): if self.debug: self.debug.write("Executing many {!r} with {} rows".format(sql, len(data))) return self.con.executemany(sql, data) + + def dump(self): # pragma: debugging + """Return a multi-line string, the dump of the database.""" + return "\n".join(self.con.iterdump()) From 2ae3ff2f6d4845c5baa6af244d96e397c235ee01 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 20 Sep 2018 19:52:50 -0400 Subject: [PATCH 252/952] Make test check commutivity of .update() --- tests/test_context.py | 89 +++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 37 deletions(-) diff --git a/tests/test_context.py b/tests/test_context.py index e20eb9bb6..a6be922db 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -6,7 +6,7 @@ import os.path import coverage -from coverage.data import CoverageData, combine_parallel_data +from coverage.data import CoverageData from tests.coveragetest import CoverageTest @@ -32,58 +32,73 @@ def test_global_context(self): data = cov.get_data() self.assertCountEqual(data.measured_contexts(), ["gooey"]) + SOURCE = """\ + a = 1 + if a > 2: + a = 3 + assert a == 1 + """ + + LINES = [1, 2, 4] + ARCS = [(-1, 1), (1, 2), (2, 4), (4, -1)] + def run_red_blue(self, **options): - self.make_file("red.py", """\ - a = 1 - if a > 2: - a = 3 - assert a == 1 - """) + """Run red.py and blue.py, and return their CoverageData objects.""" + self.make_file("red.py", self.SOURCE) red_cov = coverage.Coverage(context="red", data_suffix="r", source=["."], **options) self.start_import_stop(red_cov, "red") red_cov.save() + red_data = red_cov.get_data() - self.make_file("blue.py", """\ - b = 1 - if b > 2: - b = 3 - assert b == 1 - """) + self.make_file("blue.py", self.SOURCE) blue_cov = coverage.Coverage(context="blue", data_suffix="b", source=["."], **options) self.start_import_stop(blue_cov, "blue") blue_cov.save() + blue_data = blue_cov.get_data() + + return red_data, blue_data def test_combining_line_contexts(self): - self.run_red_blue() - combined = CoverageData() - combine_parallel_data(combined) + red_data, blue_data = self.run_red_blue() + for datas in [[red_data, blue_data], [blue_data, red_data]]: + combined = CoverageData(suffix="combined") + for data in datas: + combined.update(data) - self.assertEqual(combined.measured_contexts(), {'red', 'blue'}) + self.assertEqual(combined.measured_contexts(), {'red', 'blue'}) - full_names = {os.path.basename(f): f for f in combined.measured_files()} - self.assertCountEqual(full_names, ['red.py', 'blue.py']) + full_names = {os.path.basename(f): f for f in combined.measured_files()} + self.assertCountEqual(full_names, ['red.py', 'blue.py']) - self.assertEqual(combined.lines(full_names['red.py'], context='red'), [1, 2, 4]) - self.assertEqual(combined.lines(full_names['red.py'], context='blue'), []) - self.assertEqual(combined.lines(full_names['blue.py'], context='red'), []) - self.assertEqual(combined.lines(full_names['blue.py'], context='blue'), [1, 2, 4]) + fred = full_names['red.py'] + fblue = full_names['blue.py'] + + self.assertEqual(combined.lines(fred, context='red'), self.LINES) + self.assertEqual(combined.lines(fred, context='blue'), []) + self.assertEqual(combined.lines(fblue, context='red'), []) + self.assertEqual(combined.lines(fblue, context='blue'), self.LINES) def test_combining_arc_contexts(self): - self.run_red_blue(branch=True) - combined = CoverageData() - combine_parallel_data(combined) + red_data, blue_data = self.run_red_blue(branch=True) + for datas in [[red_data, blue_data], [blue_data, red_data]]: + combined = CoverageData(suffix="combined") + for data in datas: + combined.update(data) + + self.assertEqual(combined.measured_contexts(), {'red', 'blue'}) - self.assertEqual(combined.measured_contexts(), {'red', 'blue'}) + full_names = {os.path.basename(f): f for f in combined.measured_files()} + self.assertCountEqual(full_names, ['red.py', 'blue.py']) - full_names = {os.path.basename(f): f for f in combined.measured_files()} - self.assertCountEqual(full_names, ['red.py', 'blue.py']) + fred = full_names['red.py'] + fblue = full_names['blue.py'] - self.assertEqual(combined.lines(full_names['red.py'], context='red'), [1, 2, 4]) - self.assertEqual(combined.lines(full_names['red.py'], context='blue'), []) - self.assertEqual(combined.lines(full_names['blue.py'], context='red'), []) - self.assertEqual(combined.lines(full_names['blue.py'], context='blue'), [1, 2, 4]) + self.assertEqual(combined.lines(fred, context='red'), self.LINES) + self.assertEqual(combined.lines(fred, context='blue'), []) + self.assertEqual(combined.lines(fblue, context='red'), []) + self.assertEqual(combined.lines(fblue, context='blue'), self.LINES) - self.assertEqual(combined.arcs(full_names['red.py'], context='red'), [(-1, 1), (1, 2), (2, 4), (4, -1)]) - self.assertEqual(combined.arcs(full_names['red.py'], context='blue'), []) - self.assertEqual(combined.arcs(full_names['blue.py'], context='red'), []) - self.assertEqual(combined.arcs(full_names['blue.py'], context='blue'), [(-1, 1), (1, 2), (2, 4), (4, -1)]) + self.assertEqual(combined.arcs(fred, context='red'), self.ARCS) + self.assertEqual(combined.arcs(fred, context='blue'), []) + self.assertEqual(combined.arcs(fblue, context='red'), []) + self.assertEqual(combined.arcs(fblue, context='blue'), self.ARCS) From b0f5ac245fbd8afc6b9d2ac84f732c419a712f0a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 21 Sep 2018 17:52:33 -0400 Subject: [PATCH 253/952] Documentation for static contexts --- CHANGES.rst | 5 +++++ README.rst | 6 ++++-- doc/cmd.rst | 6 +++++- doc/config.rst | 5 +++++ doc/contexts.rst | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ doc/index.rst | 1 + 6 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 doc/contexts.rst diff --git a/CHANGES.rst b/CHANGES.rst index 8f8cd32b0..089fec7f4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,11 @@ Change history for Coverage.py Unreleased ---------- +- Context support: static contexts let you specify a label for a coverage run, + which is recorded in the data, and retained when you combine files. See + :ref:`contexts` for more information. Currently, only static contexts are + supported, with no reporting features. + - Environment variable substitution in configuration files now supports two syntaxes for controlling the behavior of undefined variables: if ``VARNAME`` is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default diff --git a/README.rst b/README.rst index b1b2e0ee9..3ad446c51 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,8 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: -* CPython 2.7 and 3.4 through 3.7. +* CPython 2.7. +* CPython 3.4 through 3.7. * PyPy2 6.0 and PyPy3 6.0. * Jython 2.7.1, though not for reporting. * IronPython 2.7.7, though not for reporting. @@ -31,7 +32,8 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on .. _GitHub: https://github.com/nedbat/coveragepy -**New in 5.0:** SQLite data storage, dropped support for Python 2.6 and 3.3. +**New in 5.0:** SQLite data storage, contexts, dropped support for Python 2.6 +and 3.3. New in 4.5: Configurator plug-ins. diff --git a/doc/cmd.rst b/doc/cmd.rst index 908b2ee98..0d1a05b50 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -69,7 +69,7 @@ control, and can provide options that other invocation techniques (like test runner plugins) may not offer. See :ref:`config` for more details. -.. _cmd_execution: +.. _cmd_run: Execution --------- @@ -118,6 +118,10 @@ configuration file for all options. .. _gevent: http://www.gevent.org/ .. _eventlet: http://eventlet.net/ +You can specify a :ref:`static context ` for a coverage run with +``--context``. This can be any label you want, and will be recorded with the +data. See :ref:`contexts` for more information. + By default, coverage.py does not measure code installed with the Python interpreter, for example, the standard library. If you want to measure that code as well as your own, add the ``-L`` (or ``--pylib``) flag. diff --git a/doc/config.rst b/doc/config.rst index 666a13213..b8117a43c 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -132,6 +132,11 @@ Before version 4.2, this option only accepted a single string. .. versionadded:: 4.0 +``context`` (string): the static context to record for this coverage run. See +:ref:`contexts` for more information + +.. versionadded:: 5.0 + ``data_file`` (string, default ".coverage"): the name of the data file to use for storing or reporting coverage. This value can include a path to another directory. diff --git a/doc/contexts.rst b/doc/contexts.rst new file mode 100644 index 000000000..c1d4a173a --- /dev/null +++ b/doc/contexts.rst @@ -0,0 +1,51 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +.. _contexts: + +==================== +Measurement Contexts +==================== + +.. :history: 20180921T085400, new for 5.0 + +.. versionadded:: 5.0 + +Coverage.py measures whether code was run, but it can also record the context +in which it was run. This can provide more information to help you understand +the behavior of your tests. + +There are two kinds of context: static and dynamic. Static contexts are fixed +for an entire run, and are set explicitly with an option. + +Dynamic contexts are coming soon. + + +Static contexts +--------------- + +A static context is set by an option when you run coverage.py. The value is +fixed for the duration of a run. They can be any text you like, for example, +"python3" or "with_numpy". The context is recorded with the data. + +When you :ref:`combine multiple data files ` together, they can +have differing contexts. All of the information is retained, so that the +different contexts are correctly recorded in the combined file. + +A static context is specified with the ``--context=CONTEXT`` option to +:ref:`coverage run `. + + +Dynamic contexts +---------------- + +Not implemented yet. + + +Context reporting +----------------- + +There is currently no support for using contexts during reporting. I'm +interested to `hear your ideas`__ for what would be useful. + +__ https://nedbatchelder.com/site/aboutned.html diff --git a/doc/index.rst b/doc/index.rst index 0a7950114..149eba07a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -206,6 +206,7 @@ More information excluding branch subprocess + contexts api howitworks plugins From 685bd2ef00bb6b92060014256ffc6363f6228446 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Sep 2018 06:16:04 -0400 Subject: [PATCH 254/952] Fix a versionadded comment --- coverage/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index f7d97cf61..678a7b3b5 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -124,8 +124,8 @@ def __init__( .. versionadded:: 4.2 The `concurrency` parameter can now be a list of strings. - .. versionadded:: 4.6 - The `check_preimported` parameter. + .. versionadded:: 5.0 + The `check_preimported` and `context` parameters. """ # Build our configuration from a number of sources. From bbf251638cb5c324fd3e3567372711c63d2cae78 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Sep 2018 07:50:22 -0400 Subject: [PATCH 255/952] Add repology badge --- README.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 3ad446c51..fb3fb691c 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,8 @@ Code coverage testing for Python. | |license| |versions| |status| |docs| | |ci-status| |win-ci-status| |codecov| -| |kit| |format| |saythanks| +| |kit| |format| |repos| +| |saythanks| .. downloads badge seems to be broken... |downloads| @@ -108,6 +109,9 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. .. |codecov| image:: https://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2 :target: https://codecov.io/github/nedbat/coveragepy?branch=master :alt: Coverage! +.. |repos| image:: https://repology.org/badge/tiny-repos/python:coverage.svg + :target: https://repology.org/metapackage/python:coverage/versions + :alt: Packaging status .. |saythanks| image:: https://img.shields.io/badge/saythanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/nedbat :alt: Say thanks :) From 774e65df7d1b4274b221905df14c1038b8d2a465 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Sep 2018 07:55:40 -0400 Subject: [PATCH 256/952] Other badge clean-up --- README.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index fb3fb691c..89af1ba11 100644 --- a/README.rst +++ b/README.rst @@ -7,13 +7,11 @@ Coverage.py Code coverage testing for Python. -| |license| |versions| |status| |docs| -| |ci-status| |win-ci-status| |codecov| +| |license| |versions| |status| +| |ci-status| |win-ci-status| |docs| |codecov| | |kit| |format| |repos| | |saythanks| -.. downloads badge seems to be broken... |downloads| - Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. From bd36f540f4ab9a7155da3993f5d7d48b10112900 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Sep 2018 08:18:32 -0400 Subject: [PATCH 257/952] Add Tidelift links --- README.rst | 10 +++++++++- doc/index.rst | 5 +++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 89af1ba11..b5a21d278 100644 --- a/README.rst +++ b/README.rst @@ -10,12 +10,17 @@ Code coverage testing for Python. | |license| |versions| |status| | |ci-status| |win-ci-status| |docs| |codecov| | |kit| |format| |repos| -| |saythanks| +| |tidelift| |saythanks| Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. +Professional support for coverage.py is available as part of the `Tidelift +Subscription`_. + +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme + Coverage.py runs on many versions of Python: * CPython 2.7. @@ -113,3 +118,6 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. .. |saythanks| image:: https://img.shields.io/badge/saythanks.io-%E2%98%BC-1EAEDB.svg :target: https://saythanks.io/to/nedbat :alt: Say thanks :) +.. |tidelift| image:: https://tidelift.com/badges/github/nedbat/coveragepy + :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme + :alt: Tidelift diff --git a/doc/index.rst b/doc/index.rst index 149eba07a..e1830f1a4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -93,6 +93,11 @@ not. .. _described here: http://coverage.readthedocs.io/ +Professional support for coverage.py is available as part of the `Tidelift +Subscription`_. + +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=docs + Quick start ----------- From b609117ef73ae372f027686b22f13c488c841253 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Sep 2018 10:13:32 -0400 Subject: [PATCH 258/952] We're calling this static, not global --- tests/test_context.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_context.py b/tests/test_context.py index a6be922db..ad010a37c 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -11,11 +11,11 @@ from tests.coveragetest import CoverageTest -class GlobalContextTest(CoverageTest): - """Tests of the global context.""" +class StaticContextTest(CoverageTest): + """Tests of the static context.""" def setUp(self): - super(GlobalContextTest, self).setUp() + super(StaticContextTest, self).setUp() self.skip_unless_data_storage_is("sql") def test_no_context(self): @@ -25,7 +25,7 @@ def test_no_context(self): data = cov.get_data() self.assertCountEqual(data.measured_contexts(), [""]) - def test_global_context(self): + def test_static_context(self): self.make_file("main.py", "a = 1") cov = coverage.Coverage(context="gooey") self.start_import_stop(cov, "main") From 106828c2cc8bbce1e5fb31c6a89ea3ac025225c1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Sep 2018 06:44:07 -0400 Subject: [PATCH 259/952] Dynamic contexts --- coverage/collector.py | 63 ++++++++++++++++----------------------- coverage/config.py | 2 ++ coverage/control.py | 20 +++++++++++++ coverage/ctracer/tracer.c | 5 ++-- coverage/ctracer/tracer.h | 3 +- coverage/sqldata.py | 2 ++ tests/test_context.py | 61 +++++++++++++++++++++++++++++++++++++ 7 files changed, 116 insertions(+), 40 deletions(-) diff --git a/coverage/collector.py b/coverage/collector.py index e01449798..686d4a7e5 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -34,14 +34,6 @@ CTracer = None -def should_start_context(frame): - """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" - fn_name = frame.f_code.co_name - if fn_name.startswith("test"): - return fn_name - return None - - class Collector(object): """Collects trace data. @@ -66,7 +58,10 @@ class Collector(object): # The concurrency settings we support here. SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"]) - def __init__(self, should_trace, check_include, timid, branch, warn, concurrency): + def __init__( + self, should_trace, check_include, should_start_context, + timid, branch, warn, concurrency, + ): """Create a collector. `should_trace` is a function, taking a file name and a frame, and @@ -75,6 +70,11 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency `check_include` is a function taking a file name and a frame. It returns a boolean: True if the file should be traced, False if not. + `should_start_context` is a function taking a frame, and returning a + string. If the frame should be the start of a new context, the string + is the new context. If the frame should not be the start of a new + context, return None. + If `timid` is true, then a slower simpler trace function will be used. This is important for some environments where manipulation of tracing functions make the faster more sophisticated trace function not @@ -96,6 +96,7 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency """ self.should_trace = should_trace self.check_include = check_include + self.should_start_context = should_start_context self.warn = warn self.branch = branch self.threading = None @@ -139,10 +140,6 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency ) ) - # Who-Tests-What is just a hack at the moment, so turn it on with an - # environment variable. - self.wtw = int(os.getenv('COVERAGE_WTW', 0)) - self.reset() if timid: @@ -175,7 +172,11 @@ def tracer_name(self): def _clear_data(self): """Clear out existing data, but stay ready for more collection.""" - self.data.clear() + # We used to used self.data.clear(), but that would remove filename + # keys and data values that were still in use higher up the stack + # when we are called as part of switch_context. + for d in self.data.values(): + d.clear() for tracer in self.tracers: tracer.reset_activity() @@ -187,10 +188,6 @@ def reset(self): # pairs as keys (if branch coverage). self.data = {} - # A dict mapping contexts to data dictionaries. - self.contexts = {} - self.contexts[None] = self.data - # A dictionary mapping file names to file tracer plugin names that will # handle them. self.file_tracers = {} @@ -252,11 +249,13 @@ def _start_tracer(self): tracer.threading = self.threading if hasattr(tracer, 'check_include'): tracer.check_include = self.check_include - if self.wtw: - if hasattr(tracer, 'should_start_context'): - tracer.should_start_context = should_start_context - if hasattr(tracer, 'switch_context'): - tracer.switch_context = self.switch_context + if hasattr(tracer, 'should_start_context'): + tracer.should_start_context = self.should_start_context + tracer.switch_context = self.switch_context + elif self.should_start_context: + raise CoverageException( + "Can't support dynamic contexts with {}".format(self.tracer_name()) + ) fn = tracer.start() self.tracers.append(tracer) @@ -372,12 +371,9 @@ def _activity(self): return any(tracer.activity() for tracer in self.tracers) def switch_context(self, new_context): - """Who-Tests-What hack: switch to a new who-context.""" - # Make a new data dict, or find the existing one, and switch all the - # tracers to use it. - data = self.contexts.setdefault(new_context, {}) - for tracer in self.tracers: - tracer.data = data + """Switch to a new dynamic context.""" + self.flush_data() + self.covdata.set_context(new_context) def cached_abs_file(self, filename): """A locally cached version of `abs_file`.""" @@ -415,7 +411,7 @@ def abs_file_dict(d): else: raise runtime_err # pylint: disable=raising-bad-type - return dict((self.cached_abs_file(k), v) for k, v in items) + return dict((self.cached_abs_file(k), v) for k, v in items if v) if self.branch: self.covdata.add_arcs(abs_file_dict(self.data)) @@ -423,12 +419,5 @@ def abs_file_dict(d): self.covdata.add_lines(abs_file_dict(self.data)) self.covdata.add_file_tracers(abs_file_dict(self.file_tracers)) - if self.wtw: - # Just a hack, so just hack it. - import pprint - out_file = "coverage_wtw_{:06}.py".format(os.getpid()) - with open(out_file, "w") as wtw_out: - pprint.pprint(self.contexts, wtw_out) - self._clear_data() return True diff --git a/coverage/config.py b/coverage/config.py index 9a11323d8..2a2818759 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -180,6 +180,7 @@ def __init__(self): self.data_file = ".coverage" self.debug = [] self.disable_warnings = [] + self.dynamic_context = None self.note = None self.parallel = False self.plugins = [] @@ -324,6 +325,7 @@ def from_file(self, filename, our_file): ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), ('disable_warnings', 'run:disable_warnings', 'list'), + ('dynamic_context', 'run:dynamic_context'), ('note', 'run:note'), ('parallel', 'run:parallel', 'boolean'), ('plugins', 'run:plugins', 'list'), diff --git a/coverage/control.py b/coverage/control.py index f7d97cf61..23f0cbdd0 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -347,9 +347,19 @@ def _init_for_start(self): # it for the main process. self.config.parallel = True + if self.config.dynamic_context is None: + should_start_context = None + elif self.config.dynamic_context == "test_function": + should_start_context = should_start_context_test_function + else: + raise CoverageException( + "Don't understand dynamic_context setting: {!r}".format(self.config.dynamic_context) + ) + self._collector = Collector( should_trace=self._should_trace, check_include=self._check_include_omit_etc, + should_start_context=should_start_context, timid=self.config.timid, branch=self.config.branch, warn=self._warn, @@ -886,6 +896,16 @@ def plugin_info(plugins): Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage) +def should_start_context_test_function(frame): + """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" + with open("/tmp/ssc.txt", "a") as f: + f.write("hello\n") + fn_name = frame.f_code.co_name + if fn_name.startswith("test"): + return fn_name + return None + + def process_startup(): """Call this at Python start-up to perhaps measure coverage. diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 01f8b19ba..7d639112d 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -341,7 +341,6 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) CFileDisposition * pdisp = NULL; STATS( self->stats.calls++; ) - self->activity = TRUE; /* Grow the stack. */ if (CTracer_set_pdata_stack(self) < 0) { @@ -353,7 +352,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; /* See if this frame begins a new context. */ - if (self->should_start_context && self->context == Py_None) { + if (self->should_start_context != Py_None && self->context == Py_None) { PyObject * context; /* We're looking for our context, ask should_start_context if this is the start. */ STATS( self->stats.start_context_calls++; ) @@ -866,6 +865,8 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse goto error; } + self->activity = TRUE; + switch (what) { case PyTrace_CALL: if (CTracer_handle_call(self, frame) < 0) { diff --git a/coverage/ctracer/tracer.h b/coverage/ctracer/tracer.h index 61c01b41a..a83742ddf 100644 --- a/coverage/ctracer/tracer.h +++ b/coverage/ctracer/tracer.h @@ -27,7 +27,6 @@ typedef struct CTracer { PyObject * trace_arcs; PyObject * should_start_context; PyObject * switch_context; - PyObject * context; /* Has the tracer been started? */ BOOL started; @@ -35,6 +34,8 @@ typedef struct CTracer { BOOL tracing_arcs; /* Have we had any activity? */ BOOL activity; + /* The current dynamic context. */ + PyObject * context; /* The data stack is a stack of dictionaries. Each dictionary collects diff --git a/coverage/sqldata.py b/coverage/sqldata.py index fb2279c9a..738fccef1 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -199,6 +199,8 @@ def _context_id(self, context): def set_context(self, context): """Set the current context for future `add_lines` etc.""" + if self._debug and self._debug.should('dataop'): + self._debug.write("Setting context: %r" % (context,)) self._start_using() context = context or "" with self._connect() as con: diff --git a/tests/test_context.py b/tests/test_context.py index ad010a37c..919286c52 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -6,7 +6,9 @@ import os.path import coverage +from coverage import env from coverage.data import CoverageData +from coverage.misc import CoverageException from tests.coveragetest import CoverageTest @@ -102,3 +104,62 @@ def test_combining_arc_contexts(self): self.assertEqual(combined.arcs(fred, context='blue'), []) self.assertEqual(combined.arcs(fblue, context='red'), []) self.assertEqual(combined.arcs(fblue, context='blue'), self.ARCS) + + +class DynamicContextTest(CoverageTest): + """Tests of dynamically changing contexts.""" + + def setUp(self): + super(DynamicContextTest, self).setUp() + self.skip_unless_data_storage_is("sql") + if not env.C_TRACER: + self.skipTest("Only the C tracer supports dynamic contexts") + + def test_simple(self): + self.make_file("two_tests.py", """\ + def helper(lineno): + x = 2 + + def test_one(): + a = 5 + helper(6) + + def test_two(): + a = 9 + b = 10 + if a > 11: + b = 12 + assert a == (13-4) + assert b == (14-4) + helper(15) + + test_one() + x = 18 + helper(19) + test_two() + """) + cov = coverage.Coverage(source=["."]) + cov.set_option("run:dynamic_context", "test_function") + self.start_import_stop(cov, "two_tests") + data = cov.get_data() + + fname = os.path.abspath("two_tests.py") + self.assertCountEqual(data.measured_contexts(), ["", "test_one", "test_two"]) + self.assertCountEqual(data.lines(fname, ""), [1, 4, 8, 17, 18, 19, 2, 20]) + self.assertCountEqual(data.lines(fname, "test_one"), [5, 6, 2]) + self.assertCountEqual(data.lines(fname, "test_two"), [9, 10, 11, 13, 14, 15, 2]) + + +class DynamicContextWithPythonTracerTest(CoverageTest): + """The Python tracer doesn't do dynamic contexts at all.""" + + run_in_temp_dir = False + + def test_python_tracer_fails_properly(self): + if env.C_TRACER: + self.skipTest("This test is specifically about the Python tracer.") + cov = coverage.Coverage() + cov.set_option("run:dynamic_context", "test_function") + msg = r"Can't support dynamic contexts with PyTracer" + with self.assertRaisesRegex(CoverageException, msg): + cov.start() From 9a0f9e19ba43887d53fdc53f015f00148e9ba2b9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 23 Sep 2018 20:21:42 -0400 Subject: [PATCH 260/952] Make static and dynamic contexts work together --- coverage/collector.py | 8 ++++- tests/test_context.py | 71 +++++++++++++++++++++++++++---------------- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/coverage/collector.py b/coverage/collector.py index 686d4a7e5..4e7058a0c 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -373,7 +373,13 @@ def _activity(self): def switch_context(self, new_context): """Switch to a new dynamic context.""" self.flush_data() - self.covdata.set_context(new_context) + if self.static_context: + context = self.static_context + if new_context: + context += ":" + new_context + else: + context = new_context + self.covdata.set_context(context) def cached_abs_file(self, filename): """A locally cached version of `abs_file`.""" diff --git a/tests/test_context.py b/tests/test_context.py index 919286c52..4339d336e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -115,29 +115,35 @@ def setUp(self): if not env.C_TRACER: self.skipTest("Only the C tracer supports dynamic contexts") - def test_simple(self): - self.make_file("two_tests.py", """\ - def helper(lineno): - x = 2 - - def test_one(): - a = 5 - helper(6) - - def test_two(): - a = 9 - b = 10 - if a > 11: - b = 12 - assert a == (13-4) - assert b == (14-4) - helper(15) - - test_one() - x = 18 - helper(19) - test_two() - """) + SOURCE = """\ + def helper(lineno): + x = 2 + + def test_one(): + a = 5 + helper(6) + + def test_two(): + a = 9 + b = 10 + if a > 11: + b = 12 + assert a == (13-4) + assert b == (14-4) + helper(15) + + test_one() + x = 18 + helper(19) + test_two() + """ + + OUTER_LINES = [1, 4, 8, 17, 18, 19, 2, 20] + TEST_ONE_LINES = [5, 6, 2] + TEST_TWO_LINES = [9, 10, 11, 13, 14, 15, 2] + + def test_dynamic_alone(self): + self.make_file("two_tests.py", self.SOURCE) cov = coverage.Coverage(source=["."]) cov.set_option("run:dynamic_context", "test_function") self.start_import_stop(cov, "two_tests") @@ -145,9 +151,22 @@ def test_two(): fname = os.path.abspath("two_tests.py") self.assertCountEqual(data.measured_contexts(), ["", "test_one", "test_two"]) - self.assertCountEqual(data.lines(fname, ""), [1, 4, 8, 17, 18, 19, 2, 20]) - self.assertCountEqual(data.lines(fname, "test_one"), [5, 6, 2]) - self.assertCountEqual(data.lines(fname, "test_two"), [9, 10, 11, 13, 14, 15, 2]) + self.assertCountEqual(data.lines(fname, ""), self.OUTER_LINES) + self.assertCountEqual(data.lines(fname, "test_one"), self.TEST_ONE_LINES) + self.assertCountEqual(data.lines(fname, "test_two"), self.TEST_TWO_LINES) + + def test_static_and_dynamic(self): + self.make_file("two_tests.py", self.SOURCE) + cov = coverage.Coverage(context="stat", source=["."]) + cov.set_option("run:dynamic_context", "test_function") + self.start_import_stop(cov, "two_tests") + data = cov.get_data() + + fname = os.path.abspath("two_tests.py") + self.assertCountEqual(data.measured_contexts(), ["stat", "stat:test_one", "stat:test_two"]) + self.assertCountEqual(data.lines(fname, "stat"), self.OUTER_LINES) + self.assertCountEqual(data.lines(fname, "stat:test_one"), self.TEST_ONE_LINES) + self.assertCountEqual(data.lines(fname, "stat:test_two"), self.TEST_TWO_LINES) class DynamicContextWithPythonTracerTest(CoverageTest): From 7b0badf176832c11ec6a08c124985638c6a13407 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 24 Sep 2018 06:15:37 -0400 Subject: [PATCH 261/952] This could come in handy eventually --- lab/find_class.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 lab/find_class.py diff --git a/lab/find_class.py b/lab/find_class.py new file mode 100644 index 000000000..d8dac0b58 --- /dev/null +++ b/lab/find_class.py @@ -0,0 +1,40 @@ +class Parent(object): + def meth(self): + print("METH") + +class Child(Parent): + pass + +def trace(frame, event, args): + # Thanks to Aleksi Torhamo for code and idea. + co = frame.f_code + fname = co.co_name + if not co.co_varnames: + return + locs = frame.f_locals + first_arg = co.co_varnames[0] + if co.co_argcount: + self = locs[first_arg] + elif co.co_flags & 0x04: # *args syntax + self = locs[first_arg][0] + else: + return + + func = getattr(self, fname).__func__ + if hasattr(func, '__qualname__'): + qname = func.__qualname__ + else: + for cls in self.__class__.__mro__: + f = cls.__dict__.get(fname, None) + if f is None: + continue + if f is func: + qname = cls.__name__ + "." + fname + break + print("{}: {}.{} {}".format(event, self, fname, qname)) + return trace + +import sys +sys.settrace(trace) + +Child().meth() From b5d5aa99ebcfa140bc779301b22a0866903b6342 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 24 Sep 2018 06:46:08 -0400 Subject: [PATCH 262/952] Minimal docs for dynamic contexts --- CHANGES.rst | 23 +++++++++++++++++++++-- doc/contexts.rst | 18 ++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 089fec7f4..b449797b7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,8 +19,27 @@ Unreleased - Context support: static contexts let you specify a label for a coverage run, which is recorded in the data, and retained when you combine files. See - :ref:`contexts` for more information. Currently, only static contexts are - supported, with no reporting features. + :ref:`contexts` for more information. + +- Dynamic contexts: specifying ``[run] dynamic_context = test_function`` in the + config file will record the test function name as a dynamic context during + execution. This is the core of "Who Tests What" (`issue 170`_). Things to + note: + + - There is no reporting support yet. Use SQLite to query the .coverage file + for information. Ideas are welcome about how reporting could be extended + to use this data. + + - There's a noticeable slow-down before any test is run. + + - Data files will now be roughly N times larger, where N is the number of + tests you have. Combining data files is therefore also N times slower. + + - No other values for ``dynamic_context`` are recognized yet. Let me know + what else would be useful. I'd like to use a pytest plugin to get better + information directly from pytest, for example. + +.. _issue 170: https://github.com/nedbat/coveragepy/issues/170 - Environment variable substitution in configuration files now supports two syntaxes for controlling the behavior of undefined variables: if ``VARNAME`` diff --git a/doc/contexts.rst b/doc/contexts.rst index c1d4a173a..1f1ce763b 100644 --- a/doc/contexts.rst +++ b/doc/contexts.rst @@ -16,9 +16,8 @@ in which it was run. This can provide more information to help you understand the behavior of your tests. There are two kinds of context: static and dynamic. Static contexts are fixed -for an entire run, and are set explicitly with an option. - -Dynamic contexts are coming soon. +for an entire run, and are set explicitly with an option. Dynamic contexts +change over the course of a single run. Static contexts @@ -39,7 +38,18 @@ A static context is specified with the ``--context=CONTEXT`` option to Dynamic contexts ---------------- -Not implemented yet. +Dynamic contexts are found during execution. There is currently support for +one kind: test function names. Set the ``dynamic_context`` option to +``test_function`` in the configuration file:: + + [run] + dynamic_context = test_function + +Each test function you run will be considered a separate dynamic context, and +coverage data will be segregated for each. A test function is any function +whose names starts with "test". + +Ideas are welcome for other dynamic contexts that would be useful. Context reporting From 2133b288dcc7e4336e7956ab49fbd8eb9eb45e3c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 17 Sep 2018 19:23:43 +0200 Subject: [PATCH 263/952] CoverageSqliteData.__nonzero__: do not create DB This makes is more lazy and avoids creating an empty DB unnecessarily. --- coverage/sqldata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 738fccef1..e9ccbede4 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -158,6 +158,8 @@ def _connect(self): return self._db def __nonzero__(self): + if self._db is None and not os.path.exists(self.filename): + return False try: with self._connect() as con: rows = con.execute("select * from file limit 1") From abb9391529c4885b2170eb64804349bd01860613 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 25 Sep 2018 05:21:18 -0400 Subject: [PATCH 264/952] Even more clarity for an error message --- coverage/cmdline.py | 3 ++- tests/test_cmdline.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 7137852db..edbc1d25a 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -622,7 +622,8 @@ def do_run(self, options, args): if getattr(options, opt_name) is not None: self.help_fn( "Options affecting multiprocessing must only be specified " - "in a configuration file." + "in a configuration file.\n" + "Remove --{} from the command line.".format(opt_name) ) return ERR diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index db89137bf..39827ff54 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -473,6 +473,10 @@ def test_multiprocessing_needs_config_file(self): "Options affecting multiprocessing must only be specified in a configuration file.", self.stderr() ) + self.assertIn( + "Remove --branch from the command line.", + self.stderr() + ) def test_run_debug(self): self.cmd_executes("run --debug=opt1 foo.py", """\ From a508264b7c8becad25a5c2dda1a034222658716f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 26 Sep 2018 15:40:31 -0400 Subject: [PATCH 265/952] Add Tidelift logo to README.rst --- README.rst | 15 +++++++++++++-- ...gos_RGB_Tidelift_Shorthand_On-White_small.png | Bin 0 -> 7070 bytes 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 doc/media/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png diff --git a/README.rst b/README.rst index b5a21d278..c0df1d18f 100644 --- a/README.rst +++ b/README.rst @@ -16,8 +16,19 @@ Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. -Professional support for coverage.py is available as part of the `Tidelift -Subscription`_. +.. |tideliftlogo| image:: doc/media/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png + :width: 75 + :alt: Tidelift + +.. list-table:: + :widths: 10 100 + + * - |tideliftlogo| + - Professional support for coverage.py is available as part of the `Tidelift + Subscription`_. Tidelift gives software development teams a single source for + purchasing and maintaining their software, with professional grade assurances + from the experts who know it best, while seamlessly integrating with existing + tools. .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme diff --git a/doc/media/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png b/doc/media/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png new file mode 100644 index 0000000000000000000000000000000000000000..53b9c0fc3f37d546a2f8c1b8fedcf95de3f3331d GIT binary patch literal 7070 zcmeG>cTiJbm!V1(0qIq`v>;Vbn)Hqc0qMO+liq{`P!WEB5(vGj5Frq1KnNg3x)g&X zNJn~!6e-d-{=RR&-JRLLcV=h4H*@bh_q=mYyXT&J?z}`(V;x#5b}9e>K&z*#X$Alg ziT`O7NW1bE{x0BSR$@ ze_u%_SASkE}=<2Ryrm6kU>4cFopJ#CJQzalUEG$eiOh(c_&;xi! zQBe^nB@L98mLO0_1V#7-JB3U51@Zrb$$#?EbPI9`^m-cX42_H;Qe{?r zrh+F&Mn-y}&+*D^Sdll7oSd8hhv>t&`T)q%`>Q zV|IALL}`w^)Q@KUVJwuf%8$Mwt(u z`OaroE1xbP!MLvXLZ|sQ9nIl>|F^RHPMh;QkrL{|!1v^SWse~Het~c;B z5zVCVWe=Fqd9Lz}SV{s|HD^P(H$#omw+N+restW(oYLQNPw= z9gG}i?pc#JOMoIVs0rmq1Ddt2wQVAn&J`XW{LB&ZqCsIEKd5D!O6SysD!h8f(gJgA z?xbYJqlN@NyBkU4hv_6w&Pz2rHXDu8HbF@%kxa$Mb0}7xZe+RDjaUl?!bnb>2;nWt ztmz0N{e3f(mf8YmF8&HY+|A7ENQ)+)j0qrH)~!6EzpB1XoFEtMM@3pWqAecVu}QJ6 zD77r4S^~D?QLI#F%KSuMlX{Q1YKARO8yoC$92aIvRW8Xoe* zplk0AY)nQ3erYq5Pt7NBftGwpl_))f0+&Q+d~d44sU4nnl09P!HRX2<0VomJwezPK zWUay#xxR#>>mizDrp6ZUsbiLEb&l>=n^>ztupTp+47`pKg5xo5(IcV4qGTqz9ZGy2 zK5rb@el;=JfuNpi!rmqPe54(2`M9mG0|7F<9TT3ru<~g2k;SdfFu_?5e)7)x?C$ z?lb!q>dOFSVWeHT=TH;-qY35Rr-%M!s;3;IgUz@Yy%?(Kb{da1jTnkX_4^qAN6OK= ziu+SFIxVt}MuTmpu?ECyyho9j2PK&{EtZiQCawJNj19qWSsv zf14a%Y_yV#g_7Cm&Yl}|Y--2GwqYShi1&O+ia{TLaF%S-67 z@8#iIVbn#XHoty+&s*=x)xSzHty~=U=Eg!xrWHfCv(tcWYK1~z_%yZ<-5?wZx~tZouJIb+I4i^GK+ zEd2dM)YpEg12p*~31i=x+beA*C{Gk>G?JI4Wdiu41lPwGQ8N`PV71W^Mc=N+TvZlh z|2|>j_1r5tB}ywjFMZ+ON*QE9oAaE!@6`KF2;@!oCOiSwtzW83v1|c>z;L;xwiL^z z1nB=7XZIp-;KbTladu~LG_Ke~F(dDeyzxkBu`p!ur5YVEX(e|}Qrm}=6`C@a-o!z% z&`BtA!??l3uIIb_BL;3XVb_H8n6JNhujBUC%ajcW7@7}w1 z^z(9Zdz%Y|^~z5FRaV`#_WlAtk3KtICPwkp%Of`?yxiD&QD!3C^L%O@k<>aRSFa8NCj!YrOeAlfvD8tA zZ(M$#y@GdEgusgD`YM&Y8cB-nPY1K`vpX$c1sOiJPjKTfd-a8V14_Q>$Mq6w9fP44 zw4%o)_9}Lvee4feZNUqRvqfK@hd^4nD#=j=Js3U7=u&m>`FLB@e$?MTphE>JY^fgr0dw6GWI-zgeDF;6GZ>TH5rjbhE(pY^Zg-Fy7IcjEh)^tF` zPFo1q0xmX&YZn&_9(JikBx2x%a4~-y{(4}cd1UN``jT4rYoOBZE>dG^+>5MY`9ZCB z#oKA4UX-iP*20_}_{ZnHgq5)bOIKpGK2%avNsnCBi?4I6^>^OcsUl!#)0c>I>@Dg; zAonDC4KzCzw^uPS<<}R?lIo2Ie>{a%$tc(fFaoZ48Hspka#NN+O6ffZPNhOmY#~Xb z9&M7A6{h6a=$y2u@t2E@vQ}QB?zFv#%pqLYv2lv`M#m>(z`7>Hm}31iMEs*Wx`JA! z=hq_jjhdGZ*Ss@G5K9*OjCpn2#N?ywIQv$I)Vnx)SgYTZa-Q6vL0Fz%A1c_ZfMSf2 zsDO2hk~ktUpz0@U5b7q8nB_#XtJ8ZO=j3Fvh+*E}b-o!YHCLU=S13o8r>=B7o+* zhB&JUQv|;)K1?rkWsK&DRxM73ei0?83t|iR955)->n*PYezHwnS{LL{%;K6spN#}F zBPW8`tbc+UoMyGY6(4@yp7rmaoW}Bg06)AQHCF!wR$3{hpAM@NB*YCUIs`Ut<~29S z?P$x5$d;li+C=iY-bXj=VsU(BUs%=<# z?A3(J0PBytE1is_RN{_2V?3Hoeix(?iq8(si!mLrvH^ZVB|g>!A&1LaO@#SH9VZr8 zUVad2ffmEa5xuF1m{nMTOuC-Z#O;{TsW1s9VWRfMq(?aAEZGerFV_}7`P}(-UmMOT zu$aP-UPoZX#3SWWBx8G&0Yg4t-}$DiRn&gUG*GqAe60(9>~lM$fSj7y8UnFc-*jl% z1x2bziwNNOf_?BK8^C1P7yh~fp)H-mQrzZGuC0&4qN?{JoCd^rD)8Cu2R}II$mse_ z{4v~CVdXXE^kfuPT9t>_-TcT9&0}-*z83@c;K!-2v;3&vF6-`V%RDtsaSPFPZ*Efd zXx|7&M!-ow3$HbqfKQc9Hd4s@f{%lmVG-3I3A^?O_F`Hesmw{s+L9*^?mN&zF&>$^ zwFVniF_Yr!5ok9DVX8B9v#}4?iB^hjUJWwQ$o#wjGk$8uAmVoXi05pY*n90C2N}NB zR<9V!otkP{Fj}#E!DzOD^@?+Yu>c~mYp_XwdQ4$T89y3w4-1=(T4AVIvA&_jV4@%29%{td z)a8Iil*Jzx9{lJUwDS5ZWY)u#B>+J#F)Nv%a$ti#u`YtZs!z9@W%X+GLy3T@mgKlk zqG*T$E;mr<@UG%TCvPvJWjNiz`+3PuGsj^2cZE+bTUAPBr6gC|e4z%}S1YVy{a`Mj zbyve0AJMH!{N(8^w)WHMy*uFgqhQw;PAF;z^5c-;`GOA_2+TCPrf%d}m+jU1o@|Yz zHLJaNaz_ojyAwpGws;_XB4lE@bzQj0WS(zL?^Bz2{DO3*>=#FvnOF6^{z9@S6WgvT zJtveB;vnC&9VL{bo}XiM9i_{%T5CnT&A^OkcQe#Yt%k-PULlQBhB}MEcICgDh9WQp z+N|CH4j5313%4KA;c)FTgqSyn;?1+5b&XE;ZMGgk#{wcKPBmSF85*y?)^&R1t9rrD zfdjiZcU?Un8QYj_%!rccL$o+!iNIXHag>0+)0*8w63HcNJNi|>Tg0HB6$f}S%w7c+ zaAX@E*f)Xe?NyO^cofs9Y+o8%aa{DhHChI7m;`48DAB($r0Dyrm;H~VF6>YaKe81kjvx0zXa;Xn>dfP4A{ok6kn5i7JoWYO9v2E3iBrfe} z#aG*m(oVY(znV9>p3LX!X0H$ zn&+Ymc*hcn3#7&pNea{)ZN(C3BhvWXninO_IPEcxf+S|x{@01#)9~sr!}D+915qA$ z*R$UhWWr-ZHJQRIhB26TjMwtqh!>4=balh%Wo#wu=F}mC+nb2DzjqDh{?MpRXsYEo zu(@|{Q+ixT86p0YShBC0k*uodpr16(f4lw=T~_VM#WtE_m=!;?A4OUzj+7?cv&1q#B*0?qe=A#H6Twxuo}CjmMbtR4*e$=Me-V zmwSk@M5f9c>uv|8HuWKrb{7xxO;e}pLhTpbJT!j{ev(=zAt{mh18(xfic{XGsWZ1(W{Qm?oKsYD01*}9 z$Ao&(t-P^cjF^mt*O94}$n+U$odDtdG4oBcB&Cv6>kthmDgI62sz)e=OQ-m%DQ?;l zQspkW+8%vBl#mh(P+Io%A|DCGQZ-?!f?VTUvLbe(F4-e~PS3xZ@ z(SqHB{NidI?2Chsy|5!`Oesqr<6l$t-kz@6k!Q3SH+ET2JYc9_1G0v9iB(Z!IV#oW zQ~Ybb-$J#M3g~a(SF~>qbO+!X9CYpRJ3Rci#_q}|D@WNrY8fs+%3mlsVn+r%F-B0| z&8B%04y_yP-l{VC@)a3{EbysR>v4Zq7&*AITAwN_$iTU0jk@<{?j_1teDU2|Ju zIHcalrb=jW>wwr!B`?S5ZAQRbkM7CB43z9PD)c(q&^fIqIQkKtEAVckNz+`>royn6 ze7C_nUV@`vShIR^Tl0W`Njv2QG;xC;RfW=o*dWm1^~ubbY>j4b;C3>)ef2r!&iKkf zA!*BCwq>dyuV!t%j(t%6FS_dYU>J>zS54l>liDko?TqDjsh|CKZKlZO9Ps$Uoaz@~ z_>jQL*sC?r2zyXf$WayAZC(cIvIiHnTgF;w|te zc)ScO*YZl^I|eQw%psbqfN%0@v{vr!Q7Az}bCvaUqb&BN{8J*NXgHdDexEPtj32N# zJpz?1xIRq0pbSdsG zHvgo5+z&gpPj}qyYT@CyCytK$S{&Ee6+CzT%NyL^^fk)R2mIi{*XPT6VO|Q}2T+7Q%VhUH?4i%|!B}p$p z1h=boE*Gse-4_}mFnd0Np_1Ix5Xu@r6luk8uYTcf63N$F4l Date: Thu, 27 Sep 2018 11:40:39 -0400 Subject: [PATCH 266/952] Faster combining --- coverage/sqldata.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index e9ccbede4..a5e84aebf 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -331,15 +331,19 @@ def update(self, other_data, aliases=None): # See what we had already measured, for accurate conflict reporting. this_measured = self.measured_files() + other_files = set() + # lines if other_data._has_lines: for context in other_data.measured_contexts(): self.set_context(context) for filename in other_data.measured_files(): lines = set(other_data.lines(filename, context=context)) - filename = aliases.map(filename) - lines.update(self.lines(filename, context=context) or ()) - self.add_lines({filename: lines}) + if lines: + other_files.add(filename) + filename = aliases.map(filename) + lines.update(self.lines(filename, context=context) or ()) + self.add_lines({filename: lines}) # arcs if other_data._has_arcs: @@ -347,12 +351,14 @@ def update(self, other_data, aliases=None): self.set_context(context) for filename in other_data.measured_files(): arcs = set(other_data.arcs(filename, context=context)) - filename = aliases.map(filename) - arcs.update(self.arcs(filename, context=context) or ()) - self.add_arcs({filename: arcs}) + if arcs: + other_files.add(filename) + filename = aliases.map(filename) + arcs.update(self.arcs(filename, context=context) or ()) + self.add_arcs({filename: arcs}) # file_tracers - for filename in other_data.measured_files(): + for filename in other_files: other_plugin = other_data.file_tracer(filename) filename = aliases.map(filename) if filename in this_measured: From 7dcafa149bad768a16a01c940a566ef19d2c6846 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 14:46:45 -0400 Subject: [PATCH 267/952] Record the sys.argv in the db --- coverage/sqldata.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index a5e84aebf..82f37a9bf 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -15,6 +15,7 @@ import itertools import os import sqlite3 +import sys from coverage.backward import iitems from coverage.data import filename_suffix @@ -36,7 +37,8 @@ create table meta ( has_lines boolean, - has_arcs boolean + has_arcs boolean, + sys_argv text ); create table file ( @@ -118,8 +120,8 @@ def _create_db(self): self._db.execute(stmt) self._db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) self._db.execute( - "insert into meta (has_lines, has_arcs) values (?, ?)", - (self._has_lines, self._has_arcs) + "insert into meta (has_lines, has_arcs, sys_argv) values (?, ?, ?)", + (self._has_lines, self._has_arcs, str(getattr(sys, 'argv', None))) ) def _open_db(self): From 8f2aa0f149366e09ede622379011add53e69e2af Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 19:13:53 -0400 Subject: [PATCH 268/952] Oops, remove noisy debugging code --- coverage/control.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 4f2afda0d..83bcedb55 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -898,8 +898,6 @@ def plugin_info(plugins): def should_start_context_test_function(frame): """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" - with open("/tmp/ssc.txt", "a") as f: - f.write("hello\n") fn_name = frame.f_code.co_name if fn_name.startswith("test"): return fn_name From 8acd852de3faab4406bc05abf312ed18ed2985ca Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 19:27:41 -0400 Subject: [PATCH 269/952] Move the context determiner to its own file --- coverage/context.py | 11 +++++++++++ coverage/control.py | 9 +-------- 2 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 coverage/context.py diff --git a/coverage/context.py b/coverage/context.py new file mode 100644 index 000000000..24d01f2a9 --- /dev/null +++ b/coverage/context.py @@ -0,0 +1,11 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Determine contexts for coverage.py""" + +def should_start_context_test_function(frame): + """Is this frame calling a test_* function?""" + fn_name = frame.f_code.co_name + if fn_name.startswith("test"): + return fn_name + return None diff --git a/coverage/control.py b/coverage/control.py index 83bcedb55..dd862ae9c 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -14,6 +14,7 @@ from coverage.backward import string_class, iitems from coverage.collector import Collector, CTracer from coverage.config import read_coverage_config +from coverage.context import should_start_context_test_function from coverage.data import CoverageData, combine_parallel_data from coverage.debug import DebugControl, write_formatted_info from coverage.disposition import disposition_debug_msg @@ -896,14 +897,6 @@ def plugin_info(plugins): Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage) -def should_start_context_test_function(frame): - """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" - fn_name = frame.f_code.co_name - if fn_name.startswith("test"): - return fn_name - return None - - def process_startup(): """Call this at Python start-up to perhaps measure coverage. From 1cf8962099a98829761fcf24f38e9acf77802f9c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 20:06:28 -0400 Subject: [PATCH 270/952] Include doc image files in the sdist --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index c9b345e10..ebc8116ea 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,7 +28,7 @@ exclude ci/appveyor.token recursive-include coverage/fullcoverage *.py recursive-include coverage/ctracer *.c *.h -recursive-include doc conf.py *.pip *.rst *.txt +recursive-include doc conf.py *.pip *.rst *.txt *.png recursive-include doc/_static *.* prune doc/_build From f9cfc98ebf12f3f04479bee9fab9358fdf9d4559 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 20:06:48 -0400 Subject: [PATCH 271/952] Get qualified names for method contexts --- coverage/context.py | 38 +++++++++++++++++++++++++++++++++++++- tests/test_context.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/coverage/context.py b/coverage/context.py index 24d01f2a9..f167de367 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -7,5 +7,41 @@ def should_start_context_test_function(frame): """Is this frame calling a test_* function?""" fn_name = frame.f_code.co_name if fn_name.startswith("test"): - return fn_name + return qualname_from_frame(frame) return None + + +def qualname_from_frame(frame): + """Get a qualified name for the code running in `frame`.""" + co = frame.f_code + fname = co.co_name + if not co.co_varnames: + return fname + + locs = frame.f_locals + first_arg = co.co_varnames[0] + if co.co_argcount and first_arg == "self": + self = locs["self"] + #elif co.co_flags & 0x04: # *args syntax + # self = locs[first_arg][0] + else: + return fname + + method = getattr(self, fname, None) + if method is None: + return fname + + func = method.__func__ + if hasattr(func, '__qualname__'): + qname = func.__qualname__ + else: + for cls in self.__class__.__mro__: + f = cls.__dict__.get(fname, None) + if f is None: + continue + if f is func: + qname = cls.__name__ + "." + fname + break + else: + qname = fname + return qname diff --git a/tests/test_context.py b/tests/test_context.py index 4339d336e..eeca81cd3 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -3,10 +3,12 @@ """Tests for context support.""" +import inspect import os.path import coverage from coverage import env +from coverage.context import qualname_from_frame from coverage.data import CoverageData from coverage.misc import CoverageException @@ -182,3 +184,44 @@ def test_python_tracer_fails_properly(self): msg = r"Can't support dynamic contexts with PyTracer" with self.assertRaisesRegex(CoverageException, msg): cov.start() + + +def get_qualname(): + """Helper to return qualname_from_frame for the caller.""" + caller_frame = inspect.stack()[1][0] + return qualname_from_frame(caller_frame) + + +class Parent(object): # pylint: disable=missing-docstring + def meth(self): + return get_qualname() + +class Child(Parent): # pylint: disable=missing-docstring + pass + +class SomethingElse(object): # pylint: disable=missing-docstring + pass + +class MultiChild(SomethingElse, Child): # pylint: disable=missing-docstring + pass + +def fake_out(self): # pylint: disable=missing-docstring + return get_qualname() + + +class QualnameTest(CoverageTest): + """Tests of qualname_from_frame.""" + + run_in_temp_dir = False + + def test_method(self): + self.assertEqual(Parent().meth(), "Parent.meth") + + def test_inherited_method(self): + self.assertEqual(Child().meth(), "Parent.meth") + + def test_mi_inherited_method(self): + self.assertEqual(MultiChild().meth(), "Parent.meth") + + def test_fake_out(self): + self.assertEqual(fake_out(0), "fake_out") From b528c1e91d56761e7ca89074164859f26bfdd7f3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 20:31:42 -0400 Subject: [PATCH 272/952] Deal with properties in qualname_from_frame --- coverage/context.py | 5 ++++- tests/test_context.py | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/coverage/context.py b/coverage/context.py index f167de367..c34162089 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -31,7 +31,10 @@ def qualname_from_frame(frame): if method is None: return fname - func = method.__func__ + func = getattr(method, '__func__', None) + if func is None: + return fname + if hasattr(func, '__qualname__'): qname = func.__qualname__ else: diff --git a/tests/test_context.py b/tests/test_context.py index eeca81cd3..2473c6941 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -188,7 +188,12 @@ def test_python_tracer_fails_properly(self): def get_qualname(): """Helper to return qualname_from_frame for the caller.""" - caller_frame = inspect.stack()[1][0] + stack = inspect.stack()[1:] + if any(sinfo[0].f_code.co_name == "get_qualname" for sinfo in stack): + # We're calling outselves recursively, maybe because we're testing + # properties. Return an int to try to get back on track. + return 17 + caller_frame = stack[0][0] return qualname_from_frame(caller_frame) @@ -196,6 +201,10 @@ class Parent(object): # pylint: disable=missing-docstr def meth(self): return get_qualname() + @property + def a_property(self): + return get_qualname() + class Child(Parent): # pylint: disable=missing-docstring pass @@ -225,3 +234,7 @@ def test_mi_inherited_method(self): def test_fake_out(self): self.assertEqual(fake_out(0), "fake_out") + + def test_property(self): + # I'd like this to be "Parent.a_property", but this might be ok too. + self.assertEqual(Parent().a_property, "a_property") From cb777b033f49ef18ef8e9098404a50df56bf8207 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 20:45:18 -0400 Subject: [PATCH 273/952] Lint --- tests/test_context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_context.py b/tests/test_context.py index 2473c6941..4d89017b4 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -198,11 +198,11 @@ def get_qualname(): class Parent(object): # pylint: disable=missing-docstring - def meth(self): + def meth(self): # pylint: disable=missing-docstring return get_qualname() @property - def a_property(self): + def a_property(self): # pylint: disable=missing-docstring return get_qualname() class Child(Parent): # pylint: disable=missing-docstring @@ -214,7 +214,7 @@ class SomethingElse(object): # pylint: disable=missing-docstr class MultiChild(SomethingElse, Child): # pylint: disable=missing-docstring pass -def fake_out(self): # pylint: disable=missing-docstring +def fake_out(self): # pylint: disable=missing-docstring, unused-argument return get_qualname() From 53a7dffb4e844c7293c6a21318fd9b33168d2598 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 20:48:58 -0400 Subject: [PATCH 274/952] Simplify --- coverage/context.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/coverage/context.py b/coverage/context.py index c34162089..25be98c52 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -5,8 +5,7 @@ def should_start_context_test_function(frame): """Is this frame calling a test_* function?""" - fn_name = frame.f_code.co_name - if fn_name.startswith("test"): + if frame.f_code.co_name.startswith("test"): return qualname_from_frame(frame) return None @@ -18,12 +17,9 @@ def qualname_from_frame(frame): if not co.co_varnames: return fname - locs = frame.f_locals first_arg = co.co_varnames[0] if co.co_argcount and first_arg == "self": - self = locs["self"] - #elif co.co_flags & 0x04: # *args syntax - # self = locs[first_arg][0] + self = frame.f_locals["self"] else: return fname From 0525cd6e3d4a3cd2b3231d14d662e7650ed627c6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 28 Sep 2018 20:49:26 -0400 Subject: [PATCH 275/952] The idea of checking the mro explicitly in qualname_from_frame was Aleksi's --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 011e234c7..2e0bcbbaf 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -5,6 +5,7 @@ Other contributions, including writing code, updating docs, and submitting useful bug reports, have been made by: Adi Roiban +Aleksi Torhamo Alex Gaynor Alex Groce Alex Sandro From dde63a57f2b9953c1c966968133743f1b4cdea9c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 30 Sep 2018 14:22:40 -0400 Subject: [PATCH 276/952] More tests of qualname_from_frame --- tests/test_context.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_context.py b/tests/test_context.py index 4d89017b4..b237af065 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -214,9 +214,17 @@ class SomethingElse(object): # pylint: disable=missing-docstr class MultiChild(SomethingElse, Child): # pylint: disable=missing-docstring pass +def no_arguments(): # pylint: disable=missing-docstring + return get_qualname() + +def plain_old_function(a, b): # pylint: disable=missing-docstring, unused-argument + return get_qualname() + def fake_out(self): # pylint: disable=missing-docstring, unused-argument return get_qualname() +def meth(self): + return get_qualname() class QualnameTest(CoverageTest): """Tests of qualname_from_frame.""" @@ -232,9 +240,20 @@ def test_inherited_method(self): def test_mi_inherited_method(self): self.assertEqual(MultiChild().meth(), "Parent.meth") + def test_no_arguments(self): + self.assertEqual(no_arguments(), "no_arguments") + + def test_plain_old_function(self): + self.assertEqual(plain_old_function(0, 1), "plain_old_function") + def test_fake_out(self): self.assertEqual(fake_out(0), "fake_out") def test_property(self): # I'd like this to be "Parent.a_property", but this might be ok too. self.assertEqual(Parent().a_property, "a_property") + + def test_changeling(self): + c = Child() + c.meth = meth + self.assertEqual(c.meth(c), "meth") From 401471c1c51c2a99b31b0508578d04b4da03eebb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 3 Oct 2018 08:01:21 -0400 Subject: [PATCH 277/952] Pin everything --- requirements/dev.pip | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/dev.pip b/requirements/dev.pip index c2cfcb1bc..b497deb80 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -5,6 +5,7 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ pip==18.0.0 +virtualenv==16.0.0 # PyPI requirements for running tests. -r tox.pip From 3d8dd2ab07cc6d3149e7308e1e3be194a21364f6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 16 Sep 2018 12:04:54 -0400 Subject: [PATCH 278/952] Add 3.8 to tox --- tox.ini | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 0eb9f6983..55819e3e9 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [tox] -envlist = py{27,34,35,36,37}, pypy{2,3}, doc, lint +envlist = py{27,34,35,36,37,38}, pypy{2,3}, doc, lint skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True} toxworkdir = {env:TOXWORKDIR:.tox} @@ -17,8 +17,8 @@ deps = setuptools==40.0.0 # gevent 1.3 causes a failure: https://bitbucket.org/ned/coveragepy/issues/663/gevent-132-on-windows-fails py{27,34,35,36}: gevent==1.2.2 - py{27,34,35,36,37}: eventlet==0.24.1 - py{27,34,35,36,37}: greenlet==0.4.14 + py{27,34,35,36,37,38}: eventlet==0.24.1 + py{27,34,35,36,37,38}: greenlet==0.4.14 # Windows can't update the pip version with pip running, so use Python # to install things. @@ -45,6 +45,9 @@ commands = python setup.py --quiet build_ext --inplace python igor.py test_with_tracer c {posargs} +[testenv:py38] +basepython = python3.8 + [testenv:pypy] # The "pypy" environment is for Travis. Probably can make Travis use one of # the other environments... From 5aca5af02423f37ec7f4a3a02849e74c54b5ccf1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 2 Oct 2018 18:54:15 -0400 Subject: [PATCH 279/952] Python 3.6 changed lnotab to signed bytes --- coverage/parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coverage/parser.py b/coverage/parser.py index c9eb793f6..bb99cf0d7 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -409,6 +409,8 @@ def _bytes_lines(self): yield (byte_num, line_num) last_line_num = line_num byte_num += byte_incr + if env.PYVERSION >= (3, 6) and line_incr >= 0x80: + line_incr -= 0x100 line_num += line_incr if line_num != last_line_num: yield (byte_num, line_num) From cf7e8717d73e638d92838f8534712351dda9e0f1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 3 Oct 2018 21:30:27 -0400 Subject: [PATCH 280/952] Python 3.8 uses Constant nodes in the AST --- coverage/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/parser.py b/coverage/parser.py index bb99cf0d7..2eae4eb51 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -722,7 +722,7 @@ def _missing__NodeList(self, node): def is_constant_expr(self, node): """Is this a compile-time constant?""" node_name = node.__class__.__name__ - if node_name in ["NameConstant", "Num"]: + if node_name in ["Constant", "NameConstant", "Num"]: return "Num" elif node_name == "Name": if node.id in ["True", "False", "None", "__debug__"]: From 04ff188349df84f73167108314e9698059830279 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 5 Oct 2018 10:39:28 -0400 Subject: [PATCH 281/952] Finally jumps back to exiting lines In Python 3.8, when a finally clause is run because a line in the try block is exiting the block, the exiting line is visited again after the finally block. --- coverage/env.py | 10 +++++++ coverage/parser.py | 47 +++++++++++++++++++++++------ tests/test_arcs.py | 70 +++++++++++++++++++++++++++++++++----------- tests/test_parser.py | 69 +++++++++++++++++++++++++++++++------------ 4 files changed, 151 insertions(+), 45 deletions(-) diff --git a/coverage/env.py b/coverage/env.py index e35d026b9..aa8bb8f6d 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -24,6 +24,16 @@ PY2 = PYVERSION < (3, 0) PY3 = PYVERSION >= (3, 0) +# Python behavior +class PYBEHAVIOR(object): + """Flags indicating this Python's behavior.""" + + # When a break/continue/return statement in a try block jumps to a finally + # block, does the finally block do the break/continue/return (pre-3.8), or + # does the finally jump back to the break/continue/return (3.8) to do the + # work? + finally_jumps_back = (PYVERSION >= (3, 8)) + # Coverage.py specifics. # Are we using the C-implemented trace function? diff --git a/coverage/parser.py b/coverage/parser.py index 2eae4eb51..5ffcad8cb 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -505,6 +505,10 @@ def __init__(self, body): self.lineno = body[0].lineno +# TODO: some add_arcs methods here don't add arcs, they return them. Rename them. +# TODO: the cause messages have too many commas. +# TODO: Shouldn't the cause messages join with "and" instead of "or"? + class AstArcAnalyzer(object): """Analyze source text with an AST to find executable code paths.""" @@ -546,6 +550,7 @@ def analyze(self): if code_object_handler is not None: code_object_handler(node) + @contract(start=int, end=int) def add_arc(self, start, end, smsg=None, emsg=None): """Add an arc, including message fragments to use if it is missing.""" if self.debug: # pragma: debugging @@ -970,21 +975,45 @@ def _handle__Try(self, node): final_exits = self.add_body_arcs(node.finalbody, prev_starts=final_from) if try_block.break_from: - self.process_break_exits( - self._combine_finally_starts(try_block.break_from, final_exits) - ) + if env.PYBEHAVIOR.finally_jumps_back: + for break_line in try_block.break_from: + lineno = break_line.lineno + cause = break_line.cause.format(lineno=lineno) + for final_exit in final_exits: + self.add_arc(final_exit.lineno, lineno, cause) + breaks = try_block.break_from + else: + breaks = self._combine_finally_starts(try_block.break_from, final_exits) + self.process_break_exits(breaks) + if try_block.continue_from: - self.process_continue_exits( - self._combine_finally_starts(try_block.continue_from, final_exits) - ) + if env.PYBEHAVIOR.finally_jumps_back: + for continue_line in try_block.continue_from: + lineno = continue_line.lineno + cause = continue_line.cause.format(lineno=lineno) + for final_exit in final_exits: + self.add_arc(final_exit.lineno, lineno, cause) + continues = try_block.continue_from + else: + continues = self._combine_finally_starts(try_block.continue_from, final_exits) + self.process_continue_exits(continues) + if try_block.raise_from: self.process_raise_exits( self._combine_finally_starts(try_block.raise_from, final_exits) ) + if try_block.return_from: - self.process_return_exits( - self._combine_finally_starts(try_block.return_from, final_exits) - ) + if env.PYBEHAVIOR.finally_jumps_back: + for return_line in try_block.return_from: + lineno = return_line.lineno + cause = return_line.cause.format(lineno=lineno) + for final_exit in final_exits: + self.add_arc(final_exit.lineno, lineno, cause) + returns = try_block.return_from + else: + returns = self._combine_finally_starts(try_block.return_from, final_exits) + self.process_return_exits(returns) if exits: # The finally clause's exits are only exits for the try block diff --git a/tests/test_arcs.py b/tests/test_arcs.py index c86147b76..f20c8cadb 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -623,6 +623,10 @@ def test_finally_in_loop(self): def test_break_through_finally(self): + if env.PYBEHAVIOR.finally_jumps_back: + arcz = ".1 12 23 34 3D 45 56 67 68 7A 7D 8A A3 A7 BC CD D." + else: + arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D." self.check_coverage("""\ a, c, d, i = 1, 1, 1, 99 try: @@ -638,11 +642,15 @@ def test_break_through_finally(self): d = 12 # C assert a == 5 and c == 10 and d == 1 # D """, - arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AD BC CD D.", + arcz=arcz, arcz_missing="3D BC CD", ) def test_continue_through_finally(self): + if env.PYBEHAVIOR.finally_jumps_back: + arcz = ".1 12 23 34 3D 45 56 67 68 73 7A 8A A3 A7 BC CD D." + else: + arcz = ".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D." self.check_coverage("""\ a, b, c, d, i = 1, 1, 1, 1, 99 try: @@ -658,7 +666,7 @@ def test_continue_through_finally(self): d = 12 # C assert (a, b, c, d) == (5, 8, 10, 1) # D """, - arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 BC CD D.", + arcz=arcz, arcz_missing="BC CD", ) @@ -794,6 +802,10 @@ def test_multiple_except_clauses(self): ) def test_return_finally(self): + if env.PYBEHAVIOR.finally_jumps_back: + arcz = ".1 12 29 9A AB BC C-1 -23 34 45 5-2 57 75 38 8-2" + else: + arcz = ".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2" self.check_coverage("""\ a = [1] def check_token(data): @@ -808,10 +820,26 @@ def check_token(data): 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", + arcz=arcz, ) def test_except_jump_finally(self): + if env.PYBEHAVIOR.finally_jumps_back: + arcz = ( + ".1 1Q QR RS ST TU U. " + ".2 23 34 45 56 4O 6L " + "78 89 9A AL LA AO 8B BC CD DL LD D4 BE EF FG GL LG G. EH HI IJ JL HL " + "L4 LM " + "MN NO O." + ) + else: + arcz = ( + ".1 1Q QR RS ST TU U. " + ".2 23 34 45 56 4O 6L " + "78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL " + "LO L4 L. LM " + "MN NO O." + ) self.check_coverage("""\ def func(x): a = f = g = 2 @@ -842,18 +870,30 @@ def func(x): assert func('continue') == (12, 21, 2, 3) # R assert func('return') == (15, 2, 2, 0) # S assert func('raise') == (18, 21, 23, 0) # T + assert func('other') == (2, 21, 2, 3) # U 30 """, - arcz= - ".1 1Q QR RS ST T. " - ".2 23 34 45 56 4O 6L " - "78 89 9A AL 8B BC CD DL BE EF FG GL EH HI IJ JL HL " - "LO L4 L. LM " - "MN NO O.", - arcz_missing="6L HL", + arcz=arcz, + arcz_missing="6L", arcz_unpredicted="67", ) def test_else_jump_finally(self): + if env.PYBEHAVIOR.finally_jumps_back: + arcz = ( + ".1 1S ST TU UV VW W. " + ".2 23 34 45 56 6A 78 8N 4Q " + "AB BC CN NC CQ AD DE EF FN NF F4 DG GH HI IN NI I. GJ JK KL LN JN " + "N4 NO " + "OP PQ Q." + ) + else: + arcz = ( + ".1 1S ST TU UV VW W. " + ".2 23 34 45 56 6A 78 8N 4Q " + "AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN " + "N4 NQ N. NO " + "OP PQ Q." + ) self.check_coverage("""\ def func(x): a = f = g = 2 @@ -886,14 +926,10 @@ def func(x): assert func('continue') == (14, 23, 2, 3) # T assert func('return') == (17, 2, 2, 0) # U assert func('raise') == (20, 23, 25, 0) # V + assert func('other') == (2, 23, 2, 3) # W 32 """, - arcz= - ".1 1S ST TU UV V. " - ".2 23 34 45 56 6A 78 8N 4Q " - "AB BC CN AD DE EF FN DG GH HI IN GJ JK KL LN JN " - "NQ N4 N. NO " - "OP PQ Q.", - arcz_missing="78 8N JN", + arcz=arcz, + arcz_missing="78 8N", arcz_unpredicted="", ) diff --git a/tests/test_parser.py b/tests/test_parser.py index c2d70ee58..6340a44bf 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -326,26 +326,57 @@ def function(): this_thing(16) that_thing(17) """) - self.assertEqual( - parser.missing_arc_description(16, 17), - "line 16 didn't jump to line 17, because the break on line 5 wasn't executed" - ) - self.assertEqual( - parser.missing_arc_description(16, 2), - "line 16 didn't jump to line 2, " - "because the continue on line 8 wasn't executed" + if env.PYBEHAVIOR.finally_jumps_back: + self.assertEqual( + parser.missing_arc_description(16, 5), + "line 16 didn't jump to line 5, because the break on line 5 wasn't executed" + ) + self.assertEqual( + parser.missing_arc_description(5, 17), + "line 5 didn't jump to line 17, because the break on line 5 wasn't executed" + ) + self.assertEqual( + parser.missing_arc_description(16, 8), + "line 16 didn't jump to line 8, because the continue on line 8 wasn't executed" + ) + self.assertEqual( + parser.missing_arc_description(8, 2), + "line 8 didn't jump to line 2, because the continue on line 8 wasn't executed" + ) + self.assertEqual( + parser.missing_arc_description(16, 12), + "line 16 didn't jump to line 12, because the return on line 12 wasn't executed" + ) + self.assertEqual( + parser.missing_arc_description(12, -1), + "line 12 didn't return from function 'function', " + "because the return on line 12 wasn't executed" + ) + self.assertEqual( + parser.missing_arc_description(16, -1), + "line 16 didn't except from function 'function', " + "because the raise on line 14 wasn't executed" + ) + else: + self.assertEqual( + parser.missing_arc_description(16, 17), + "line 16 didn't jump to line 17, because the break on line 5 wasn't executed" + ) + self.assertEqual( + parser.missing_arc_description(16, 2), + "line 16 didn't jump to line 2, " + "because the continue on line 8 wasn't executed" + " or " + "the continue on line 10 wasn't executed" + ) + self.assertEqual( + parser.missing_arc_description(16, -1), + "line 16 didn't except from function 'function', " + "because the raise on line 14 wasn't executed" " or " - "the continue on line 10 wasn't executed" - ) - self.assertEqual( - parser.missing_arc_description(16, -1), - "line 16 didn't except from function 'function', " - "because the raise on line 14 wasn't executed" - " or " - "line 16 didn't return from function 'function', " - "because the return on line 12 wasn't executed" - ) - + "line 16 didn't return from function 'function', " + "because the return on line 12 wasn't executed" + ) def test_missing_arc_descriptions_bug460(self): parser = self.parse_text(u"""\ x = 1 From d62fbd378dde67612546076c8c804dfdb595a15e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Oct 2018 10:46:14 -0400 Subject: [PATCH 282/952] Add words about 3.8 --- CHANGES.rst | 6 ++++++ README.rst | 2 +- doc/index.rst | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b449797b7..f38ffbace 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -41,6 +41,12 @@ Unreleased .. _issue 170: https://github.com/nedbat/coveragepy/issues/170 +- Tentative support for Python 3.8, which has not yet released an alpha. Fixes + `issue 707` and `issue 714`_. + +.. _issue 707: https://github.com/nedbat/coveragepy/issues/707 +.. _issue 714: https://github.com/nedbat/coveragepy/issues/714 + - Environment variable substitution in configuration files now supports two syntaxes for controlling the behavior of undefined variables: if ``VARNAME`` is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default diff --git a/README.rst b/README.rst index c0df1d18f..a2eacf4dd 100644 --- a/README.rst +++ b/README.rst @@ -35,7 +35,7 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: * CPython 2.7. -* CPython 3.4 through 3.7. +* CPython 3.4 through 3.8. * PyPy2 6.0 and PyPy3 6.0. * Jython 2.7.1, though not for reporting. * IronPython 2.7.7, though not for reporting. diff --git a/doc/index.rst b/doc/index.rst index fc9f31c14..4aa7ee3ca 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -80,7 +80,7 @@ not. The latest version is coverage.py 5.0a2, released September 3rd 2018. It is supported on: - * Python versions 2.7, 3.4, 3.5, 3.6, and 3.7. + * Python versions 2.7, 3.4, 3.5, 3.6, 3.7, and pre-alpha 3.8. * PyPy2 6.0 and PyPy3 6.0. From eec5c5cdb669e632c556131c48df1055142072d8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Oct 2018 16:30:12 -0400 Subject: [PATCH 283/952] Prep for 5.0a3 --- CHANGES.rst | 16 +++++++++------- README.rst | 16 +--------------- doc/conf.py | 2 +- doc/index.rst | 51 +-------------------------------------------------- 4 files changed, 12 insertions(+), 73 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f38ffbace..c2ccf674a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,8 +14,10 @@ Change history for Coverage.py .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- -Unreleased ----------- +.. _changes_50a3: + +Version 5.0a3 --- 2018-10-06 +---------------------------- - Context support: static contexts let you specify a label for a coverage run, which is recorded in the data, and retained when you combine files. See @@ -41,17 +43,17 @@ Unreleased .. _issue 170: https://github.com/nedbat/coveragepy/issues/170 +- Environment variable substitution in configuration files now supports two + syntaxes for controlling the behavior of undefined variables: if ``VARNAME`` + is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default + value}`` will use "default value". + - Tentative support for Python 3.8, which has not yet released an alpha. Fixes `issue 707` and `issue 714`_. .. _issue 707: https://github.com/nedbat/coveragepy/issues/707 .. _issue 714: https://github.com/nedbat/coveragepy/issues/714 -- Environment variable substitution in configuration files now supports two - syntaxes for controlling the behavior of undefined variables: if ``VARNAME`` - is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default - value}`` will use "default value". - .. _changes_50a2: diff --git a/README.rst b/README.rst index a2eacf4dd..fe37f2dcc 100644 --- a/README.rst +++ b/README.rst @@ -35,7 +35,7 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: * CPython 2.7. -* CPython 3.4 through 3.8. +* CPython 3.4 through pre-alpha 3.8. * PyPy2 6.0 and PyPy3 6.0. * Jython 2.7.1, though not for reporting. * IronPython 2.7.7, though not for reporting. @@ -50,20 +50,6 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on **New in 5.0:** SQLite data storage, contexts, dropped support for Python 2.6 and 3.3. -New in 4.5: Configurator plug-ins. - -New in 4.4: Suppressable warnings, continuous coverage measurement. - -New in 4.3: HTML ``--skip-covered``, sys.excepthook support, tox.ini -support. - -New in 4.2: better support for multiprocessing and combining data. - -New in 4.1: much-improved branch coverage. - -New in 4.0: ``--concurrency``, plugins for non-Python files, setup.cfg -support, --skip-covered, HTML filtering, and more than 50 issues closed. - Getting Started --------------- diff --git a/doc/conf.py b/doc/conf.py index a87047ebe..4c37c29d2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -58,7 +58,7 @@ # The short X.Y version. version = '5.0' # CHANGEME # The full version, including alpha/beta/rc tags. -release = '5.0a2' # CHANGEME +release = '5.0a3' # CHANGEME # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/index.rst b/doc/index.rst index 4aa7ee3ca..3239e105f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -5,55 +5,6 @@ Coverage.py =========== -.. :history: 20090524T134300, brand new docs. -.. :history: 20090613T164000, final touches for 3.0 -.. :history: 20090618T195900, minor tweaks -.. :history: 20090707T205200, changes for 3.0.1 -.. :history: 20090913T084400, new command line syntax -.. :history: 20091004T211900, version 3.1 -.. :history: 20091127T155100, version 3.2 -.. :history: 20091205T161429, version 3.2 for real. -.. :history: 20100224T204700, version 3.3 -.. :history: 20100306T181500, version 3.3.1 -.. :history: 20100725T211700, updated for 3.4. -.. :history: 20100820T151500, updated for 3.4b1. -.. :history: 20100906T134700, updated for 3.4b2. -.. :history: 20100919T163500, updated for 3.4 release. -.. :history: 20110213T081200, claim true 3.2 compatibility. -.. :history: 20110604T114800, update for 3.5b1 -.. :history: 20110629T082300, update for 3.5 -.. :history: 20110827T221800, update for 3.5.1b1 -.. :history: 20110923T081800, update for 3.5.1 -.. :history: 20120429T162100, updated for 3.5.2b1 -.. :history: 20120503T233800, updated for 3.5.2 -.. :history: 20120929T093500, updated for 3.5.3 -.. :history: 20121117T094900, Change from easy_install to pip. -.. :history: 20121128T203700, Updated for 3.6b1. -.. :history: 20121223T180600, Updated for 3.6b2. -.. :history: 20121229T112300, Updated for 3.6b3. -.. :history: 20130105T174000, Updated for 3.6 -.. :history: 20131005T210000, Updated for 3.7 -.. :history: 20131212T213300, Updated for 3.7.1 -.. :history: 20140924T073000, Updated for 4.0a1 -.. :history: 20150124T023900, Updated for 4.0a4 -.. :history: 20150216T201000, Updated for 4.0a5 -.. :history: 20150802T160200, Updated for 4.0b1 -.. :history: 20150822T092900, Updated for 4.0b2 -.. :history: 20150918T072700, Updated for 4.0 -.. :history: 20151013T103200, Updated for 4.0.1 -.. :history: 20151104T050900, updated for 4.0.2 -.. :history: 20151124T065900, updated for 4.0.3 -.. :history: 20160110T125900, updated for 4.1b1 -.. :history: 20160123T171300, updated for 4.1b2 -.. :history: 20160510T125300, updated for 4.1b3 -.. :history: 20160521T074500, updated for 4.1 -.. :history: 20160726T161300, updated for 4.2 -.. :history: 20161226T160400, updated for 4.3 -.. :history: 20170116T180100, updated for 4.3.2 -.. :history: 20180203T130300, updated for 4.5 -.. :history: 20180210T125300, updated for 4.5.1 - - Coverage.py is a tool for measuring code coverage of Python programs. It monitors your program, noting which parts of the code have been executed, then analyzes the source to identify code that could have been executed but was not. @@ -77,7 +28,7 @@ not. .. ifconfig:: prerelease - The latest version is coverage.py 5.0a2, released September 3rd 2018. + The latest version is coverage.py 5.0a3, released October 6th 2018. It is supported on: * Python versions 2.7, 3.4, 3.5, 3.6, 3.7, and pre-alpha 3.8. From fe390493cf99e179f73d4c0547c7e4503b099cb2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Oct 2018 17:32:17 -0400 Subject: [PATCH 284/952] Diagnose an appveyor problem --- tests/test_context.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_context.py b/tests/test_context.py index b237af065..ee5441ee5 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -152,6 +152,8 @@ def test_dynamic_alone(self): data = cov.get_data() fname = os.path.abspath("two_tests.py") + print(fname) + print(data.measured_files()) self.assertCountEqual(data.measured_contexts(), ["", "test_one", "test_two"]) self.assertCountEqual(data.lines(fname, ""), self.OUTER_LINES) self.assertCountEqual(data.lines(fname, "test_one"), self.TEST_ONE_LINES) @@ -165,6 +167,8 @@ def test_static_and_dynamic(self): data = cov.get_data() fname = os.path.abspath("two_tests.py") + print(fname) + print(data.measured_files()) self.assertCountEqual(data.measured_contexts(), ["stat", "stat:test_one", "stat:test_two"]) self.assertCountEqual(data.lines(fname, "stat"), self.OUTER_LINES) self.assertCountEqual(data.lines(fname, "stat:test_one"), self.TEST_ONE_LINES) From 51aacdbef7b0adf49d2487cc12f0876fc57d49ef Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Oct 2018 17:54:24 -0400 Subject: [PATCH 285/952] Look up names so we don't care about case --- tests/test_context.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/test_context.py b/tests/test_context.py index ee5441ee5..efc054182 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -151,9 +151,8 @@ def test_dynamic_alone(self): self.start_import_stop(cov, "two_tests") data = cov.get_data() - fname = os.path.abspath("two_tests.py") - print(fname) - print(data.measured_files()) + full_names = {os.path.basename(f): f for f in data.measured_files()} + fname = full_names["two_tests.py"] self.assertCountEqual(data.measured_contexts(), ["", "test_one", "test_two"]) self.assertCountEqual(data.lines(fname, ""), self.OUTER_LINES) self.assertCountEqual(data.lines(fname, "test_one"), self.TEST_ONE_LINES) @@ -166,9 +165,8 @@ def test_static_and_dynamic(self): self.start_import_stop(cov, "two_tests") data = cov.get_data() - fname = os.path.abspath("two_tests.py") - print(fname) - print(data.measured_files()) + full_names = {os.path.basename(f): f for f in data.measured_files()} + fname = full_names["two_tests.py"] self.assertCountEqual(data.measured_contexts(), ["stat", "stat:test_one", "stat:test_two"]) self.assertCountEqual(data.lines(fname, "stat"), self.OUTER_LINES) self.assertCountEqual(data.lines(fname, "stat:test_one"), self.TEST_ONE_LINES) From c6559f4bcb6f1edd1aa486a289f3186d90552dde Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Oct 2018 19:20:58 -0400 Subject: [PATCH 286/952] Version bump --- CHANGES.rst | 6 ++++++ coverage/version.py | 2 +- howto.txt | 6 +----- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index c2ccf674a..2ca34b11d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -14,6 +14,12 @@ Change history for Coverage.py .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- +Unreleased +---------- + +Nothing yet. + + .. _changes_50a3: Version 5.0a3 --- 2018-10-06 diff --git a/coverage/version.py b/coverage/version.py index b56980376..4a3bff401 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 0, 0, 'alpha', 3) +version_info = (5, 0, 0, 'alpha', 4) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/howto.txt b/howto.txt index ae20d0b97..ef36279ac 100644 --- a/howto.txt +++ b/howto.txt @@ -80,11 +80,7 @@ - wait for the new tag build to finish successfully. - visit https://readthedocs.org/dashboard/coverage/versions/ - change the default version to the new version -- Update bitbucket: - - Issue tracker should get new version number in picker. - # Note: don't delete old version numbers: it marks changes on the tickets - # with that number. -- Visit the fixed issues on bitbucket and mention the version it was fixed in. +- Visit the fixed issues on GitHub and mention the version it was fixed in. - Announce on coveragepy-announce@googlegroups.com . - Announce on TIP. From df6abbe9eef2e7884654d87be0bc1591d4f9cf6d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Oct 2018 21:08:39 -0400 Subject: [PATCH 287/952] Update dependencies --- doc/requirements.pip | 8 +++----- requirements/ci.pip | 2 +- requirements/dev.pip | 8 ++++---- requirements/pytest.pip | 4 ++-- requirements/tox.pip | 5 ++++- requirements/wheel.pip | 7 +++++-- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/doc/requirements.pip b/doc/requirements.pip index 71b171348..8e3eed525 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -2,10 +2,8 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ +doc8==0.8.0 pyenchant==2.0.0 -sphinx==1.7.6 +sphinx==1.8.1 sphinxcontrib-spelling==4.2.0 -sphinx_rtd_theme==0.4.1 - -# A version of doc8 with a -q flag. -git+https://github.com/nedbat/doc8.git#egg=doc8==0.0 +sphinx_rtd_theme==0.4.2 diff --git a/requirements/ci.pip b/requirements/ci.pip index cb94d7379..fe17a4c41 100644 --- a/requirements/ci.pip +++ b/requirements/ci.pip @@ -5,4 +5,4 @@ -r tox.pip -r pytest.pip -r wheel.pip -tox-travis==0.10 +tox-travis==0.11 diff --git a/requirements/dev.pip b/requirements/dev.pip index b497deb80..107d00e4f 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -4,7 +4,7 @@ # Requirements for doing local development work on coverage.py. # https://requires.io/github/nedbat/coveragepy/requirements/ -pip==18.0.0 +pip==18.1 virtualenv==16.0.0 # PyPI requirements for running tests. @@ -12,11 +12,11 @@ virtualenv==16.0.0 -r pytest.pip # for linting. -greenlet==0.4.14 +greenlet==0.4.15 pylint==1.9.2 check-manifest==0.37 -readme_renderer==21.0 +readme_renderer==22.0 # for kitting. requests==2.19.1 -twine==1.11.0 +twine==1.12.1 diff --git a/requirements/pytest.pip b/requirements/pytest.pip index a946a59af..fe7729de8 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -3,9 +3,9 @@ # The pytest specifics used by coverage.py -pytest==3.8.0 +pytest==3.8.2 pluggy>=0.7 # pytest needs this, but pip doesn't understand -pytest-xdist==1.23.0 +pytest-xdist==1.23.2 flaky==3.4.0 mock==2.0.0 PyContracts==1.8.3 diff --git a/requirements/tox.pip b/requirements/tox.pip index 07d889e6f..86a8f8df0 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -1,4 +1,7 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + # The version of tox used by coverage.py -tox==3.2.1 +tox==3.4.0 # Adds env recreation on requirements file changes. tox-battery==0.5.1 diff --git a/requirements/wheel.pip b/requirements/wheel.pip index 30ac5455c..2ef8a0022 100644 --- a/requirements/wheel.pip +++ b/requirements/wheel.pip @@ -1,3 +1,6 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + # Things needed to make wheels for coverage.py -setuptools==40.0.0 -wheel==0.31.1 +setuptools==40.4.3 +wheel==0.32.1 From b892fd503ae46f7f98db172e77d8f84cc63289a5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 7 Oct 2018 11:18:09 -0400 Subject: [PATCH 288/952] All-in on contexts for ourselves --- igor.py | 2 ++ metacov.ini | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/igor.py b/igor.py index 4731b8c51..36025c44f 100644 --- a/igor.py +++ b/igor.py @@ -118,6 +118,7 @@ def run_tests_with_coverage(tracer, *runner_args): version += "_%s%s" % sys.pypy_version_info[:2] suffix = "%s%s_%s_%s" % (impl, version, tracer, platform.platform()) + os.environ['COVERAGE_PYVERSION'] = version os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov."+suffix) import coverage @@ -159,6 +160,7 @@ def do_combine_html(): """Combine data from a meta-coverage run, and make the HTML and XML reports.""" import coverage os.environ['COVERAGE_HOME'] = os.getcwd() + os.environ['COVERAGE_PYVERSION'] = '' os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov") cov = coverage.Coverage(config_file="metacov.ini") cov.load() diff --git a/metacov.ini b/metacov.ini index 50ea524df..fc85f26e3 100644 --- a/metacov.ini +++ b/metacov.ini @@ -4,11 +4,13 @@ # Settings to use when using coverage.py to measure itself. [run] branch = true -data_file = $COVERAGE_METAFILE +data_file = ${COVERAGE_METAFILE?} parallel = true source = - $COVERAGE_HOME/coverage - $COVERAGE_HOME/tests + ${COVERAGE_HOME?}/coverage + ${COVERAGE_HOME?}/tests +context = ${COVERAGE_PYVERSION?} +dynamic_context = test_function [report] # We set a different pragma so our code won't be confused with test code. From 32e8008a5d99b3f5bfe6a6a220eec5c4f730ba43 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 7 Oct 2018 12:09:01 -0400 Subject: [PATCH 289/952] Finish updating requirements --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 55819e3e9..6f2881d41 100644 --- a/tox.ini +++ b/tox.ini @@ -13,12 +13,12 @@ deps = # Check here for what might be out of date: # https://requires.io/github/nedbat/coveragepy/requirements/ -rrequirements/pytest.pip - pip==18.0 - setuptools==40.0.0 + pip==18.1 + setuptools==40.4.3 # gevent 1.3 causes a failure: https://bitbucket.org/ned/coveragepy/issues/663/gevent-132-on-windows-fails py{27,34,35,36}: gevent==1.2.2 py{27,34,35,36,37,38}: eventlet==0.24.1 - py{27,34,35,36,37,38}: greenlet==0.4.14 + py{27,34,35,36,37,38}: greenlet==0.4.15 # Windows can't update the pip version with pip running, so use Python # to install things. From a629ecb8abe816ffc50a435eb386f075754908da Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 7 Oct 2018 12:09:18 -0400 Subject: [PATCH 290/952] Sphinx complains about app.info now, and what's wrong with print? --- doc/conf.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index 4c37c29d2..757bf64eb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -186,13 +186,9 @@ # into the class docs. autoclass_content = "class" - - - - prerelease = bool(max(release).isalpha()) def setup(app): app.add_stylesheet('coverage.css') app.add_config_value('prerelease', False, 'env') - app.info("** Prerelease = %r" % prerelease) + print("** Prerelease = %r" % prerelease) From 8f41c9714de5a75e031b4ea8a744f9d7860e8c05 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 13 Oct 2018 10:01:08 -0400 Subject: [PATCH 291/952] Debugging improvements --- coverage/debug.py | 8 ++++---- coverage/sqldata.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index 9077a3af9..ce22d7253 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -177,6 +177,7 @@ def __repr__(self): show_attrs = ( (k, v) for k, v in self.__dict__.items() if getattr(v, "show_repr_attr", True) + and not callable(v) ) return "<{klass} @0x{id:x} {attrs}>".format( klass=self.__class__.__name__, @@ -229,11 +230,10 @@ def __init__(self, outfile, show_process, filters): if self.show_process: self.filters.append(CwdTracker().filter) - cmd = " ".join(getattr(sys, 'argv', ['???'])) - self.write("New process: executable: %s\n" % (sys.executable,)) - self.write("New process: cmd: %s\n" % (cmd,)) + self.write("New process: executable: %r\n" % (sys.executable,)) + self.write("New process: cmd: %r\n" % (getattr(sys, 'argv', None),)) if hasattr(os, 'getppid'): - self.write("New process: pid: %s, parent pid: %s\n" % (os.getpid(), os.getppid())) + self.write("New process: pid: %r, parent pid: %r\n" % (os.getpid(), os.getppid())) SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 82f37a9bf..48d2d6701 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -489,10 +489,10 @@ def run_infos(self): class Sqlite(SimpleReprMixin): def __init__(self, filename, debug): self.debug = debug if (debug and debug.should('sql')) else None - if self.debug: - self.debug.write("Connecting to {!r}".format(filename)) self.filename = filename self.nest = 0 + if self.debug: + self.debug.write("Connecting to {!r}".format(filename)) def connect(self): # SQLite on Windows on py2 won't open a file if the filename argument From 3569f921992e10036281d469f22cf4a2e06a6c45 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 13 Oct 2018 10:17:10 -0400 Subject: [PATCH 292/952] Fewer conditionals for debug output --- coverage/data.py | 25 +++++++++++++------------ coverage/debug.py | 6 ++++++ coverage/sqldata.py | 22 +++++++++++----------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index e6d56d84a..4a996e687 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -15,6 +15,7 @@ from coverage import env from coverage.backward import iitems, string_class +from coverage.debug import NoDebugging from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone, isolate_module @@ -133,7 +134,7 @@ def __init__(self, basename=None, suffix=None, warn=None, debug=None): """ self._warn = warn - self._debug = debug + self._debug = debug or NoDebugging() self.filename = os.path.abspath(basename or ".coverage") self.suffix = suffix @@ -293,7 +294,7 @@ def _read_fileobj(self, file_obj): def _read_file(self, filename): """Read the coverage data from `filename` into this object.""" - if self._debug and self._debug.should('dataio'): + if self._debug.should('dataio'): self._debug.write("Reading data from %r" % (filename,)) try: with self._open_for_reading(filename) as f: @@ -338,7 +339,7 @@ def add_lines(self, line_data): { filename: { lineno: None, ... }, ...} """ - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Adding lines: %d files, %d lines total" % ( len(line_data), sum(len(lines) for lines in line_data.values()) )) @@ -364,7 +365,7 @@ def add_arcs(self, arc_data): { filename: { (l1,l2): None, ... }, ...} """ - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Adding arcs: %d files, %d arcs total" % ( len(arc_data), sum(len(arcs) for arcs in arc_data.values()) )) @@ -388,7 +389,7 @@ def add_file_tracers(self, file_tracers): `file_tracers` is { filename: plugin_name, ... } """ - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Adding file tracers: %d files" % (len(file_tracers),)) existing_files = self._arcs or self._lines or {} @@ -416,7 +417,7 @@ def add_run_info(self, **kwargs): but repeated keywords overwrite each other. """ - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Adding run info: %r" % (kwargs,)) if not self._runs: self._runs = [{}] @@ -429,7 +430,7 @@ def touch_file(self, filename, plugin_name=""): `plugin_name` is the name of the plugin resposible for this file. It is used to associate the right filereporter, etc. """ - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Touching %r" % (filename,)) if not self._has_arcs() and not self._has_lines(): raise CoverageException("Can't touch files in an empty CoverageData") @@ -489,7 +490,7 @@ def _write_fileobj(self, file_obj): def _write_file(self, filename): """Write the coverage data to `filename`.""" - if self._debug and self._debug.should('dataio'): + if self._debug.should('dataio'): self._debug.write("Writing data to %r" % (filename,)) with open(filename, 'w') as fdata: self._write_fileobj(fdata) @@ -507,7 +508,7 @@ def erase(self, parallel=False): self._runs = [] self._validate() - if self._debug and self._debug.should('dataio'): + if self._debug.should('dataio'): self._debug.write("Erasing data file %r" % (self.filename,)) file_be_gone(self.filename) if parallel: @@ -515,7 +516,7 @@ def erase(self, parallel=False): localdot = local + '.*' pattern = os.path.join(os.path.abspath(data_dir), localdot) for filename in glob.glob(pattern): - if self._debug and self._debug.should('dataio'): + if self._debug.should('dataio'): self._debug.write("Erasing parallel data file %r" % (filename,)) file_be_gone(filename) @@ -727,7 +728,7 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): files_combined = 0 for f in files_to_combine: - if data._debug and data._debug.should('dataio'): + if data._debug.should('dataio'): data._debug.write("Combining data file %r" % (f,)) try: new_data = CoverageData(f, debug=data._debug) @@ -740,7 +741,7 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): else: data.update(new_data, aliases=aliases) files_combined += 1 - if data._debug and data._debug.should('dataio'): + if data._debug.should('dataio'): data._debug.write("Deleting combined data file %r" % (f,)) file_be_gone(f) diff --git a/coverage/debug.py b/coverage/debug.py index ce22d7253..2c5c6aaf9 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -91,6 +91,12 @@ def get_output(self): return self.raw_output.getvalue() +class NoDebugging(object): + """A replacement for DebugControl that will never try to do anything.""" + def should(self, option): + return False + + def info_header(label): """Make a nice header string.""" return "--{0:-<60s}".format(" "+label+" ") diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 48d2d6701..6c12d6814 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -19,7 +19,7 @@ from coverage.backward import iitems from coverage.data import filename_suffix -from coverage.debug import SimpleReprMixin +from coverage.debug import NoDebugging, SimpleReprMixin from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone @@ -80,7 +80,7 @@ def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._basename = os.path.abspath(basename or ".coverage") self._suffix = suffix self._warn = warn - self._debug = debug + self._debug = debug or NoDebugging() self._choose_filename() self._file_map = {} @@ -110,7 +110,7 @@ def _reset(self): self._current_context_id = None def _create_db(self): - if self._debug and self._debug.should('dataio'): + if self._debug.should('dataio'): self._debug.write("Creating data file {!r}".format(self.filename)) self._db = Sqlite(self.filename, self._debug) with self._db: @@ -125,7 +125,7 @@ def _create_db(self): ) def _open_db(self): - if self._debug and self._debug.should('dataio'): + if self._debug.should('dataio'): self._debug.write("Opening data file {!r}".format(self.filename)) self._db = Sqlite(self.filename, self._debug) with self._db: @@ -203,7 +203,7 @@ def _context_id(self, context): def set_context(self, context): """Set the current context for future `add_lines` etc.""" - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Setting context: %r" % (context,)) self._start_using() context = context or "" @@ -223,7 +223,7 @@ def add_lines(self, line_data): { filename: { lineno: None, ... }, ...} """ - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Adding lines: %d files, %d lines total" % ( len(line_data), sum(len(lines) for lines in line_data.values()) )) @@ -248,7 +248,7 @@ def add_arcs(self, arc_data): { filename: { (l1,l2): None, ... }, ...} """ - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Adding arcs: %d files, %d arcs total" % ( len(arc_data), sum(len(arcs) for arcs in arc_data.values()) )) @@ -312,7 +312,7 @@ def touch_file(self, filename, plugin_name=""): to associate the right filereporter, etc. """ self._start_using() - if self._debug and self._debug.should('dataop'): + if self._debug.should('dataop'): self._debug.write("Touching %r" % (filename,)) if not self._has_arcs and not self._has_lines: raise CoverageException("Can't touch files in an empty CoverageSqliteData") @@ -384,7 +384,7 @@ def erase(self, parallel=False): """ self._reset() - if self._debug and self._debug.should('dataio'): + if self._debug.should('dataio'): self._debug.write("Erasing data file {!r}".format(self.filename)) file_be_gone(self.filename) if parallel: @@ -392,7 +392,7 @@ def erase(self, parallel=False): localdot = local + '.*' pattern = os.path.join(os.path.abspath(data_dir), localdot) for filename in glob.glob(pattern): - if self._debug and self._debug.should('dataio'): + if self._debug.should('dataio'): self._debug.write("Erasing parallel data file {!r}".format(filename)) file_be_gone(filename) @@ -488,7 +488,7 @@ def run_infos(self): class Sqlite(SimpleReprMixin): def __init__(self, filename, debug): - self.debug = debug if (debug and debug.should('sql')) else None + self.debug = debug if debug.should('sql') else None self.filename = filename self.nest = 0 if self.debug: From 1fbfb4077380530b01306e1e052b1d34a0930ec9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 13 Oct 2018 10:41:33 -0400 Subject: [PATCH 293/952] Oops, we were always measuring coverage on Travis --- igor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/igor.py b/igor.py index 36025c44f..5d80ebe35 100644 --- a/igor.py +++ b/igor.py @@ -179,7 +179,7 @@ def do_test_with_tracer(tracer, *runner_args): return None os.environ["COVERAGE_TEST_TRACER"] = tracer - if os.environ.get("COVERAGE_COVERAGE", ""): + if os.environ.get("COVERAGE_COVERAGE", "no") == "yes": return run_tests_with_coverage(tracer, *runner_args) else: return run_tests(tracer, *runner_args) From 6ec04a713879a30add3f4e44b4d091ff37443a9a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 14 Oct 2018 13:18:10 -0400 Subject: [PATCH 294/952] Not sure how I went for so long without one of these creeping in --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index b952771a1..ad5864979 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ sample_html_beta # Stuff in the ci directory. *.token + +# OS junk +.DS_Store From 7a5e8a4fa39c9840e292120783b69566ac15f1e5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 14 Oct 2018 13:23:01 -0400 Subject: [PATCH 295/952] Missed one file when updating license links --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index bff28ab4b..056c189cf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # This file is for unifying the coding style for different editors and IDEs. # More information at http://EditorConfig.org From 83ccf7ad923f75c736c42bf9117a57a156042c4a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 14 Oct 2018 17:13:46 -0400 Subject: [PATCH 296/952] Have to rethink how to use contexts ourselves Contexts fail when using PYTRACER, and we want to measure coverage under PYTRACER (at least for PyPy), so we need a more sophisticated strategy for how to use contexts. --- igor.py | 2 -- metacov.ini | 2 -- 2 files changed, 4 deletions(-) diff --git a/igor.py b/igor.py index 5d80ebe35..39d751957 100644 --- a/igor.py +++ b/igor.py @@ -118,7 +118,6 @@ def run_tests_with_coverage(tracer, *runner_args): version += "_%s%s" % sys.pypy_version_info[:2] suffix = "%s%s_%s_%s" % (impl, version, tracer, platform.platform()) - os.environ['COVERAGE_PYVERSION'] = version os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov."+suffix) import coverage @@ -160,7 +159,6 @@ def do_combine_html(): """Combine data from a meta-coverage run, and make the HTML and XML reports.""" import coverage os.environ['COVERAGE_HOME'] = os.getcwd() - os.environ['COVERAGE_PYVERSION'] = '' os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov") cov = coverage.Coverage(config_file="metacov.ini") cov.load() diff --git a/metacov.ini b/metacov.ini index fc85f26e3..80c508e9a 100644 --- a/metacov.ini +++ b/metacov.ini @@ -9,8 +9,6 @@ parallel = true source = ${COVERAGE_HOME?}/coverage ${COVERAGE_HOME?}/tests -context = ${COVERAGE_PYVERSION?} -dynamic_context = test_function [report] # We set a different pragma so our code won't be confused with test code. From 4a404ce724280bf9c5f4bcf1e6a5ba76d7499d89 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 14 Oct 2018 17:59:40 -0400 Subject: [PATCH 297/952] Use --source to ensure tests are showing what we want. Travis started having these extra files in coverage reports from our tests: site-packages/pkg_resources/_vendor/six.py site-packages/pkg_resources/extern/__init__.py I don't know why they are being pulled in, but the tests aren't interested in them, and it is throwing off the results. --- tests/test_html.py | 2 +- tests/test_process.py | 8 +++++--- tests/test_summary.py | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/test_html.py b/tests/test_html.py index 955a85375..8c6dc6936 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -343,7 +343,7 @@ def test_dothtml_not_python(self): # Run an "HTML" file self.make_file("innocuous.html", "a = 3") - self.run_command("coverage run innocuous.html") + self.run_command("coverage run --source=. innocuous.html") # Before reporting, change it to be an HTML file. self.make_file("innocuous.html", "

This isn't python at all!

") output = self.run_command("coverage html") diff --git a/tests/test_process.py b/tests/test_process.py index 626c8094a..fe817c5d0 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -312,6 +312,7 @@ def test_combine_with_rc(self): self.make_file(".coveragerc", """\ [run] + source = . parallel = true """) @@ -366,6 +367,7 @@ def test_combine_with_aliases(self): self.make_file(".coveragerc", """\ [run] + source = . parallel = True [paths] @@ -1056,7 +1058,7 @@ def setUp(self): d = 6 e = 7 """) - st, _ = self.run_command_status("coverage run forty_two_plus.py") + st, _ = self.run_command_status("coverage run --source=. forty_two_plus.py") self.assertEqual(st, 0) def test_report_43_is_ok(self): @@ -1107,7 +1109,7 @@ def setUp(self): 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')") - out = self.run_command(u"coverage run h\xe2t.py") + out = self.run_command(u"coverage run --source=. h\xe2t.py") self.assertEqual(out, "accented\n") # The HTML report uses ascii-encoded HTML entities. @@ -1141,7 +1143,7 @@ def test_accented_dot_py(self): def test_accented_directory(self): # Make a file with a non-ascii character in the directory name. self.make_file(u"\xe2/accented.py", "print('accented')") - out = self.run_command(u"coverage run \xe2/accented.py") + out = self.run_command(u"coverage run --source=. \xe2/accented.py") self.assertEqual(out, "accented\n") # The HTML report uses ascii-encoded HTML entities. diff --git a/tests/test_summary.py b/tests/test_summary.py index c3b572d29..51bedb6d4 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -174,7 +174,7 @@ def branch(x): return x branch(1) """) - out = self.run_command("coverage run --branch mybranch.py") + out = self.run_command("coverage run --source=. --branch mybranch.py") self.assertEqual(out, 'x\n') report = self.report_from_command("coverage report") @@ -203,7 +203,7 @@ def missing(x, y): return x missing(0, 1) """) - out = self.run_command("coverage run mymissing.py") + out = self.run_command("coverage run --source=. mymissing.py") self.assertEqual(out, 'y\nz\n') report = self.report_from_command("coverage report --show-missing") @@ -389,7 +389,7 @@ def foo(): pass foo() """) - out = self.run_command("coverage run --branch main.py") + out = self.run_command("coverage run --source=. --branch main.py") self.assertEqual(out, "") report = self.report_from_command("coverage report --skip-covered") @@ -408,7 +408,7 @@ def foo(): pass foo() """) - out = self.run_command("coverage run --branch long_______________filename.py") + out = self.run_command("coverage run --source=. --branch long_______________filename.py") self.assertEqual(out, "") report = self.report_from_command("coverage report --skip-covered") From ae9f040dc4189d73fdd708aa4ba61bf50d7b2a25 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 14 Oct 2018 20:39:46 -0400 Subject: [PATCH 298/952] Defer using the database when calling set_context #716 The collector calls set_context() before any code is run. If we touch the database there, it will get created *very* early. This causes problems with pytest-cov, which will delete those early-created files when erasing data. By deferring the database access until add_lines is called, the data file stays off the disk until the collection is done (or until the context switches), which avoids the problem. --- CHANGES.rst | 4 +++- coverage/sqldata.py | 25 ++++++++++++++----------- tests/test_debug.py | 4 ++-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2ca34b11d..e5f4aeb3b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,7 +17,9 @@ Change history for Coverage.py Unreleased ---------- -Nothing yet. +- Bug fixes to context support: `issue 716`_. + +.. _issue 716: https://github.com/nedbat/coveragepy/issues/716 .. _changes_50a3: diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 6c12d6814..561878faa 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -93,6 +93,7 @@ def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._has_lines = False self._has_arcs = False + self._current_context = None self._current_context_id = None def _choose_filename(self): @@ -205,13 +206,17 @@ def set_context(self, context): """Set the current context for future `add_lines` etc.""" if self._debug.should('dataop'): self._debug.write("Setting context: %r" % (context,)) - self._start_using() - context = context or "" - with self._connect() as con: - row = con.execute("select id from context where context = ?", (context,)).fetchone() - if row is not None: - self._current_context_id = row[0] - else: + self._current_context = context + self._current_context_id = None + + def _set_context_id(self): + """Use the _current_context to set _current_context_id.""" + context = self._current_context or "" + context_id = self._context_id(context) + if context_id is not None: + self._current_context_id = context_id + else: + with self._connect() as con: cur = con.execute("insert into context (context) values (?)", (context,)) self._current_context_id = cur.lastrowid @@ -229,8 +234,7 @@ def add_lines(self, line_data): )) self._start_using() self._choose_lines_or_arcs(lines=True) - if self._current_context_id is None: - self.set_context("") + self._set_context_id() with self._connect() as con: for filename, linenos in iitems(line_data): file_id = self._file_id(filename, add=True) @@ -254,8 +258,7 @@ def add_arcs(self, arc_data): )) self._start_using() self._choose_lines_or_arcs(arcs=True) - if self._current_context_id is None: - self.set_context("") + self._set_context_id() with self._connect() as con: for filename, arcs in iitems(arc_data): file_id = self._file_id(filename, add=True) diff --git a/tests/test_debug.py b/tests/test_debug.py index 63edc84ff..284d9567d 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -144,8 +144,8 @@ def test_debug_callers(self): self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") else: - self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Adding lines") - self.assertRegex(last_line, r"\s+add_lines : .*coverage[/\\]sqldata.py @\d+$") + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Creating data file") + self.assertRegex(last_line, r"\s+_create_db : .*coverage[/\\]sqldata.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) From d621e6ba2367ffb5c94b27198de71a0c3d956e9b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 14 Oct 2018 21:15:40 -0400 Subject: [PATCH 299/952] =?UTF-8?q?Micha=C5=82=20suggested=20this=20would?= =?UTF-8?q?=20make=20the=20information=20easier=20to=20find.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONTRIBUTORS.txt | 1 + doc/config.rst | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 2e0bcbbaf..4c07024c0 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -79,6 +79,7 @@ Martin Fuzzey Matthew Boehm Matthew Desmarais Max Linke +Michał Bultrowicz Mickie Betz Nathan Land Noel O'Boyle diff --git a/doc/config.rst b/doc/config.rst index b8117a43c..625a544c0 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -3,9 +3,9 @@ .. _config: -=================== -Configuration files -=================== +======================= +Configuration reference +======================= .. :history: 20100223T201600, new for 3.3 .. :history: 20100725T211700, updated for 3.4. From 83a3b3fe96e0e7d5c25ab4cb423a7a5b2927b232 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 15 Oct 2018 05:51:50 -0400 Subject: [PATCH 300/952] Use one transaction to speed combining --- CHANGES.rst | 6 +++- coverage/sqldata.py | 84 ++++++++++++++++++++++++--------------------- 2 files changed, 50 insertions(+), 40 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e5f4aeb3b..e4d197aed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,7 +17,11 @@ Change history for Coverage.py Unreleased ---------- -- Bug fixes to context support: `issue 716`_. +- Improvements to context support: + + - The "no such table: meta" error is fixed.: `issue 716`_. + + - Combining data files now goes much faster. .. _issue 716: https://github.com/nedbat/coveragepy/issues/716 diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 561878faa..91ad3cd5e 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -338,46 +338,52 @@ def update(self, other_data, aliases=None): other_files = set() - # lines - if other_data._has_lines: - for context in other_data.measured_contexts(): - self.set_context(context) - for filename in other_data.measured_files(): - lines = set(other_data.lines(filename, context=context)) - if lines: - other_files.add(filename) - filename = aliases.map(filename) - lines.update(self.lines(filename, context=context) or ()) - self.add_lines({filename: lines}) - - # arcs - if other_data._has_arcs: - for context in other_data.measured_contexts(): - self.set_context(context) - for filename in other_data.measured_files(): - arcs = set(other_data.arcs(filename, context=context)) - if arcs: - other_files.add(filename) - filename = aliases.map(filename) - arcs.update(self.arcs(filename, context=context) or ()) - self.add_arcs({filename: arcs}) - - # file_tracers - for filename in other_files: - other_plugin = other_data.file_tracer(filename) - filename = aliases.map(filename) - if filename in this_measured: - this_plugin = self.file_tracer(filename) - else: - this_plugin = None - if this_plugin is None: - self.add_file_tracers({filename: other_plugin}) - elif this_plugin != other_plugin: - raise CoverageException( - "Conflicting file tracer name for '%s': %r vs %r" % ( - filename, this_plugin, other_plugin, + # Force the database we're writing to to exist before we start nesting + # contexts. + self._start_using() + + # Start a single transaction in each file. + with self._connect(), other_data._connect(): + # lines + if other_data._has_lines: + for context in other_data.measured_contexts(): + self.set_context(context) + for filename in other_data.measured_files(): + lines = set(other_data.lines(filename, context=context)) + if lines: + other_files.add(filename) + filename = aliases.map(filename) + lines.update(self.lines(filename, context=context) or ()) + self.add_lines({filename: lines}) + + # arcs + if other_data._has_arcs: + for context in other_data.measured_contexts(): + self.set_context(context) + for filename in other_data.measured_files(): + arcs = set(other_data.arcs(filename, context=context)) + if arcs: + other_files.add(filename) + filename = aliases.map(filename) + arcs.update(self.arcs(filename, context=context) or ()) + self.add_arcs({filename: arcs}) + + # file_tracers + for filename in other_files: + other_plugin = other_data.file_tracer(filename) + filename = aliases.map(filename) + if filename in this_measured: + this_plugin = self.file_tracer(filename) + else: + this_plugin = None + if this_plugin is None: + self.add_file_tracers({filename: other_plugin}) + elif this_plugin != other_plugin: + raise CoverageException( + "Conflicting file tracer name for '%s': %r vs %r" % ( + filename, this_plugin, other_plugin, + ) ) - ) def erase(self, parallel=False): """Erase the data in this object. From 5d8487492ff68d59c90b97b8b634c03c4770666a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 15 Oct 2018 07:31:22 -0400 Subject: [PATCH 301/952] Do we need more than 10 tries? --- tests/test_concurrency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 9e2d73d9c..16d1af815 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -360,7 +360,7 @@ def process_worker_main(args): """ -@flaky(max_runs=10) # Sometimes a test fails due to inherent randomness. Try more times. +@flaky(max_runs=30) # Sometimes a test fails due to inherent randomness. Try more times. class MultiprocessingTest(CoverageTest): """Test support of the multiprocessing module.""" From 65ea37a09eaf9f68271181c820e3389cddb1baa6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Oct 2018 06:12:42 -0400 Subject: [PATCH 302/952] Note a bug we closed --- CHANGES.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e4d197aed..cb08ec6fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -93,13 +93,15 @@ Version 5.0a2 --- 2018-09-03 - HTML files no longer have trailing and extra whitespace. - The sort order in the HTML report is stored in local storage rather than - cookies. Thanks, Federico Bond. + cookies, closing `issue 611`_. Thanks, Federico Bond. - pickle2json, for converting v3 data files to v4 data files, has been removed. .. _Bitbucket: https://bitbucket.org/ned/coveragepy .. _GitHub: https://github.com/nedbat/coveragepy +.. _issue 611: https://github.com/nedbat/coveragepy/issues/611 + .. _changes_50a1: From fcd516438e393ed85438daaddddc570ec0627ad2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Oct 2018 18:58:14 -0400 Subject: [PATCH 303/952] Put this helper in the right place --- tests/test_api.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index b44a5d340..ab9f9cf66 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -732,13 +732,8 @@ def test_nose_plugin(self): def test_nose_plugin_with_erase(self): self.pretend_to_be_nose_with_cover(erase=True) - def test_pytestcov_parallel(self): - self.pretend_to_be_pytestcov(append=False) - - def test_pytestcov_parallel_append(self): - self.pretend_to_be_pytestcov(append=True) - def pretend_to_be_pytestcov(self, append): + """Act like pytest-cov.""" self.make_file("prog.py", """\ a = 1 b = 2 @@ -769,6 +764,12 @@ def pretend_to_be_pytestcov(self, append): self.assert_file_count(".coverage", 0) self.assert_file_count(".coverage.*", 1) + def test_pytestcov_parallel(self): + self.pretend_to_be_pytestcov(append=False) + + def test_pytestcov_parallel_append(self): + self.pretend_to_be_pytestcov(append=True) + class ReporterDeprecatedAttributeTest(CoverageTest): """Test that Reporter.file_reporters has been deprecated.""" From 9307f55b72dbe47742655bced4e0ae05914c4664 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Oct 2018 20:03:52 -0400 Subject: [PATCH 304/952] Allow later DebugOutputFile to replace earlier ones When logging calls, get_one() is called with no filters. It would be the_one, so a later get_one with filters for pids wouldn't take effect. Now the earlier is only interim, and the later one wins. --- coverage/debug.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index 2c5c6aaf9..77ff3d119 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -244,7 +244,7 @@ def __init__(self, outfile, show_process, filters): SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' @classmethod - def get_one(cls, fileobj=None, show_process=True, filters=()): + def get_one(cls, fileobj=None, show_process=True, filters=(), interim=False): """Get a DebugOutputFile. If `fileobj` is provided, then a new DebugOutputFile is made with it. @@ -256,6 +256,11 @@ def get_one(cls, fileobj=None, show_process=True, filters=()): `show_process` controls whether the debug file adds process-level information, and filters is a list of other message filters to apply. + `filters` are the text filters to apply to the stream to annotate with + pids, etc. + + If `interim` is true, then a future `get_one` can replace this one. + """ if fileobj is not None: # Make DebugOutputFile around the fileobj passed. @@ -265,15 +270,16 @@ def get_one(cls, fileobj=None, show_process=True, filters=()): # this class can be defined more than once. But we really want # a process-wide singleton. So stash it in sys.modules instead of # on a class attribute. Yes, this is aggressively gross. - the_one = sys.modules.get(cls.SYS_MOD_NAME) - if the_one is None: + the_one, is_interim = sys.modules.get(cls.SYS_MOD_NAME, (None, True)) + if the_one is None or is_interim: if fileobj is None: debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE") if debug_file_name: fileobj = open(debug_file_name, "a") else: fileobj = sys.stderr - sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters) + the_one = cls(fileobj, show_process, filters) + sys.modules[cls.SYS_MOD_NAME] = (the_one, interim) return the_one def write(self, text): @@ -288,7 +294,7 @@ def flush(self): def log(msg, stack=False): # pragma: debugging """Write a log message as forcefully as possible.""" - out = DebugOutputFile.get_one() + out = DebugOutputFile.get_one(interim=True) out.write(msg+"\n") if stack: dump_stack_frames(out=out, skip=1) @@ -344,7 +350,7 @@ def _wrapper(self, *args, **kwargs): extra += " @ " extra += "; ".join(_clean_stack_line(l) for l in short_stack().splitlines()) msg = "{} {:04d} {}{}\n".format(oid, next(CALLS), func.__name__, extra) - DebugOutputFile.get_one().write(msg) + DebugOutputFile.get_one(interim=True).write(msg) return func(self, *args, **kwargs) return _wrapper return _decorator From 745d8f20047ee63d8e8378ab77a3488951e97b0e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Oct 2018 20:38:30 -0400 Subject: [PATCH 305/952] A test for 'no such table: meta' as in #716. --- tests/test_data.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_data.py b/tests/test_data.py index 60fd341e5..bda73810c 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -831,3 +831,15 @@ def test_combining_from_nonexistent_directories(self): msg = "Couldn't combine from non-existent path 'xyzzy'" with self.assertRaisesRegex(CoverageException, msg): combine_parallel_data(covdata, data_paths=['xyzzy']) + + def test_interleaved_erasing_bug716(self): + # pytest-cov could produce this scenario. #716 + covdata1 = CoverageData() + covdata2 = CoverageData() + # this used to create the .coverage database file.. + covdata2.set_context("") + # then this would erase it all.. + covdata1.erase() + # then this would try to use tables that no longer exist. + # "no such table: meta" + covdata2.add_lines(LINES_1) From e82d24da2355df86fc46d7d41142ce308ed2c919 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Oct 2018 20:58:52 -0400 Subject: [PATCH 306/952] Don't clobber sys.path[0]. #715 --- CHANGES.rst | 4 ++++ coverage/cmdline.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index cb08ec6fb..8845d7e57 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,12 +17,16 @@ Change history for Coverage.py Unreleased ---------- +- Coverage commands no longer clobber the first entry in sys.path, fixing + `issue 715`_. + - Improvements to context support: - The "no such table: meta" error is fixed.: `issue 716`_. - Combining data files now goes much faster. +.. _issue 715: https://github.com/nedbat/coveragepy/issues/715 .. _issue 716: https://github.com/nedbat/coveragepy/issues/716 diff --git a/coverage/cmdline.py b/coverage/cmdline.py index edbc1d25a..e6ea6e236 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -465,10 +465,6 @@ def command_line(self, argv): if self.do_help(options, args, parser): return OK - # We need to be able to import from the current directory, because - # plugins may try to, for example, to read Django settings. - sys.path[0] = '' - # Listify the list options. source = unshell_list(options.source) omit = unshell_list(options.omit) From cc828900b37e48a542d1f2718df478c505684e6e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Oct 2018 22:22:16 -0400 Subject: [PATCH 307/952] Clean some lint --- coverage/debug.py | 3 ++- tests/test_context.py | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index 77ff3d119..3a6b1fdc8 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -93,7 +93,8 @@ def get_output(self): class NoDebugging(object): """A replacement for DebugControl that will never try to do anything.""" - def should(self, option): + def should(self, option): # pylint: disable=unused-argument + """Should we write debug messages? Never.""" return False diff --git a/tests/test_context.py b/tests/test_context.py index efc054182..e5ab4800e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -225,12 +225,15 @@ def plain_old_function(a, b): # pylint: disable=missing-docstring, unu def fake_out(self): # pylint: disable=missing-docstring, unused-argument return get_qualname() -def meth(self): +def patch_meth(self): # pylint: disable=missing-docstring, unused-argument return get_qualname() class QualnameTest(CoverageTest): """Tests of qualname_from_frame.""" + # Pylint gets confused about meth() below. + # pylint: disable=no-value-for-parameter + run_in_temp_dir = False def test_method(self): @@ -257,5 +260,5 @@ def test_property(self): def test_changeling(self): c = Child() - c.meth = meth - self.assertEqual(c.meth(c), "meth") + c.meth = patch_meth + self.assertEqual(c.meth(c), "patch_meth") From 05e94ab0a8a108e77df134ee4a8c5c91c65c47d6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 18 Oct 2018 08:27:13 -0400 Subject: [PATCH 308/952] This is literally what setdefault is for --- tests/test_cmdline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 39827ff54..d1b38b987 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -104,8 +104,7 @@ def cmd_executes(self, args, code, ret=OK): # the defaults. This lets the tests just mention the interesting ones. 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 + kwargs.setdefault(k, v) self.assert_same_method_calls(m1, m2) def cmd_executes_same(self, args1, args2): From 24219dda093239a09596116dd1fdd8839c1957b4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 18 Oct 2018 11:50:25 -0400 Subject: [PATCH 309/952] We weren't using this argument --- tests/coveragetest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 378097c80..e97f914b8 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -369,7 +369,7 @@ def assert_recent_datetime(self, dt, seconds=10, msg=None): self.assertGreaterEqual(age.total_seconds(), 0, msg) self.assertLessEqual(age.total_seconds(), seconds, msg) - def command_line(self, args, ret=OK, _covpkg=None): + def command_line(self, args, ret=OK): """Run `args` through the command line. Use this when you want to run the full coverage machinery, but in the @@ -381,7 +381,7 @@ def command_line(self, args, ret=OK, _covpkg=None): Returns None. """ - ret_actual = command_line(args, _covpkg=_covpkg) + ret_actual = command_line(args) self.assertEqual(ret_actual, ret) coverage_command = "coverage" From adf376f699e9ce977dcc47329f1cef7a7a8ce9c7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 18 Oct 2018 14:59:51 -0400 Subject: [PATCH 310/952] This was in the wrong place --- doc/config.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/config.rst b/doc/config.rst index 625a544c0..8b534637c 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -115,9 +115,6 @@ to more than one command. ``branch`` (boolean, default False): whether to measure :ref:`branch coverage ` in addition to statement coverage. -``cover_pylib`` (boolean, default False): whether to measure the Python -standard library. - ``concurrency`` (multi-string, default "thread"): the name concurrency libraries in use by the product code. If your program uses `multiprocessing`_, `gevent`_, `greenlet`_, or `eventlet`_, you must name that library in this @@ -137,6 +134,9 @@ Before version 4.2, this option only accepted a single string. .. versionadded:: 5.0 +``cover_pylib`` (boolean, default False): whether to measure the Python +standard library. + ``data_file`` (string, default ".coverage"): the name of the data file to use for storing or reporting coverage. This value can include a path to another directory. From e57549076220764ace6f2b67da3600ff5ae33f02 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 18 Oct 2018 15:10:58 -0400 Subject: [PATCH 311/952] `[run] command_line` is the command line to use for 'coverage run'. #695 --- CHANGES.rst | 4 ++++ coverage/cmdline.py | 8 ++++++++ coverage/config.py | 2 ++ doc/config.rst | 7 +++++++ tests/test_cmdline.py | 47 +++++++++++++++++++++++++++++++++++++++---- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8845d7e57..68128ecda 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,9 @@ Change history for Coverage.py Unreleased ---------- +- You can specify the command line to run your program with the ``[run] + command_line`` configuration setting. `issue 695`_. + - Coverage commands no longer clobber the first entry in sys.path, fixing `issue 715`_. @@ -26,6 +29,7 @@ Unreleased - Combining data files now goes much faster. +.. _issue 695: https://github.com/nedbat/coveragepy/issues/695 .. _issue 715: https://github.com/nedbat/coveragepy/issues/715 .. _issue 716: https://github.com/nedbat/coveragepy/issues/716 diff --git a/coverage/cmdline.py b/coverage/cmdline.py index e6ea6e236..b45547ba2 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -8,6 +8,7 @@ import glob import optparse import os.path +import shlex import sys import textwrap import traceback @@ -601,6 +602,13 @@ def do_help(self, options, args, parser): def do_run(self, options, args): """Implementation of 'coverage run'.""" + if not args: + command_line = self.coverage.get_option("run:command_line") + if command_line is not None: + args = shlex.split(command_line) + if args and args[0] == "-m": + options.module = True + args = args[1:] if not args: self.help_fn("Nothing to do.") return ERR diff --git a/coverage/config.py b/coverage/config.py index 2a2818759..f61d69519 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -174,6 +174,7 @@ def __init__(self): # Defaults for [run] self.branch = False + self.command_line = None self.concurrency = None self.context = None self.cover_pylib = False @@ -319,6 +320,7 @@ def from_file(self, filename, our_file): # [run] ('branch', 'run:branch', 'boolean'), + ('command_line', 'run:command_line'), ('concurrency', 'run:concurrency', 'list'), ('context', 'run:context'), ('cover_pylib', 'run:cover_pylib', 'boolean'), diff --git a/doc/config.rst b/doc/config.rst index 8b534637c..0b668351d 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -115,6 +115,13 @@ to more than one command. ``branch`` (boolean, default False): whether to measure :ref:`branch coverage ` in addition to statement coverage. +``command_line`` (string): the command-line to run your program. This will be +used if you run ``coverage run`` with no further arguments. Coverage.py +options cannot be specified here, other than ``-m`` to indicate the module to +run. + +.. versionadded:: 5.0 + ``concurrency`` (multi-string, default "thread"): the name concurrency libraries in use by the product code. If your program uses `multiprocessing`_, `gevent`_, `greenlet`_, or `eventlet`_, you must name that library in this diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index d1b38b987..e7d3fafaa 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -59,9 +59,10 @@ def model_object(self): # same object as the resulting coverage object. mk.Coverage.return_value = mk - # The mock needs to get options, but shouldn't need to set them. + # The mock needs options. config = CoverageConfig() mk.get_option = config.get_option + mk.set_option = config.set_option # Get the type right for the result of reporting. mk.report.return_value = 50.0 @@ -70,14 +71,19 @@ def model_object(self): return mk - def mock_command_line(self, args): + def mock_command_line(self, args, options=None): """Run `args` through the command line, with a Mock. + `options` is a dict of names and values to pass to `set_option`. + Returns the Mock it used and the status code returned. """ m = self.model_object() + for name, value in (options or {}).items(): + m.set_option(name, value) + ret = command_line( args, _covpkg=m, _run_python_file=m.run_python_file, @@ -86,9 +92,9 @@ def mock_command_line(self, args): return m, ret - def cmd_executes(self, args, code, ret=OK): + def cmd_executes(self, args, code, ret=OK, options=None): """Assert that the `args` end up executing the sequence in `code`.""" - m1, r1 = self.mock_command_line(args) + m1, r1 = self.mock_command_line(args, options=options) self.assertEqual(r1, ret, "Wrong status: got %r, wanted %r" % (r1, ret)) # Remove all indentation, and change ".foo()" to "m2.foo()". @@ -521,6 +527,39 @@ def test_run_nothing(self): self.command_line("run", ret=ERR) self.assertIn("Nothing to do", self.stderr()) + def test_run_from_config(self): + options = {"run:command_line": "myprog.py a 123 'a quoted thing' xyz"} + self.cmd_executes("run", """\ + .Coverage() + .start() + .run_python_file('myprog.py', ['myprog.py', 'a', '123', 'a quoted thing', 'xyz']) + .stop() + .save() + """, + options=options, + ) + + def test_run_module_from_config(self): + options = {"run:command_line": "-m mymodule thing1 thing2"} + self.cmd_executes("run", """\ + .Coverage() + .start() + .run_python_module('mymodule', ['mymodule', 'thing1', 'thing2']) + .stop() + .save() + """, + options=options, + ) + + def test_run_from_config_but_empty(self): + self.cmd_executes("run", """\ + .Coverage() + .help_fn('Nothing to do.') + """, + ret=1, + options={"run:command_line": ""}, + ) + 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.stderr()) From f0414651b855de1f2d834a3c3a99d56b6c2a0753 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 19 Oct 2018 06:12:39 -0400 Subject: [PATCH 312/952] Clean up a metacov .pth file that could get left behind. --- Makefile | 1 + igor.py | 1 + 2 files changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 6c83b5627..045996a4b 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ clean: -rm -f coverage/*,cover -rm -f MANIFEST -rm -f .coverage .coverage.* coverage.xml .metacov* + -rm -f .tox/*/lib/*/site-packages/zzz_metacov.pth -rm -f */.coverage */*/.coverage */*/*/.coverage */*/*/*/.coverage */*/*/*/*/.coverage */*/*/*/*/*/.coverage -rm -f tests/zipmods.zip -rm -rf tests/eggsrc/build tests/eggsrc/dist tests/eggsrc/*.egg-info diff --git a/igor.py b/igor.py index 39d751957..750ea6882 100644 --- a/igor.py +++ b/igor.py @@ -106,6 +106,7 @@ def run_tests_with_coverage(tracer, *runner_args): # Create the .pth file that will let us measure coverage in sub-processes. # The .pth file seems to have to be alphabetically after easy-install.pth # or the sys.path entries aren't created right? + # There's an entry in "make clean" to get rid of this file. pth_dir = os.path.dirname(pytest.__file__) pth_path = os.path.join(pth_dir, "zzz_metacov.pth") with open(pth_path, "w") as pth_file: From e08528bb01063c7b6db60b9c7c2516615b818303 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 19 Oct 2018 06:16:56 -0400 Subject: [PATCH 313/952] One more error case for the command line --- coverage/cmdline.py | 4 ++++ tests/test_cmdline.py | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index b45547ba2..d04da3990 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -603,6 +603,10 @@ def do_run(self, options, args): """Implementation of 'coverage run'.""" if not args: + if options.module: + # Specified -m with nothing else. + self.help_fn("No module specified for -m") + return ERR command_line = self.coverage.get_option("run:command_line") if command_line is not None: args = shlex.split(command_line) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index e7d3fafaa..ff0357648 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -540,7 +540,6 @@ def test_run_from_config(self): ) def test_run_module_from_config(self): - options = {"run:command_line": "-m mymodule thing1 thing2"} self.cmd_executes("run", """\ .Coverage() .start() @@ -548,7 +547,7 @@ def test_run_module_from_config(self): .stop() .save() """, - options=options, + options={"run:command_line": "-m mymodule thing1 thing2"}, ) def test_run_from_config_but_empty(self): @@ -556,10 +555,25 @@ def test_run_from_config_but_empty(self): .Coverage() .help_fn('Nothing to do.') """, - ret=1, + ret=ERR, options={"run:command_line": ""}, ) + def test_run_dashm_only(self): + self.cmd_executes("run -m", """\ + .Coverage() + .help_fn('No module specified for -m') + """, + ret=ERR, + ) + self.cmd_executes("run -m", """\ + .Coverage() + .help_fn('No module specified for -m') + """, + ret=ERR, + options={"run:command_line": "myprog.py"} + ) + 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.stderr()) From a28cd71722750230a6c13794fa47dd54ccb6f224 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 19 Oct 2018 07:01:37 -0400 Subject: [PATCH 314/952] PyPy3 fixed an obscure bug long ago, we don't need this override --- coverage/backunittest.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/coverage/backunittest.py b/coverage/backunittest.py index 21d7bcb23..242e9d39d 100644 --- a/coverage/backunittest.py +++ b/coverage/backunittest.py @@ -20,13 +20,9 @@ class TestCase(unittest.TestCase): """ # pylint: disable=missing-docstring - # Many Pythons have this method defined. But PyPy3 has a bug with it - # somehow (https://bitbucket.org/pypy/pypy/issues/2092), so always use our - # own implementation that works everywhere, at least for the ways we're - # calling it. - def assertCountEqual(self, s1, s2): - """Assert these have the same elements, regardless of order.""" - self.assertEqual(sorted(s1), sorted(s2)) + if not unittest_has('assertCountEqual'): + def assertCountEqual(self, *args, **kwargs): + return self.assertItemsEqual(*args, **kwargs) if not unittest_has('assertRaisesRegex'): def assertRaisesRegex(self, *args, **kwargs): From 6bea0ea1c75981d13e5e01c18bbab34b891757b3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 19 Oct 2018 07:12:17 -0400 Subject: [PATCH 315/952] Clean __pycache__ before .pyc (a little faster?) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 045996a4b..e6ea9a30c 100644 --- a/Makefile +++ b/Makefile @@ -13,11 +13,11 @@ clean: -PYTHONPATH=. python tests/test_farm.py clean -rm -rf tests/farm/*/out -rm -rf build coverage.egg-info dist htmlcov + -rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__ -rm -f *.pyc */*.pyc */*/*.pyc */*/*/*.pyc */*/*/*/*.pyc */*/*/*/*/*.pyc -rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo */*/*/*/*.pyo */*/*/*/*/*.pyo -rm -f *.bak */*.bak */*/*.bak */*/*/*.bak */*/*/*/*.bak */*/*/*/*/*.bak -rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class */*/*/*/*$$py.class */*/*/*/*/*$$py.class - -rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__ -rm -f coverage/*,cover -rm -f MANIFEST -rm -f .coverage .coverage.* coverage.xml .metacov* From 23963607312a888fc1c1949537497aa951b421c5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 21 Oct 2018 15:18:41 -0400 Subject: [PATCH 316/952] Helpers for making directories --- coverage/control.py | 7 +++---- coverage/misc.py | 15 +++++++++++++++ coverage/report.py | 5 ++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index dd862ae9c..a89a9da6b 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -5,6 +5,7 @@ import atexit import os +import os.path import platform import sys import time @@ -22,7 +23,7 @@ from coverage.html import HtmlReporter from coverage.inorout import InOrOut from coverage.misc import CoverageException, bool_or_none, join_regex -from coverage.misc import file_be_gone, isolate_module +from coverage.misc import ensure_dir_for_file, file_be_gone, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins from coverage.python import PythonFileReporter @@ -815,9 +816,7 @@ def xml_report( # because this report pre-opens the output file. # HTMLReport does this using the Report plumbing because # its task is more complex, being multiple files. - output_dir = os.path.dirname(self.config.xml_output) - if output_dir and not os.path.isdir(output_dir): - os.makedirs(output_dir) + ensure_dir_for_file(self.config.xml_output) open_kwargs = {} if env.PY3: open_kwargs['encoding'] = 'utf8' diff --git a/coverage/misc.py b/coverage/misc.py index a923829d0..59114863a 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -8,6 +8,7 @@ import inspect import locale import os +import os.path import re import sys import types @@ -181,6 +182,20 @@ def file_be_gone(path): raise +def ensure_dir(directory): + """Make sure the directory exists. + + If `directory` is None or empty, do nothing. + """ + if directory and not os.path.isdir(directory): + os.makedirs(directory) + + +def ensure_dir_for_file(path): + """Make sure the directory for the path exists.""" + ensure_dir(os.path.dirname(path)) + + def output_encoding(outfile=None): """Determine the encoding to use for output written to `outfile` or stdout.""" if outfile is None: diff --git a/coverage/report.py b/coverage/report.py index e4378f6db..6f87bbf2b 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -7,7 +7,7 @@ import warnings from coverage.files import prep_patterns, FnmatchMatcher -from coverage.misc import CoverageException, NoSource, NotPython, isolate_module +from coverage.misc import CoverageException, NoSource, NotPython, ensure_dir, isolate_module os = isolate_module(os) @@ -83,8 +83,7 @@ def report_files(self, report_fn, morfs, directory=None): raise CoverageException("No data to report.") self.directory = directory - if self.directory and not os.path.exists(self.directory): - os.makedirs(self.directory) + ensure_dir(self.directory) for fr in file_reporters: try: From 8271bf4c4d5272043677f37a65e16d21621133cb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 21 Oct 2018 15:57:40 -0400 Subject: [PATCH 317/952] Make directories for the data file if needed. #721 --- CHANGES.rst | 4 ++++ coverage/control.py | 1 + tests/test_api.py | 9 +++++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 68128ecda..8b8f4cf67 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,9 @@ Unreleased - You can specify the command line to run your program with the ``[run] command_line`` configuration setting. `issue 695`_. +- Coverage will create directories as needed for the data file if they don't + exist, closing `issue 721`_. + - Coverage commands no longer clobber the first entry in sys.path, fixing `issue 715`_. @@ -32,6 +35,7 @@ Unreleased .. _issue 695: https://github.com/nedbat/coveragepy/issues/695 .. _issue 715: https://github.com/nedbat/coveragepy/issues/715 .. _issue 716: https://github.com/nedbat/coveragepy/issues/716 +.. _issue 721: https://github.com/nedbat/coveragepy/issues/721 .. _changes_50a3: diff --git a/coverage/control.py b/coverage/control.py index a89a9da6b..2eeec85ce 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -408,6 +408,7 @@ def _init_data(self, suffix): # Create the data file. We do this at construction time so that the # data file will be written into the directory where the process # started rather than wherever the process eventually chdir'd to. + ensure_dir_for_file(self.config.data_file) self._data = CoverageData( basename=self.config.data_file, suffix=suffix, diff --git a/tests/test_api.py b/tests/test_api.py index ab9f9cf66..2f6f7a2f9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -250,6 +250,15 @@ def test_datafile_from_rcfile(self): cov.save() self.assertFiles(["datatest4.py", ".coveragerc", "mydata.dat"]) + def test_deep_datafile(self): + self.make_file("datatest5.py", "fooey = 17") + self.assertFiles(["datatest5.py"]) + cov = coverage.Coverage(data_file="deep/sub/cov.data") + self.start_import_stop(cov, "datatest5") + cov.save() + self.assertFiles(["datatest5.py", "deep"]) + self.assert_exists("deep/sub/cov.data") + def test_empty_reporting(self): # empty summary reports raise exception, just like the xml report cov = coverage.Coverage() From bf86427e1861aa353d7e80de0d373d9a30edb462 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 21 Oct 2018 17:31:27 -0400 Subject: [PATCH 318/952] Always include a documentation link at the end of help messages --- coverage/cmdline.py | 18 ++++++++++-------- tests/test_cmdline.py | 20 ++++++++++++++------ 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index d04da3990..23107c01e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -548,23 +548,27 @@ def command_line(self, argv): def help(self, error=None, topic=None, parser=None): """Display an error message, or the named topic.""" assert error or topic or parser + + help_params = dict(self.covpkg.__dict__) + help_params['program_name'] = self.program_name + if CTracer is not None: + help_params['extension_modifier'] = 'with C extension' + else: + help_params['extension_modifier'] = 'without C extension' + if error: print(error, file=sys.stderr) print("Use '%s help' for help." % (self.program_name,), file=sys.stderr) elif parser: print(parser.format_help().strip()) + print() else: - help_params = dict(self.covpkg.__dict__) - help_params['program_name'] = self.program_name - if CTracer is not None: - help_params['extension_modifier'] = 'with C extension' - else: - help_params['extension_modifier'] = 'without C extension' help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() if help_msg: print(help_msg.format(**help_params)) else: print("Don't know topic %r" % topic) + print("Full documentation is at {__url__}".format(**help_params)) def do_help(self, options, args, parser): """Deal with help requests. @@ -744,7 +748,6 @@ def unglob_args(args): xml Create an XML report of coverage results. Use "{program_name} help " for detailed help on any command. - For full documentation, see {__url__} """, 'minimum_help': """\ @@ -753,7 +756,6 @@ def unglob_args(args): 'version': """\ Coverage.py, version {__version__} {extension_modifier} - Documentation at {__url__} """, } diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index ff0357648..fec0787b6 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -18,6 +18,7 @@ from coverage.config import CoverageConfig from coverage.data import CoverageData from coverage.misc import ExceptionDuringRun +from coverage.version import __url__ from tests.coveragetest import CoverageTest, OK, ERR, command_line @@ -714,21 +715,25 @@ def test_help_contains_command_name_from_package(self): def test_help(self): self.command_line("help") - out = self.stdout() - self.assertIn("readthedocs.io", out) - self.assertGreater(out.count("\n"), 10) + lines = self.stdout().splitlines() + self.assertGreater(len(lines), 10) + self.assertEqual(lines[-1], "Full documentation is at {}".format(__url__)) def test_cmd_help(self): self.command_line("help run") out = self.stdout() - self.assertIn("", out) + lines = out.splitlines() + self.assertIn("", lines[0]) self.assertIn("--timid", out) - self.assertGreater(out.count("\n"), 10) + self.assertGreater(len(lines), 30) + self.assertEqual(lines[-1], "Full documentation is at {}".format(__url__)) def test_unknown_topic(self): # Should probably be an ERR return, but meh. self.command_line("help foobar") - self.assertEqual(self.stdout(), "Don't know topic 'foobar'\n") + lines = self.stdout().splitlines() + self.assertEqual(lines[0], "Don't know topic 'foobar'") + self.assertEqual(lines[-1], "Full documentation is at {}".format(__url__)) def test_error(self): self.command_line("fooey kablooey", ret=ERR) @@ -736,6 +741,9 @@ def test_error(self): self.assertIn("fooey", err) self.assertIn("help", err) + def test_doc_url(self): + self.assertTrue(__url__.startswith("https://coverage.readthedocs.io")) + class CmdMainTest(CoverageTest): """Tests of coverage.cmdline.main(), using mocking for isolation.""" From 86bc083074b5b2dcaa9165218dceca4376942e3c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 24 Oct 2018 07:22:03 -0400 Subject: [PATCH 319/952] Add 3.7, and other appveyor changes --- appveyor.yml | 52 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 8b9e1b619..795c8ee4d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,67 +16,84 @@ environment: # .pth files in the shared virtualenv. Disable parallel tests. PYTEST_ADDOPTS: "-n 0" + # Note: There is logic to install Python version $PYTHON_VERSION if the + # $PYTHON directory doesn't exist. Last I tried (Oct 2018), that installation + # failed, and I don't know why. $PYTHON_VERSION is visible in the job + # descriptions, but can be wrong in the minor version, since we use the + # version pre-installed on AppVeyor. matrix: - JOB: "2.7 32-bit" TOXENV: "py27" - PYTHON: "C:\\Python27.11" - PYTHON_VERSION: "2.7.11" + PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7.15" PYTHON_ARCH: "32" - JOB: "2.7 64-bit" TOXENV: "py27" - PYTHON: "C:\\Python27.11-x64" - PYTHON_VERSION: "2.7.11" + PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.15" PYTHON_ARCH: "64" - JOB: "3.4 32-bit" TOXENV: "py34" PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4" + PYTHON_VERSION: "3.4.4" PYTHON_ARCH: "32" - JOB: "3.4 64-bit" TOXENV: "py34" PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4" + PYTHON_VERSION: "3.4.4" PYTHON_ARCH: "64" - JOB: "3.5 32-bit" TOXENV: "py35" PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.0" + PYTHON_VERSION: "3.5.4" PYTHON_ARCH: "32" - JOB: "3.5 64-bit" TOXENV: "py35" PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.0" + PYTHON_VERSION: "3.5.4" PYTHON_ARCH: "64" - JOB: "3.6 32-bit" TOXENV: "py36" PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.0" + PYTHON_VERSION: "3.6.7" PYTHON_ARCH: "32" - JOB: "3.6 64-bit" TOXENV: "py36" PYTHON: "C:\\Python36-x64" - PYTHON_VERSION: "3.6.0" + PYTHON_VERSION: "3.6.7" + PYTHON_ARCH: "64" + + - JOB: "3.7 32-bit" + TOXENV: "py37" + PYTHON: "C:\\Python37" + PYTHON_VERSION: "3.7.1" + PYTHON_ARCH: "32" + + - JOB: "3.7 64-bit" + TOXENV: "py37" + PYTHON: "C:\\Python37-x64" + PYTHON_VERSION: "3.7.1" PYTHON_ARCH: "64" # Meta coverage - JOB: "Meta 2.7" TOXENV: "py27" PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7" + PYTHON_VERSION: "2.7.15" PYTHON_ARCH: "32" COVERAGE_COVERAGE: "yes" - - JOB: "Meta 3.5" - TOXENV: "py35" - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5" + - JOB: "Meta 3.6" + TOXENV: "py36" + PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.7" PYTHON_ARCH: "32" COVERAGE_COVERAGE: "yes" @@ -126,5 +143,10 @@ after_test: - if "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% pip install codecov - if "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% codecov -X gcov --file coverage.xml +# Uncomment this to enable RDP access to the build when done. +# https://www.appveyor.com/docs/how-to/rdp-to-build-worker/ +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + artifacts: - path: "metacov-*.zip" From 85a878e76bad95c01d50e2533a7653b8e7699770 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 23 Oct 2018 06:23:14 -0400 Subject: [PATCH 320/952] Use a new event loop --- tests/test_arcs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index f20c8cadb..0cf4dd3b1 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -1484,7 +1484,7 @@ async def print_sum(x, y): # 8 ) print("%s + %s = %s" % (x, y, result)) - loop = asyncio.get_event_loop() # E + loop = asyncio.new_event_loop() # E loop.run_until_complete(print_sum(1, 2)) loop.close() # G """, @@ -1518,7 +1518,7 @@ async def doit(): # G print(letter) print(".") - loop = asyncio.get_event_loop() # L + loop = asyncio.new_event_loop() # L loop.run_until_complete(doit()) loop.close() """, From 4113a321b239c0da518c6a8ae8bc96409f216fa9 Mon Sep 17 00:00:00 2001 From: Jon Dufresne Date: Sat, 27 Oct 2018 13:46:53 -0700 Subject: [PATCH 321/952] Remove use_2to3=False; it is the default Slightly simplifies setup.py. --- setup.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/setup.py b/setup.py index af99b59b3..95b4944cf 100644 --- a/setup.py +++ b/setup.py @@ -186,13 +186,6 @@ def build_extension(self, ext): }, )) -# Py3.x-specific details. - -if sys.version_info >= (3, 0): - setup_args.update(dict( - use_2to3=False, - )) - def main(): """Actually invoke setup() with the arguments we built above.""" From 85d3f588552128b29d5c7eb4b030fa29df39df1a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 31 Oct 2018 06:24:05 -0400 Subject: [PATCH 322/952] Better version showing --- appveyor.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 795c8ee4d..1131efe13 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -111,8 +111,7 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" # Check that we have the expected version and architecture for Python - - "python --version" - - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" + - "python -c \"import struct, sys; print('{}\\n{}-bit'.format(sys.version, struct.calcsize('P') * 8))\"" # Upgrade to the latest version of pip to avoid it displaying warnings # about it being out of date. From 9cdf251d60223c38d560b654c6b6abf95604b0ef Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 1 Nov 2018 19:59:37 -0400 Subject: [PATCH 323/952] More correct --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8b8f4cf67..426246fe6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -72,7 +72,7 @@ Version 5.0a3 --- 2018-10-06 is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default value}`` will use "default value". -- Tentative support for Python 3.8, which has not yet released an alpha. Fixes +- Partial support for Python 3.8, which has not yet released an alpha. Fixes `issue 707` and `issue 714`_. .. _issue 707: https://github.com/nedbat/coveragepy/issues/707 From 649c70527ae602512cfa6ea62b60ebc43fc69797 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 3 Nov 2018 11:40:31 -0400 Subject: [PATCH 324/952] Make this useful for py3 also --- lab/run_trace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lab/run_trace.py b/lab/run_trace.py index 27c24a1da..ddfbfe57b 100644 --- a/lab/run_trace.py +++ b/lab/run_trace.py @@ -31,5 +31,6 @@ def trace(frame, event, arg): the_program = sys.argv[1] +code = open(the_program).read() sys.settrace(trace) -execfile(the_program) +exec(code) From d469f30b3126159a7a6693703203556d124a3dea Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 3 Nov 2018 12:05:11 -0400 Subject: [PATCH 325/952] You can turn off contracts while debugging tests --- coverage/misc.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coverage/misc.py b/coverage/misc.py index 59114863a..1782d285a 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -47,9 +47,13 @@ def _decorator(func): return _decorator +# Environment COVERAGE_NO_CONTRACTS=1 can turn off contracts while debugging +# tests to remove noise from stack traces. +USE_CONTRACTS = env.TESTING and not bool(int(os.environ.get("COVERAGE_NO_CONTRACTS", 0))) + # Use PyContracts for assertion testing on parameters and returns, but only if # we are running our own test suite. -if env.TESTING: +if USE_CONTRACTS: from contracts import contract # pylint: disable=unused-import from contracts import new_contract as raw_new_contract From d8f1d925dc0aa4908f3f5a1b965f671bcf1b5ba6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 3 Nov 2018 17:12:30 -0400 Subject: [PATCH 326/952] Adapt to 3.8's way of tracing decorated functions --- coverage/env.py | 6 ++++++ coverage/parser.py | 31 ++++++++++++++++++++++++------- tests/test_coverage.py | 16 ++++++++++++---- tests/test_parser.py | 17 ++++++++++++++--- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/coverage/env.py b/coverage/env.py index aa8bb8f6d..fd98fa2ba 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -34,6 +34,12 @@ class PYBEHAVIOR(object): # work? finally_jumps_back = (PYVERSION >= (3, 8)) + # When a function is decorated, does the trace function get called for the + # @-line and also the def-line (new behavior in 3.8)? Or just the @-line + # (old behavior)? + trace_decorated_def = (PYVERSION >= (3, 8)) + + # Coverage.py specifics. # Are we using the C-implemented trace function? diff --git a/coverage/parser.py b/coverage/parser.py index 5ffcad8cb..6faa36e2d 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -579,9 +579,19 @@ def line_for_node(self, node): else: return node.lineno + def _line_decorated(self, node): + """Compute first line number for things that can be decorated (classes and functions).""" + lineno = node.lineno + if env.PYBEHAVIOR.trace_decorated_def: + if node.decorator_list: + lineno = node.decorator_list[0].lineno + return lineno + def _line__Assign(self, node): return self.line_for_node(node.value) + _line__ClassDef = _line_decorated + def _line__Dict(self, node): # Python 3.5 changed how dict literals are made. if env.PYVERSION >= (3, 5) and node.keys: @@ -594,6 +604,8 @@ def _line__Dict(self, node): else: return node.lineno + _line__FunctionDef = _line_decorated + def _line__List(self, node): if node.elts: return self.line_for_node(node.elts[0]) @@ -812,10 +824,10 @@ def process_return_exits(self, exits): # Handlers: _handle__* # # Each handler deals with a specific AST node type, dispatched from - # add_arcs. Each deals with a particular kind of node type, and returns - # the set of exits from that node. These functions mirror the Python - # semantics of each syntactic construct. See the docstring for add_arcs to - # understand the concept of exits from a node. + # add_arcs. Handlers return the set of exits from that node, and can + # also call self.add_arc to record arcs they find. These functions mirror + # the Python semantics of each syntactic construct. See the docstring + # for add_arcs to understand the concept of exits from a node. @contract(returns='ArcStarts') def _handle__Break(self, node): @@ -827,13 +839,18 @@ def _handle__Break(self, node): @contract(returns='ArcStarts') def _handle_decorated(self, node): """Add arcs for things that can be decorated (classes and functions).""" - last = self.line_for_node(node) + main_line = last = node.lineno if node.decorator_list: + if env.PYBEHAVIOR.trace_decorated_def: + last = None for dec_node in node.decorator_list: dec_start = self.line_for_node(dec_node) - if dec_start != last: + if last is not None and dec_start != last: self.add_arc(last, dec_start) - last = dec_start + last = dec_start + if env.PYBEHAVIOR.trace_decorated_def: + self.add_arc(last, main_line) + last = main_line # The definition line may have been missed, but we should have it # in `self.statements`. For some constructs, `line_for_node` is # not what we'd think of as the first line in the statement, so map diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 676fc831f..602059009 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -1579,6 +1579,9 @@ class Py24Test(CoverageTest): """Tests of new syntax in Python 2.4.""" def test_function_decorators(self): + lines = [1, 2, 3, 4, 6, 8, 10, 12] + if env.PYBEHAVIOR.trace_decorated_def: + lines = sorted(lines + [9]) self.check_coverage("""\ def require_int(func): def wrapper(arg): @@ -1593,9 +1596,12 @@ def p1(arg): assert p1(10) == 20 """, - [1,2,3,4,6,8,10,12], "") + lines, "") def test_function_decorators_with_args(self): + lines = [1, 2, 3, 4, 5, 6, 8, 10, 12] + if env.PYBEHAVIOR.trace_decorated_def: + lines = sorted(lines + [9]) self.check_coverage("""\ def boost_by(extra): def decorator(func): @@ -1610,9 +1616,12 @@ def boosted(arg): assert boosted(10) == 200 """, - [1,2,3,4,5,6,8,10,12], "") + lines, "") def test_double_function_decorators(self): + lines = [1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 14, 15, 17, 19, 21, 22, 24, 26] + if env.PYBEHAVIOR.trace_decorated_def: + lines = sorted(lines + [16, 23]) self.check_coverage("""\ def require_int(func): def wrapper(arg): @@ -1641,8 +1650,7 @@ def boosted2(arg): assert boosted2(10) == 200 """, - ([1,2,3,4,5,7,8,9,10,11,12,14,15,17,19,21,22,24,26], - [1,2,3,4,5,7,8,9,10,11,12,14, 17,19,21, 24,26]), "") + lines, "") class Py25Test(CoverageTest): diff --git a/tests/test_parser.py b/tests/test_parser.py index 6340a44bf..e6768a22e 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -166,6 +166,8 @@ def func(x=25): return 26 """) raw_statements = set([3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26]) + if env.PYBEHAVIOR.trace_decorated_def: + raw_statements.update([11, 19, 25]) self.assertEqual(parser.raw_statements, raw_statements) self.assertEqual(parser.statements, set([8])) @@ -196,13 +198,22 @@ def foo(self): def bar(self): pass """) - self.assertEqual(parser.statements, set([1, 2, 4, 8, 10])) - expected_arcs = set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8")) - expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1} + + if env.PYBEHAVIOR.trace_decorated_def: + expected_statements = {1, 2, 4, 5, 8, 9, 10} + expected_arcs = set(self.arcz_to_arcs(".1 14 45 58 89 9. .2 2. -8A A-8")) + expected_exits = {1: 1, 2: 1, 4: 1, 5: 1, 8: 1, 9: 1, 10: 1} + else: + expected_statements = {1, 2, 4, 8, 10} + expected_arcs = set(self.arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8")) + expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1} + if env.PYVERSION >= (3, 7, 0, 'beta', 5): # 3.7 changed how functions with only docstrings are numbered. expected_arcs.update(set(self.arcz_to_arcs("-46 6-4"))) expected_exits.update({6: 1}) + + self.assertEqual(parser.statements, expected_statements) self.assertEqual(parser.arcs(), expected_arcs) self.assertEqual(parser.exit_counts(), expected_exits) From 25ef14c52abbac9c136fdb674ad0fa1b5ecc847f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 3 Nov 2018 17:51:40 -0400 Subject: [PATCH 327/952] Everything should get a docstring --- tests/coveragetest.py | 1 + tests/test_html.py | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index e97f914b8..08edd62fa 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -97,6 +97,7 @@ def setUp(self): self.last_module_name = None def skip_unless_data_storage_is(self, storage): + """Skip a test for tests that are particular about the storage implementation.""" if STORAGE != storage: self.skipTest("Not using {} for data storage".format(storage)) diff --git a/tests/test_html.py b/tests/test_html.py index 8c6dc6936..ad8087fd8 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -764,6 +764,7 @@ def test_isolatin1(self): ) def make_main_etc(self): + """Make main.py and m1-m3.py for other tests.""" self.make_file("main.py", """\ import m1 import m2 From e1331826649b26465f090ad9e3100ea0870aefc3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 3 Nov 2018 18:23:53 -0400 Subject: [PATCH 328/952] Record that a bug was fixed. #700 --- CHANGES.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 426246fe6..b1c680b30 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -125,6 +125,9 @@ Version 5.0a1 --- 2018-06-05 - The location of the configuration file can now be specified with a ``COVERAGE_RCFILE`` environment variable, as requested in `issue 650`_. +- Namespace packages are supported on Python 3.7, where they used to cause + TypeErrors about path being None. Fixes `issue 700`_. + - A new warning (``already-imported``) is issued if measurable files have already been imported before coverage.py started measurement. See :ref:`cmd_warnings` for more information. @@ -136,7 +139,7 @@ Version 5.0a1 --- 2018-06-05 .. _issue 625: https://bitbucket.org/ned/coveragepy/issues/625/lstat-dominates-in-the-case-of-small .. _issue 650: https://bitbucket.org/ned/coveragepy/issues/650/allow-setting-configuration-file-location - +.. _issue 700: https://github.com/nedbat/coveragepy/issues/700 .. _changes_451: From 89d529895a21c40d1e3f3913b02540f32ab9a0b9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 3 Nov 2018 19:29:00 -0400 Subject: [PATCH 329/952] Debug-time environment variables can be set with set_env.py --- coverage/data.py | 1 + coverage/misc.py | 1 + coverage/parser.py | 2 + doc/contributing.rst | 2 +- igor.py | 2 + lab/set_env.py | 95 +++++++++++++++++++++++++++++++++++++++++++ tests/coveragetest.py | 1 + 7 files changed, 103 insertions(+), 1 deletion(-) create mode 100755 lab/set_env.py diff --git a/coverage/data.py b/coverage/data.py index 4a996e687..d60612930 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -640,6 +640,7 @@ def _has_arcs(self): return self._arcs is not None +# $set_env.py: COVERAGE_STORAGE - The storage implementation to use: sql (default), or json. STORAGE = os.environ.get("COVERAGE_STORAGE", "sql") if STORAGE == "json": CoverageData = CoverageJsonData diff --git a/coverage/misc.py b/coverage/misc.py index 1782d285a..c484d61e5 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -49,6 +49,7 @@ def _decorator(func): # Environment COVERAGE_NO_CONTRACTS=1 can turn off contracts while debugging # tests to remove noise from stack traces. +# $set_env.py: COVERAGE_NO_CONTRACTS - Disable PyContracts to simplify stack traces. USE_CONTRACTS = env.TESTING and not bool(int(os.environ.get("COVERAGE_NO_CONTRACTS", 0))) # Use PyContracts for assertion testing on parameters and returns, but only if diff --git a/coverage/parser.py b/coverage/parser.py index 6faa36e2d..6ae81c19d 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -491,6 +491,7 @@ def __new__(cls, lineno, cause=None): # Turn on AST dumps with an environment variable. +# $set_env.py: COVERAGE_AST_DUMP - Dump the AST nodes when parsing code. AST_DUMP = bool(int(os.environ.get("COVERAGE_AST_DUMP", 0))) class NodeList(object): @@ -535,6 +536,7 @@ def __init__(self, text, statements, multiline): self.missing_arc_fragments = collections.defaultdict(list) self.block_stack = [] + # $set_env.py: COVERAGE_TRACK_ARCS - Trace every arc added while parsing code. self.debug = bool(int(os.environ.get("COVERAGE_TRACK_ARCS", 0))) def analyze(self): diff --git a/doc/contributing.rst b/doc/contributing.rst index 90d730975..319a9d5cf 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -137,7 +137,7 @@ these as 1 to use them: - COVERAGE_NO_CTRACER disables the C tracer if you only want to run the PyTracer tests. -- COVEGE_AST_DUMP will dump the AST tree as it is being used during code +- COVERAGE_AST_DUMP will dump the AST tree as it is being used during code parsing. - COVERAGE_KEEP_TMP keeps the temporary directories in which tests are run. diff --git a/igor.py b/igor.py index 750ea6882..78de37a2d 100644 --- a/igor.py +++ b/igor.py @@ -74,8 +74,10 @@ def label_for_tracer(tracer): def should_skip(tracer): """Is there a reason to skip these tests?""" if tracer == "py": + # $set_env.py: COVERAGE_NO_PYTRACER - Don't run the tests under the Python tracer. skipper = os.environ.get("COVERAGE_NO_PYTRACER") else: + # $set_env.py: COVERAGE_NO_CTRACER - Don't run the tests under the C tracer. skipper = os.environ.get("COVERAGE_NO_CTRACER") if skipper: diff --git a/lab/set_env.py b/lab/set_env.py new file mode 100755 index 000000000..184649eee --- /dev/null +++ b/lab/set_env.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# +# Run this like: +# +# $ $(lab/set_env.py) +# + +import functools +import glob +import itertools +import os +import re +import sys + +pstderr = functools.partial(print, file=sys.stderr) + +SETTINGS = [] + +line_pattern = r"\$set_env.py: (\w+) - (.*)" +globs = "*/*.py *.py" + +filenames = itertools.chain.from_iterable(glob.glob(g) for g in globs.split()) +files = 0 +for filename in filenames: + files += 1 + with open(filename) as f: + for line in f: + m = re.search(line_pattern, line) + if m: + SETTINGS.append(m.groups()) +pstderr("Read {} files".format(files)) + + +def read_them(): + values = {} + for name, _ in SETTINGS: + values[name] = os.environ.get(name) + return values + +def show_them(values): + for i, (name, description) in enumerate(SETTINGS, start=1): + value = values[name] + if value is None: + eq = ' ' + value = '' + else: + eq = '=' + value = repr(value) + pstderr("{:2d}: {:>30s} {} {:12s} {}".format(i, name, eq, value, description)) + +def set_by_num(values, n, value): + setting_name = SETTINGS[int(n)-1][0] + values[setting_name] = value + +def get_new_values(values): + show = True + while True: + if show: + show_them(values) + show = False + pstderr("") + pstderr("> ", end='') + sys.stderr.flush() + try: + cmd = input("").strip().split(None, 1) + except EOFError: + pstderr("\n") + break + if not cmd: + continue + if cmd[0] == 'q': + break + if cmd[0] == 'x': + set_by_num(values, cmd[1], None) + else: + try: + nsetting = int(cmd[0]) + except ValueError: + pass + else: + set_by_num(values, nsetting, cmd[1]) + show = True + + return values + +def as_exports(values): + exports = [] + for name, value in values.items(): + if value is None: + exports.append("export -n {}".format(name)) + else: + exports.append("export {}={!r}".format(name, value)) + return "eval " + "; ".join(exports) + +print(as_exports(get_new_values(read_them()))) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 08edd62fa..d08c334e0 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -84,6 +84,7 @@ class CoverageTest( temp_dir_prefix = "coverage_test/" # Keep the temp directories if the env says to. + # $set_env.py: COVERAGE_KEEP_TMP - Keep the temp directories made by tests. keep_temp_dir = bool(int(os.getenv("COVERAGE_KEEP_TMP", 0))) def setUp(self): From 4534dca7187ce8ffa841ca53448ae1555a07cc44 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 5 Nov 2018 10:35:06 -0500 Subject: [PATCH 330/952] Minor cleanup in tox.ini --- tox.ini | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 6f2881d41..d79dff36e 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ usedevelop = True deps = # Check here for what might be out of date: # https://requires.io/github/nedbat/coveragepy/requirements/ - -rrequirements/pytest.pip + -r requirements/pytest.pip pip==18.1 setuptools==40.4.3 # gevent 1.3 causes a failure: https://bitbucket.org/ned/coveragepy/issues/663/gevent-132-on-windows-fails @@ -66,7 +66,8 @@ basepython = jython # Build the docs so we know if they are successful. We build twice: once with # -q to get all warnings, and once with -QW to get a success/fail status # return. -deps = -rdoc/requirements.pip +deps = + -r doc/requirements.pip commands = doc8 -q --ignore-path doc/_build doc CHANGES.rst README.rst sphinx-build -b html -aqE doc doc/_build/html @@ -75,7 +76,8 @@ commands = sphinx-build -b html -b linkcheck -aEnQW doc doc/_build/html [testenv:lint] -deps = -rrequirements/dev.pip +deps = + -r requirements/dev.pip setenv = LINTABLE = coverage tests igor.py setup.py __main__.py From cc12f4b7c40347b7297f7f6d938150bfde8c9ed5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 9 Nov 2018 06:09:20 -0500 Subject: [PATCH 331/952] Better error handling in set_env.py --- lab/set_env.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lab/set_env.py b/lab/set_env.py index 184649eee..c87fa48a2 100755 --- a/lab/set_env.py +++ b/lab/set_env.py @@ -52,6 +52,7 @@ def set_by_num(values, n, value): setting_name = SETTINGS[int(n)-1][0] values[setting_name] = value +PROMPT = "( val | x | q) ::> " def get_new_values(values): show = True while True: @@ -59,10 +60,10 @@ def get_new_values(values): show_them(values) show = False pstderr("") - pstderr("> ", end='') + pstderr(PROMPT, end='') sys.stderr.flush() try: - cmd = input("").strip().split(None, 1) + cmd = input("").strip().split() except EOFError: pstderr("\n") break @@ -71,14 +72,29 @@ def get_new_values(values): if cmd[0] == 'q': break if cmd[0] == 'x': - set_by_num(values, cmd[1], None) + if len(cmd) < 2: + pstderr("Need numbers of entries to delete") + continue + try: + nums = map(int, cmd[1:]) + except ValueError: + pstderr("Need numbers of entries to delete") + continue + else: + for num in nums: + set_by_num(values, num, None) else: try: - nsetting = int(cmd[0]) + num = int(cmd[0]) except ValueError: - pass + pstderr("Don't understand option {!r}".format(cmd[0])) + continue else: - set_by_num(values, nsetting, cmd[1]) + if len(cmd) >= 2: + set_by_num(values, num, " ".join(cmd[1:])) + else: + pstderr("Need a value to set") + continue show = True return values From e5dcb933ab791206040a849eacd726ffe40c348a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 9 Nov 2018 07:03:25 -0500 Subject: [PATCH 332/952] Python 3.8 will optimize away "while True:" --- coverage/env.py | 2 ++ coverage/parser.py | 21 +++++++++++++++++++++ tests/test_arcs.py | 37 ++++++++++++++++++++++++++++--------- tests/test_concurrency.py | 2 +- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/coverage/env.py b/coverage/env.py index fd98fa2ba..d97b193c1 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -39,6 +39,8 @@ class PYBEHAVIOR(object): # (old behavior)? trace_decorated_def = (PYVERSION >= (3, 8)) + # Are while-true loops optimized into absolute jumps with no loop setup? + nix_while_true = (PYVERSION >= (3, 8)) # Coverage.py specifics. diff --git a/coverage/parser.py b/coverage/parser.py index 6ae81c19d..1c19f69ee 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -711,6 +711,13 @@ def find_non_missing_node(self, node): node = None return node + # Missing nodes: _missing__* + # + # Entire statements can be optimized away by Python. They will appear in + # the AST, but not the bytecode. These functions are called (by + # find_non_missing_node) to find a node to use instead of the missing + # node. They can return None if the node should truly be gone. + def _missing__If(self, node): # If the if-node is missing, then one of its children might still be # here, but not both. So return the first of the two that isn't missing. @@ -738,6 +745,20 @@ def _missing__NodeList(self, node): return non_missing_children[0] return NodeList(non_missing_children) + def _missing__While(self, node): + body_nodes = self.find_non_missing_node(NodeList(node.body)) + if not body_nodes: + return None + # Make a synthetic While-true node. + new_while = ast.While() + new_while.lineno = body_nodes.lineno + new_while.test = ast.Name() + new_while.test.lineno = body_nodes.lineno + new_while.test.id = "True" + new_while.body = body_nodes.body + new_while.orelse = None + return new_while + def is_constant_expr(self, node): """Is this a compile-time constant?""" node_name = node.__class__.__name__ diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 0cf4dd3b1..324bd53f6 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -248,6 +248,10 @@ def test_nested_breaks(self): def test_while_true(self): # With "while 1", the loop knows it's constant. + if env.PYBEHAVIOR.nix_while_true: + arcz = ".1 13 34 45 36 63 57 7." + else: + arcz = ".1 12 23 34 45 36 63 57 7." self.check_coverage("""\ a, i = 1, 0 while 1: @@ -257,11 +261,13 @@ def test_while_true(self): i += 1 assert a == 4 and i == 3 """, - arcz=".1 12 23 34 45 36 63 57 7.", + arcz=arcz, ) # With "while True", 2.x thinks it's computation, # 3.x thinks it's constant. - if env.PY3: + if env.PYBEHAVIOR.nix_while_true: + arcz = ".1 13 34 45 36 63 57 7." + elif env.PY3: arcz = ".1 12 23 34 45 36 63 57 7." else: arcz = ".1 12 23 34 45 36 62 57 7." @@ -287,22 +293,31 @@ def method(self): """) out = self.run_command("coverage run --branch --source=. main.py") self.assertEqual(out, 'done\n') + if env.PYBEHAVIOR.nix_while_true: + num_stmts = 2 + else: + num_stmts = 3 + expected = "zero.py {n} {n} 0 0 0% 1-3".format(n=num_stmts) 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]) + self.assertIn(expected, 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." + # A continue in a while-true needs to jump to the right place. + if env.PYBEHAVIOR.nix_while_true: + arcz = ".1 13 34 45 53 46 67 7." + elif env.PY3: + arcz = ".1 12 23 34 45 53 46 67 7." else: - arcz = ".1 12 23 34 45 52 46 6." + arcz = ".1 12 23 34 45 52 46 67 7." self.check_coverage("""\ up = iter('ta') while True: char = next(up) if char == 't': continue + i = "line 6" break """, arcz=arcz @@ -685,10 +700,12 @@ def test_finally_in_loop_bug_92(self): def test_bug_212(self): # "except Exception as e" is crucial here. + # Bug 212 said that the "if exc" line was incorrectly marked as only + # partially covered. self.check_coverage("""\ def b(exc): try: - while 1: + while "no peephole".upper(): raise Exception(exc) # 4 except Exception as e: if exc != 'expected': @@ -701,8 +718,10 @@ def b(exc): except: pass """, - arcz=".1 .2 1A 23 34 45 56 67 68 7. 8. AB BC C. DE E.", - arcz_missing="C.", arcz_unpredicted="CD") + arcz=".1 .2 1A 23 34 3. 45 56 67 68 7. 8. AB BC C. DE E.", + arcz_missing="3. C.", + arcz_unpredicted="CD", + ) def test_except_finally(self): self.check_coverage("""\ diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 16d1af815..2877b7c2f 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -115,7 +115,7 @@ def __init__(self, q, qresult): def run(self): sum = 0 - while True: + while "no peephole".upper(): i = self.q.get() if i is None: break From 66e64c837fe04ff12d86ec3f196a3ca888bc9c78 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Nov 2018 08:48:37 -0500 Subject: [PATCH 333/952] Keep test tempdirs distinct, for saving and comparing test output --- igor.py | 19 ++++++++++++------- tests/coveragetest.py | 2 +- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/igor.py b/igor.py index 78de37a2d..a17463512 100644 --- a/igor.py +++ b/igor.py @@ -90,10 +90,21 @@ def should_skip(tracer): return msg +def make_env_id(tracer): + """An environment id that will keep all the test runs distinct.""" + impl = platform.python_implementation().lower() + version = "%s%s" % sys.version_info[:2] + if '__pypy__' in sys.builtin_module_names: + version += "_%s%s" % sys.pypy_version_info[:2] + env_id = "%s%s_%s" % (impl, version, tracer) + return env_id + + def run_tests(tracer, *runner_args): """The actual running of tests.""" if 'COVERAGE_TESTING' not in os.environ: os.environ['COVERAGE_TESTING'] = "True" + os.environ['COVERAGE_ENV_ID'] = make_env_id(tracer) print_banner(label_for_tracer(tracer)) return pytest.main(list(runner_args)) @@ -114,13 +125,7 @@ def run_tests_with_coverage(tracer, *runner_args): with open(pth_path, "w") as pth_file: pth_file.write("import coverage; coverage.process_startup()\n") - # Make names for the data files that keep all the test runs distinct. - impl = platform.python_implementation().lower() - version = "%s%s" % sys.version_info[:2] - if '__pypy__' in sys.builtin_module_names: - version += "_%s%s" % sys.pypy_version_info[:2] - suffix = "%s%s_%s_%s" % (impl, version, tracer, platform.platform()) - + suffix = "%s_%s" % (make_env_id(tracer), platform.platform()) os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov."+suffix) import coverage diff --git a/tests/coveragetest.py b/tests/coveragetest.py index d08c334e0..8b893e560 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -81,7 +81,7 @@ class CoverageTest( show_stderr = True # Temp dirs go to $TMPDIR/coverage_test/* - temp_dir_prefix = "coverage_test/" + temp_dir_prefix = "coverage_test/{}/".format(os.getenv('COVERAGE_ENV_ID', 'x')) # Keep the temp directories if the env says to. # $set_env.py: COVERAGE_KEEP_TMP - Keep the temp directories made by tests. From 595166ea38b970d652d10e1c6a91a09180ecd84a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Nov 2018 10:31:41 -0500 Subject: [PATCH 334/952] Always compare expected then actual --- tests/test_html.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_html.py b/tests/test_html.py index ad8087fd8..40c2cbf12 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -581,8 +581,8 @@ def filepath_to_regex(path): return regex -def compare_html(dir1, dir2): - """Specialized compare function for HTML files.""" +def compare_html(expected, actual): + """Specialized compare function for our HTML files.""" scrubs = [ (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), (r'coverage.py v[\d.abc]+', 'coverage.py vVER'), @@ -603,7 +603,7 @@ def compare_html(dir1, dir2): if env.WINDOWS: # For file paths... scrubs += [(r"\\", "/")] - return compare(dir1, dir2, file_pattern="*.html", scrubs=scrubs) + compare(expected, actual, file_pattern="*.html", scrubs=scrubs) class HtmlGoldTests(CoverageTest): @@ -624,7 +624,7 @@ def test_a(self): cov.stop() # pragma: nested cov.html_report(a, directory='out') - compare_html("out", gold_path("html/gold_a")) + compare_html(gold_path("html/gold_a"), "out") contains( "out/a_py.html", ('if 1 ' @@ -677,7 +677,7 @@ def three(): cov.stop() # pragma: nested cov.html_report(b, directory="out") - compare_html("out", gold_path("html/gold_b_branch")) + compare_html(gold_path("html/gold_b_branch"), "out") contains( "out/b_py.html", ('if x ' @@ -736,7 +736,7 @@ def test_bom(self): cov.stop() # pragma: nested cov.html_report(bom, directory="out") - compare_html("out", gold_path("html/gold_bom")) + compare_html(gold_path("html/gold_bom"), "out") contains( "out/bom_py.html", '"3×4 = 12, ÷2 = 6±0"', @@ -757,7 +757,7 @@ def test_isolatin1(self): cov.stop() # pragma: nested cov.html_report(isolatin1, directory="out") - compare_html("out", gold_path("html/gold_isolatin1")) + compare_html(gold_path("html/gold_isolatin1"), "out") contains( "out/isolatin1_py.html", '"3×4 = 12, ÷2 = 6±0"', @@ -799,7 +799,7 @@ def test_omit_1(self): cov.stop() # pragma: nested cov.html_report(directory="out") - compare_html("out", gold_path("html/gold_omit_1")) + compare_html(gold_path("html/gold_omit_1"), "out") def test_omit_2(self): self.make_main_etc() @@ -810,7 +810,7 @@ def test_omit_2(self): cov.stop() # pragma: nested cov.html_report(directory="out", omit=["m1.py"]) - compare_html("out", gold_path("html/gold_omit_2")) + compare_html(gold_path("html/gold_omit_2"), "out") def test_omit_3(self): self.make_main_etc() @@ -821,7 +821,7 @@ def test_omit_3(self): cov.stop() # pragma: nested cov.html_report(directory="out", omit=["m1.py", "m2.py"]) - compare_html("out", gold_path("html/gold_omit_3")) + compare_html(gold_path("html/gold_omit_3"), "out") def test_omit_4(self): self.make_main_etc() @@ -836,7 +836,7 @@ def test_omit_4(self): cov.stop() # pragma: nested cov.html_report(directory="out") - compare_html("out", gold_path("html/gold_omit_4")) + compare_html(gold_path("html/gold_omit_4"), "out") def test_omit_5(self): self.make_main_etc() @@ -857,7 +857,7 @@ def test_omit_5(self): cov.stop() # pragma: nested cov.html_report() - compare_html("out/omit_5", gold_path("html/gold_omit_5")) + compare_html(gold_path("html/gold_omit_5"), "out/omit_5") def test_other(self): self.make_file("src/here.py", """\ @@ -888,7 +888,7 @@ def test_other(self): for p in glob.glob("out/*_other_py.html"): os.rename(p, "out/blah_blah_other_py.html") - compare_html("out", gold_path("html/gold_other")) + compare_html(gold_path("html/gold_other"), "out") contains( "out/index.html", 'here.py', @@ -933,7 +933,7 @@ def test_partial(self): cov.stop() # pragma: nested cov.html_report(partial, directory="out") - compare_html("out", gold_path("html/gold_partial")) + compare_html(gold_path("html/gold_partial"), "out") contains( "out/partial_py.html", '

', @@ -970,8 +970,8 @@ def test_styled(self): cov.stop() # pragma: nested cov.html_report(a, directory="out", extra_css="extra.css") - compare_html("out", gold_path("html/gold_styled")) - compare("out", gold_path("html/gold_styled"), size_within=10, file_pattern="*.css") + compare_html(gold_path("html/gold_styled"), "out") + compare(gold_path("html/gold_styled"), "out", size_within=10, file_pattern="*.css") contains( "out/a_py.html", '', @@ -1040,7 +1040,7 @@ def test_unicode(self): cov.stop() # pragma: nested cov.html_report(unicode, directory="out") - compare_html("out", gold_path("html/gold_unicode")) + compare_html(gold_path("html/gold_unicode"), "out") contains( "out/unicode_py.html", '"ʎd˙ǝbɐɹǝʌoɔ"', From 116f2f6b5a47eb2380c22f242327a7169a10ea7a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Nov 2018 11:16:41 -0500 Subject: [PATCH 335/952] Comparison is not symmetric, it's expected vs actual --- tests/farm/annotate/annotate_dir.py | 2 +- tests/farm/annotate/run.py | 2 +- tests/farm/annotate/run_encodings.py | 2 +- tests/farm/annotate/run_multi.py | 2 +- tests/test_farm.py | 78 ++++++++++++++-------------- 5 files changed, 44 insertions(+), 42 deletions(-) diff --git a/tests/farm/annotate/annotate_dir.py b/tests/farm/annotate/annotate_dir.py index 9bf1b7685..0ecf71bd7 100644 --- a/tests/farm/annotate/annotate_dir.py +++ b/tests/farm/annotate/annotate_dir.py @@ -6,5 +6,5 @@ coverage run multi.py coverage annotate -d out_anno_dir """, rundir="run") -compare("run/out_anno_dir", "gold_anno_dir", "*,cover", left_extra=True) +compare("gold_anno_dir", "run/out_anno_dir", "*,cover", actual_extra=True) clean("run") diff --git a/tests/farm/annotate/run.py b/tests/farm/annotate/run.py index 6c98a770b..6a985d5e5 100644 --- a/tests/farm/annotate/run.py +++ b/tests/farm/annotate/run.py @@ -6,5 +6,5 @@ coverage run white.py coverage annotate white.py """, rundir="out") -compare("out", "gold", "*,cover") +compare("gold", "out", "*,cover") clean("out") diff --git a/tests/farm/annotate/run_encodings.py b/tests/farm/annotate/run_encodings.py index 8fd98fead..1b7389886 100644 --- a/tests/farm/annotate/run_encodings.py +++ b/tests/farm/annotate/run_encodings.py @@ -6,5 +6,5 @@ coverage run utf8.py coverage annotate utf8.py """, rundir="out_encodings") -compare("out_encodings", "gold_encodings", "*,cover") +compare("gold_encodings", "out_encodings", "*,cover") clean("out_encodings") diff --git a/tests/farm/annotate/run_multi.py b/tests/farm/annotate/run_multi.py index d3c4cf7c8..eff9ff76b 100644 --- a/tests/farm/annotate/run_multi.py +++ b/tests/farm/annotate/run_multi.py @@ -6,5 +6,5 @@ coverage run multi.py coverage annotate """, rundir="out_multi") -compare("out_multi", "gold_multi", "*,cover") +compare("gold_multi", "out_multi", "*,cover") clean("out_multi") diff --git a/tests/test_farm.py b/tests/test_farm.py index 54eeb4992..6e7b98cbb 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -48,7 +48,7 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): coverage run white.py coverage annotate white.py ''', rundir="out") - compare("out", "gold", "*,cover") + compare("gold", "out", "*,cover") clean("out") Verbs (copy, run, compare, clean) are methods in this class. FarmTestCase @@ -199,10 +199,13 @@ def versioned_directory(d): raise Exception("Directory missing: {}".format(d)) # pragma: only failure -def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scrubs=None): - """Compare files matching `file_pattern` in `dir1` and `dir2`. +def compare( + expected_dir, actual_dir, file_pattern=None, size_within=0, + actual_extra=False, scrubs=None, + ): + """Compare files matching `file_pattern` in `expected_dir` and `actual_dir`. - A version-specific subdirectory of `dir1` or `dir2` will be used if + A version-specific subdirectory of `expected_dir` will be used if it exists. `size_within` is a percentage delta for the file sizes. If non-zero, @@ -211,23 +214,22 @@ def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scru For example, size_within=10 means that the two files' sizes must be within 10 percent of each other to compare equal. - `left_extra` true means the left directory can have extra files in it + `actual_extra` true means `actual_dir` can have extra files in it without triggering an assertion. - `scrubs` is a list of pairs, regexes to find and replace to scrub the + `scrubs` is a list of pairs: regexes to find and replace to scrub the files of unimportant differences. An assertion will be raised if the directories fail one of their matches. """ - dir1 = versioned_directory(dir1) - dir2 = versioned_directory(dir2) + expected_dir = versioned_directory(expected_dir) - dc = filecmp.dircmp(dir1, dir2) + dc = filecmp.dircmp(expected_dir, actual_dir) diff_files = fnmatch_list(dc.diff_files, file_pattern) - left_only = fnmatch_list(dc.left_only, file_pattern) - right_only = fnmatch_list(dc.right_only, file_pattern) + expected_only = fnmatch_list(dc.left_only, file_pattern) + actual_only = fnmatch_list(dc.right_only, file_pattern) show_diff = True if size_within: @@ -235,19 +237,19 @@ def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scru # guide for size comparison. wrong_size = [] for f in diff_files: - with open(os.path.join(dir1, f), "rb") as fobj: - left = fobj.read() - with open(os.path.join(dir2, f), "rb") as fobj: - right = fobj.read() - size_l, size_r = len(left), len(right) - big, little = max(size_l, size_r), min(size_l, size_r) + with open(os.path.join(expected_dir, f), "rb") as fobj: + expected = fobj.read() + with open(os.path.join(actual_dir, f), "rb") as fobj: + actual = fobj.read() + size_e, size_a = len(expected), len(actual) + big, little = max(size_e, size_a), min(size_e, size_a) 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)) # pragma: only failure + # print "expected: ---\n%s\n-----\n%s" % (expected, actual) + wrong_size.append("%s (%s,%s)" % (f, size_e, size_a)) # pragma: only failure if wrong_size: print("File sizes differ between %s and %s: %s" % ( # pragma: only failure - dir1, dir2, ", ".join(wrong_size) + expected_dir, actual_dir, ", ".join(wrong_size) )) # We'll show the diff iff the files differed enough in size. @@ -259,27 +261,27 @@ def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scru # ourselves. text_diff = [] for f in diff_files: - left_file = os.path.join(dir1, f) - right_file = os.path.join(dir2, f) - with open(left_file, READ_MODE) as fobj: - left = fobj.read() - with open(right_file, READ_MODE) as fobj: - right = fobj.read() + expected_file = os.path.join(expected_dir, f) + actual_file = os.path.join(actual_dir, f) + with open(expected_file, READ_MODE) as fobj: + expected = fobj.read() + with open(actual_file, READ_MODE) as fobj: + actual = fobj.read() if scrubs: - left = scrub(left, scrubs) - right = scrub(right, scrubs) - if left != right: # pragma: only failure - text_diff.append('%s != %s' % (left_file, right_file)) - left = left.splitlines() - right = right.splitlines() - print(":::: diff {!r} and {!r}".format(left_file, right_file)) - print("\n".join(difflib.Differ().compare(left, right))) - print(":::: end diff {!r} and {!r}".format(left_file, right_file)) + expected = scrub(expected, scrubs) + actual = scrub(actual, scrubs) + if expected != actual: # pragma: only failure + text_diff.append('%s != %s' % (expected_file, actual_file)) + expected = expected.splitlines() + actual = actual.splitlines() + print(":::: diff {!r} and {!r}".format(expected_file, actual_file)) + print("\n".join(difflib.Differ().compare(expected, actual))) + print(":::: end diff {!r} and {!r}".format(expected_file, actual_file)) assert not text_diff, "Files differ: %s" % '\n'.join(text_diff) - if not left_extra: - assert not left_only, "Files in %s only: %s" % (dir1, left_only) - assert not right_only, "Files in %s only: %s" % (dir2, right_only) + assert not expected_only, "Files in %s only: %s" % (expected_dir, expected_only) + if not actual_extra: + assert not actual_only, "Files in %s only: %s" % (actual_dir, actual_only) def contains(filename, *strlist): From 19c093580d9ad80fd45dd6ea333c24c059c891bf Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Nov 2018 11:17:11 -0500 Subject: [PATCH 336/952] Refactor the XML comparison --- tests/test_xml.py | 39 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/tests/test_xml.py b/tests/test_xml.py index f3a9e70b7..58a235abb 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -309,6 +309,19 @@ def clean(text, scrub=None): return text +def compare_xml(expected, actual, **kwargs): + """Specialized compare function for our XML files.""" + source_path = coverage.files.relative_directory().rstrip(r"\/") + + scrubs=[ + (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), + (r' version="[-.\w]+"', ' version="VERSION"'), + (r'\s*.*?\s*', '%s' % re.escape(source_path)), + (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), + ] + compare(expected, actual, scrubs=scrubs, **kwargs) + + class XmlGoldTest(CoverageTest): """Tests of XML reporting that use gold files.""" @@ -326,14 +339,8 @@ def test_a_xml_1(self): import a # pragma: nested # pylint: disable=import-error cov.stop() # pragma: nested cov.xml_report(a, outfile="coverage.xml") - source_path = coverage.files.relative_directory().rstrip(r"\/") - compare(".", gold_path("html/gold_x_xml"), left_extra=True, scrubs=[ - (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), - (r' version="[-.\w]+"', ' version="VERSION"'), - (r'\s*.*?\s*', '%s' % re.escape(source_path)), - (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), - ]) + compare_xml(gold_path("html/gold_x_xml"), ".", actual_extra=True) def test_a_xml_2(self): self.make_file("a.py", """\ @@ -355,14 +362,7 @@ def test_a_xml_2(self): import a # pragma: nested # pylint: disable=import-error cov.stop() # pragma: nested cov.xml_report(a) - source_path = coverage.files.relative_directory().rstrip(r"\/") - - compare("xml_2", gold_path("html/gold_x_xml"), scrubs=[ - (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), - (r' version="[-.\w]+"', ' version="VERSION"'), - (r'\s*.*?\s*', '%s' % re.escape(source_path)), - (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), - ]) + compare_xml(gold_path("html/gold_x_xml"), "xml_2") def test_y_xml_branch(self): self.make_file("y.py", """\ @@ -380,11 +380,4 @@ def choice(x): import y # pragma: nested # pylint: disable=import-error cov.stop() # pragma: nested cov.xml_report(y, outfile="y_xml_branch/coverage.xml") - source_path = coverage.files.relative_directory().rstrip(r"\/") - - compare("y_xml_branch", gold_path("html/gold_y_xml_branch"), scrubs=[ - (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), - (r' version="[-.\w]+"', ' version="VERSION"'), - (r'\s*.*?\s*', '%s' % re.escape(source_path)), - (r'/coverage.readthedocs.io/?[-.\w/]*', '/coverage.readthedocs.io/VER'), - ]) + compare_xml(gold_path("html/gold_y_xml_branch"), "y_xml_branch") From c376209f35331b358e59c79ed3537a2ed920d677 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Nov 2018 14:11:47 -0500 Subject: [PATCH 337/952] Canonicalize the XML output https://bugs.python.org/issue34160 added retaining the user's attribute order to the XML output, which removed the sorting that used to happen. This broke our XML tests, which compare against saved gold files. This adds in a rough-and-ready canonicalization to avoid the problem. Maybe the core devs will eventually support a sort_attributes option, and I can get rid of this. --- coverage/xmlreport.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 5148b54a2..6c07337a1 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -1,3 +1,4 @@ +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt @@ -5,6 +6,7 @@ import os import os.path +import re import sys import time import xml.dom.minidom @@ -124,11 +126,8 @@ def report(self, morfs, outfile=None): xcoverage.setAttribute("branch-rate", "0") xcoverage.setAttribute("complexity", "0") - # Use the DOM to write the output file. - out = self.xml_out.toprettyxml() - if env.PY2: - out = out.encode("utf8") - outfile.write(out) + # Write the output file. + outfile.write(serialize_xml(self.xml_out)) # Return the total percentage. denom = lnum_tot + bnum_tot @@ -219,3 +218,23 @@ def xml_file(self, fr, analysis): package[2] += class_lines package[3] += class_br_hits package[4] += class_branches + + +def serialize_xml(dom): + """Serialize a minidom node to XML.""" + out = dom.toprettyxml() + if env.PY2: + out = out.encode("utf8") + # In Python 3.8, minidom lost the sorting of attributes: https://bugs.python.org/issue34160 + # For the limited kinds of XML we produce, this re-sorts them. + if env.PYVERSION >= (3, 8): + rx_attr = r' [\w-]+="[^"]*"' + rx_attrs = r'(' + rx_attr + ')+' + fixed_lines = [] + for line in out.splitlines(True): + hollow_line = re.sub(rx_attrs, u"☺", line) + attrs = sorted(re.findall(rx_attr, line)) + new_line = hollow_line.replace(u"☺", "".join(attrs)) + fixed_lines.append(new_line) + out = "".join(fixed_lines) + return out From 5dd9b8027d2f2edc46de157ffd00a93e6297a235 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 10 Nov 2018 15:35:13 -0500 Subject: [PATCH 338/952] Comparison should always be expected,actual --- tests/coveragetest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 8b893e560..151091346 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -260,18 +260,18 @@ def check_coverage( if arcs is not None: with self.delayed_assertions(): self.assert_equal_args( - analysis.arc_possibilities(), arcs, - "Possible arcs differ: minus is actual, plus is expected" + arcs, analysis.arc_possibilities(), + "Possible arcs differ: minus is expected, plus is actual" ) self.assert_equal_args( - analysis.arcs_missing(), arcs_missing, - "Missing arcs differ: minus is actual, plus is expected" + arcs_missing, analysis.arcs_missing(), + "Missing arcs differ: minus is expected, plus is actual" ) self.assert_equal_args( - analysis.arcs_unpredicted(), arcs_unpredicted, - "Unpredicted arcs differ: minus is actual, plus is expected" + arcs_unpredicted, analysis.arcs_unpredicted(), + "Unpredicted arcs differ: minus is expected, plus is actual" ) if report: From c86186ac5b305f7d72b0bd8976c0f217e604d882 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Nov 2018 07:51:23 -0500 Subject: [PATCH 339/952] Update an HTML gold test due to new peephole optimizations --- tests/farm/html/gold_partial/index.html | 22 ++++----- tests/farm/html/gold_partial/partial_py.html | 48 +++++++++----------- tests/test_html.py | 14 ++---- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/tests/farm/html/gold_partial/index.html b/tests/farm/html/gold_partial/index.html index 1948615cd..6ac435313 100644 --- a/tests/farm/html/gold_partial/index.html +++ b/tests/farm/html/gold_partial/index.html @@ -17,7 +17,7 @@

diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index c663ed330..27f0e47ae 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -297,6 +297,7 @@ td.text { color: #000080; } +/* Line contexts */ td.contexts p { margin: 0; padding: 0 .5em; @@ -305,15 +306,12 @@ td.contexts p { white-space: nowrap; position: relative; } - td.contexts p:hover { background: #eee; } - td.contexts p span.context-list { display: none; } - td.contexts p:hover span.context-list { display: block; min-width: 30em; @@ -332,11 +330,9 @@ td.contexts p:hover span.context-list { border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } - span.context-list span.context-line { - display: block; -} - + display: block; + } td.contexts p span.context-button { display: inline-block; cursor: pointer; diff --git a/tests/gold/html/styled/style.css b/tests/gold/html/styled/style.css index 41bcf2bee..27f0e47ae 100644 --- a/tests/gold/html/styled/style.css +++ b/tests/gold/html/styled/style.css @@ -297,6 +297,49 @@ td.text { color: #000080; } +/* Line contexts */ +td.contexts p { + margin: 0; + padding: 0 .5em; + color: #999999; + font-family: verdana, sans-serif; + white-space: nowrap; + position: relative; + } +td.contexts p:hover { + background: #eee; + } +td.contexts p span.context-list { + display: none; + } +td.contexts p:hover span.context-list { + display: block; + min-width: 30em; + //max-width: 50%; + white-space: normal; + float: right; + position: absolute; + top: 1.75em; + right: 1em; + height: auto; + color: #333; + background: #ffffcc; + border: 1px solid #888; + padding: .25em .5em; + z-index: 999; + border-radius: .2em; + box-shadow: #cccccc .2em .2em .2em; + } +span.context-list span.context-line { + display: block; + } +td.contexts p span.context-button { + display: inline-block; + cursor: pointer; + font-size: .8333em; /* 10/12 */ + line-height: 1em; + } + /* index styles */ #index td, #index th { text-align: right; From 368676b837045636141238ac295ac302e099611a Mon Sep 17 00:00:00 2001 From: Albertas Agejevas Date: Thu, 24 Jan 2019 09:33:23 -0500 Subject: [PATCH 508/952] Handle TestCase classes with just one test, too. --- coverage/context.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coverage/context.py b/coverage/context.py index 9ef680a3b..fb1b76acd 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -36,7 +36,8 @@ def should_start_context(frame): def should_start_context_test_function(frame): """Is this frame calling a test_* function?""" - if frame.f_code.co_name.startswith("test"): + co_name = frame.f_code.co_name + if co_name.startswith("test") or co_name == "runTest": return qualname_from_frame(frame) return None From 772620925a8e8c173fd1131a80be0f9cc84a6cce Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 24 Jan 2019 17:38:22 -0500 Subject: [PATCH 509/952] Tricky case: Thread switching is getting in the way. --- coverage/sqldata.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 9291bddd2..fe32dcdcf 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -662,7 +662,12 @@ def connect(self): # has non-ascii characters in it. Opening a relative file name avoids # a problem if the current directory has non-ascii. filename = os.path.relpath(self.filename) - self.con = sqlite3.connect(filename) + # It can happen that Python switches threads while the tracer writes + # data. The second thread will also try to write to the data, + # effectively causing a nested context. However, given the indempotent + # nature of the tracer operations, sharing a conenction among threads + # is not a problem. + self.con = sqlite3.connect(filename, check_same_thread=False) # This pragma makes writing faster. It disables rollbacks, but we never need them. # PyPy needs the .close() calls here, or sqlite gets twisted up: From 6e0f7bae413234f50b2cc3eb618185f4f8bf82ef Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Thu, 24 Jan 2019 19:54:09 -0500 Subject: [PATCH 510/952] Fix a test. --- tests/test_cmdline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 58c053960..387bf61fb 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -36,7 +36,7 @@ class BaseCmdLineTest(CoverageTest): ) _defaults.Coverage().html_report( directory=None, ignore_errors=None, include=None, omit=None, morfs=[], - skip_covered=None, title=None, contexts=None, + skip_covered=None, show_contexts=None, title=None, contexts=None, ) _defaults.Coverage().report( ignore_errors=None, include=None, omit=None, morfs=[], From 0ef160da9c07e1186f1f59f1490ff3160ffff97e Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sat, 26 Jan 2019 09:50:34 -0500 Subject: [PATCH 511/952] Only compute contexts in HTML coverage when the option is specified. (For large coverage data sets, reporting the context can be expensive.) --- coverage/data.py | 1 + coverage/html.py | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/coverage/data.py b/coverage/data.py index 71d6624dd..3d3647bb3 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -3,6 +3,7 @@ """Coverage data for coverage.py.""" +import collections import glob import itertools import json diff --git a/coverage/html.py b/coverage/html.py index 3cd47d5a7..f7dbeba47 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -3,6 +3,7 @@ """HTML reporting for coverage.py.""" +import collections import datetime import json import os @@ -214,8 +215,10 @@ def html_file(self, fr, analysis): c_mis = "mis" c_par = "par " + c_run - # Lookup line number contexts. - contexts_by_lineno = analysis.data.contexts_by_lineno(fr.filename) + contexts_by_lineno = collections.defaultdict(list) + if self.config.show_contexts: + # Lookup line number contexts. + contexts_by_lineno = analysis.data.contexts_by_lineno(fr.filename) lines = [] @@ -271,7 +274,9 @@ def html_file(self, fr, analysis): 'html': ''.join(html), 'number': lineno, 'class': ' '.join(line_class) or "pln", - 'contexts': sorted(filter(None, contexts_by_lineno[lineno])) or None, + 'contexts': \ + (sorted(filter(None, contexts_by_lineno[lineno])) or None) + if self.config.show_contexts else None, 'annotate': annotate_html, 'annotate_long': annotate_long, }) From e1574b260db45ec00074c1d9aafc3586cb3952a0 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sat, 26 Jan 2019 15:15:07 -0500 Subject: [PATCH 512/952] Add some tests for new methods. --- tests/test_data.py | 54 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_html.py | 24 +++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/tests/test_data.py b/tests/test_data.py index 417f97711..4a3db93cd 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -184,6 +184,16 @@ def test_touch_file_with_arcs(self): covdata.touch_file('zzz.py') self.assert_measured_files(covdata, MEASURED_FILES_3 + ['zzz.py']) + def test_set_query_contexts(self): + self.skip_unless_data_storage_is("sql") + covdata = CoverageData() + covdata.set_context('test_a') + covdata.add_lines(LINES_1) + covdata.set_query_contexts(['test_*']) + self.assertEqual(covdata.lines('a.py'), [1, 2]) + covdata.set_query_contexts(['other*']) + self.assertEqual(covdata.lines('a.py'), []) + def test_no_lines_vs_unmeasured_file(self): covdata = CoverageData() covdata.add_lines(LINES_1) @@ -191,6 +201,24 @@ def test_no_lines_vs_unmeasured_file(self): self.assertEqual(covdata.lines('zzz.py'), []) self.assertIsNone(covdata.lines('no_such_file.py')) + def test_lines_with_contexts(self): + self.skip_unless_data_storage_is("sql") + covdata = CoverageData() + covdata.set_context('test_a') + covdata.add_lines(LINES_1) + self.assertEqual(covdata.lines('a.py'), [1, 2]) + self.assertEqual(covdata.lines('a.py', contexts=['test*']), [1, 2]) + self.assertEqual(covdata.lines('a.py', contexts=['other*']), []) + + def test_contexts_by_lineno_with_lines(self): + self.skip_unless_data_storage_is("sql") + covdata = CoverageData() + covdata.set_context('test_a') + covdata.add_lines(LINES_1) + self.assertDictEqual( + covdata.contexts_by_lineno('a.py'), + {1: ['test_a'], 2: ['test_a']}) + def test_run_info(self): self.skip_unless_data_storage_is("json") covdata = CoverageData() @@ -225,6 +253,32 @@ def test_no_arcs_vs_unmeasured_file(self): self.assertEqual(covdata.arcs('zzz.py'), []) self.assertIsNone(covdata.arcs('no_such_file.py')) + def test_arcs_with_contexts(self): + self.skip_unless_data_storage_is("sql") + covdata = CoverageData() + covdata.set_context('test_x') + covdata.add_arcs(ARCS_3) + self.assertEqual( + covdata.arcs('x.py'), [(-1, 1), (1, 2), (2, 3), (3, -1)]) + self.assertEqual(covdata.arcs( + 'x.py', contexts=['test*']), [(-1, 1), (1, 2), (2, 3), (3, -1)]) + self.assertEqual(covdata.arcs('x.py', contexts=['other*']), []) + + def test_contexts_by_lineno_with_arcs(self): + self.skip_unless_data_storage_is("sql") + covdata = CoverageData() + covdata.set_context('test_x') + covdata.add_arcs(ARCS_3) + self.assertDictEqual( + covdata.contexts_by_lineno('x.py'), + {-1: ['test_x'], 1: ['test_x'], 2: ['test_x'], 3: ['test_x']}) + + def test_contexts_by_lineno_with_unknown_file(self): + self.skip_unless_data_storage_is("sql") + covdata = CoverageData() + self.assertDictEqual( + covdata.contexts_by_lineno('xyz.py'), {}) + def test_file_tracer_name(self): covdata = CoverageData() covdata.add_lines({ diff --git a/tests/test_html.py b/tests/test_html.py index 64546df1e..c561f99d8 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -90,6 +90,30 @@ def assert_correct_timestamp(self, html): ) +class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): + """Tests of the HTML reports with shown contexts.""" + + def setUp(self): + super(HtmlWithContextsTest, self).setUp() + + # 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(setattr, coverage, "__version__", self.real_coverage_version) + + def test_html_created(self): + # Test basic HTML generation: files should be created. + self.create_initial_files() + self.run_coverage(htmlargs={'show_contexts': True}) + + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/helper1_py.html") + self.assert_exists("htmlcov/helper2_py.html") + self.assert_exists("htmlcov/style.css") + self.assert_exists("htmlcov/coverage_html.js") + + class FileWriteTracker(object): """A fake object to track how `open` is used to write files.""" def __init__(self, written): From c399478e86892f0e7ade51873dfbe13f87ecfa83 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sun, 12 May 2019 12:17:41 -0400 Subject: [PATCH 513/952] Support for full qnames for old-style classes. --- coverage/context.py | 16 +++++++++++++++- tests/test_context.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/coverage/context.py b/coverage/context.py index fb1b76acd..d2f75db28 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -78,5 +78,19 @@ def qualname_from_frame(frame): qname = cls.__module__ + '.' + cls.__name__ + "." + fname break else: - qname = func.__module__ + '.' + fname + # Support for old-style classes. + def mro(bases): + for base in bases: + f = base.__dict__.get(fname, None) + if f is func: + return base.__module__ + '.' + base.__name__ + "." + fname + for base in bases: + qname = mro(base.__bases__) + if qname is not None: + return qname + return None + qname = mro([self.__class__]) + if qname is None: + qname = func.__module__ + '.' + fname + return qname diff --git a/tests/test_context.py b/tests/test_context.py index 0aa31e4b3..0baa51e99 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -284,4 +284,4 @@ def test_oldstyle(self): if not env.PY2: self.skipTest("Old-style classes are only in Python 2") self.assertEqual(OldStyle().meth(), "tests.test_context.OldStyle.meth") - self.assertEqual(OldChild().meth(), "tests.test_context.OldChild.meth") + self.assertEqual(OldChild().meth(), "tests.test_context.OldStyle.meth") From 5af8150018ccd76c4ac5643b3675faff67856246 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Sun, 12 May 2019 12:18:04 -0400 Subject: [PATCH 514/952] Update tests to use updated APIs. --- tests/test_api.py | 12 ++++++++---- tests/test_context.py | 13 +++++++++---- tests/test_plugins.py | 24 ++++++++++++------------ 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 107617ea2..eb13896d6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -556,8 +556,10 @@ def test_switch_context_testrunner(self): filenames = self.get_measured_filenames(data) suite_filename = filenames['testsuite.py'] - self.assertEqual([2, 8], data.lines(suite_filename, context="multiply_six")) - self.assertEqual([2, 5], data.lines(suite_filename, context="multiply_zero")) + self.assertEqual( + [2, 8], data.lines(suite_filename, contexts=["multiply_six"])) + self.assertEqual( + [2, 5], data.lines(suite_filename, contexts=["multiply_zero"])) def test_switch_context_with_static(self): # This test simulates a coverage-aware test runner, @@ -594,8 +596,10 @@ def test_switch_context_with_static(self): filenames = self.get_measured_filenames(data) suite_filename = filenames['testsuite.py'] - self.assertEqual([2, 8], data.lines(suite_filename, context="mysuite|multiply_six")) - self.assertEqual([2, 5], data.lines(suite_filename, context="mysuite|multiply_zero")) + self.assertEqual( + [2, 8], data.lines(suite_filename, contexts=["mysuite|multiply_six"])) + self.assertEqual( + [2, 5], data.lines(suite_filename, contexts=["mysuite|multiply_zero"])) def test_switch_context_unstarted(self): # Coverage must be started to switch context diff --git a/tests/test_context.py b/tests/test_context.py index 0baa51e99..24e17069d 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -172,10 +172,15 @@ def test_static_and_dynamic(self): full_names = {os.path.basename(f): f for f in data.measured_files()} fname = full_names["two_tests.py"] - self.assertCountEqual(data.measured_contexts(), ["stat", "stat|test_one", "stat|test_two"]) - self.assertCountEqual(data.lines(fname, "stat"), self.OUTER_LINES) - self.assertCountEqual(data.lines(fname, "stat|test_one"), self.TEST_ONE_LINES) - self.assertCountEqual(data.lines(fname, "stat|test_two"), self.TEST_TWO_LINES) + self.assertCountEqual( + data.measured_contexts(), + ["stat", "stat|two_tests.test_one", "stat|two_tests.test_two"]) + self.assertCountEqual( + data.lines(fname, ["stat"]), self.OUTER_LINES) + self.assertCountEqual( + data.lines(fname, ["stat|two_tests.test_one"]), self.TEST_ONE_LINES) + self.assertCountEqual( + data.lines(fname, ["stat|two_tests.test_two"]), self.TEST_TWO_LINES) class DynamicContextWithPythonTracerTest(CoverageTest): diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d6f5ac46b..a4c7a5ee8 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -997,15 +997,15 @@ def test_plugin_standalone(self): ) self.assertEqual( [2], - data.lines(filenames['rendering.py'], context="doctest:HTML_TAG"), + data.lines(filenames['rendering.py'], contexts=["doctest:HTML_TAG"]), ) self.assertEqual( [2], - data.lines(filenames['rendering.py'], context="test:HTML_TAG"), + data.lines(filenames['rendering.py'], contexts=["test:HTML_TAG"]), ) self.assertEqual( [2, 5, 8, 11], - data.lines(filenames['rendering.py'], context="test:RENDERERS"), + data.lines(filenames['rendering.py'], contexts=["test:RENDERERS"]), ) def test_static_context(self): @@ -1047,20 +1047,20 @@ def test_plugin_with_test_function(self): data = cov.get_data() filenames = self.get_measured_filenames(data) self.assertEqual( - ['', 'doctest:HTML_TAG', 'test_html_tag', 'test_renderers'], + ['', 'doctest:HTML_TAG', 'testsuite.test_html_tag', 'testsuite.test_renderers'], sorted(data.measured_contexts()), ) self.assertEqual( [2], - data.lines(filenames['rendering.py'], context="doctest:HTML_TAG"), + data.lines(filenames['rendering.py'], contexts=["doctest:HTML_TAG"]), ) self.assertEqual( [2], - data.lines(filenames['rendering.py'], context="test_html_tag"), + data.lines(filenames['rendering.py'], contexts=["testsuite.test_html_tag"]), ) self.assertEqual( [2, 5, 8, 11], - data.lines(filenames['rendering.py'], context="test_renderers"), + data.lines(filenames['rendering.py'], contexts=["testsuite.test_renderers"]), ) def test_multiple_plugins(self): @@ -1094,23 +1094,23 @@ def test_multiple_plugins(self): self.assertEqual(expected, sorted(data.measured_contexts())) self.assertEqual( [2], - data.lines(filenames['rendering.py'], context="test:HTML_TAG"), + data.lines(filenames['rendering.py'], contexts=["test:HTML_TAG"]), ) self.assertEqual( [2, 5, 8, 11], - data.lines(filenames['rendering.py'], context="test:RENDERERS"), + data.lines(filenames['rendering.py'], contexts=["test:RENDERERS"]), ) self.assertEqual( [2], - data.lines(filenames['rendering.py'], context="doctest:HTML_TAG"), + data.lines(filenames['rendering.py'], contexts=["doctest:HTML_TAG"]), ) self.assertEqual( [2, 5], - data.lines(filenames['rendering.py'], context="renderer:paragraph"), + data.lines(filenames['rendering.py'], contexts=["renderer:paragraph"]), ) self.assertEqual( [2, 8], - data.lines(filenames['rendering.py'], context="renderer:span"), + data.lines(filenames['rendering.py'], contexts=["renderer:span"]), ) From fef5badc5ef135a781d2f197fcef4f1f333bf38d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 27 May 2019 08:25:54 -0400 Subject: [PATCH 515/952] Make --contexts available for HTML reports --- coverage/cmdline.py | 3 ++- coverage/html.py | 1 + coverage/sqldata.py | 33 +++++++++++++++++++++------------ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 1c31b1e78..a6a72c325 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -346,7 +346,8 @@ def get_prog_name(self): Opts.omit, Opts.title, Opts.skip_covered, - Opts.show_contexts + Opts.show_contexts, + Opts.contexts, ] + GLOBAL_ARGS, usage="[options] [modules]", description=( diff --git a/coverage/html.py b/coverage/html.py index f7dbeba47..61edcb5d9 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -124,6 +124,7 @@ def report(self, morfs): """ assert self.config.html_dir, "must give a directory for html reporting" + self.coverage.get_data().set_query_contexts(self.config.query_contexts) # Read the status data. self.status.read(self.config.html_dir) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index fe32dcdcf..2feca6e02 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -627,23 +627,32 @@ def contexts_by_lineno(self, filename): "where arc.file_id = ? and arc.context_id = context.id" ) data = [file_id] + context_ids = self._get_query_context_ids() + if context_ids is not None: + ids_array = ', '.join('?'*len(context_ids)) + query += " and arc.context_id in (" + ids_array + ")" + data += context_ids for fromno, tono, context in con.execute(query, data): if context not in lineno_contexts_map[fromno]: lineno_contexts_map[fromno].append(context) if context not in lineno_contexts_map[tono]: lineno_contexts_map[tono].append(context) - return lineno_contexts_map - - query = ( - "select line.lineno, context.context " - "from line, context " - "where line.file_id = ? and line.context_id = context.id" - ) - data = [file_id] - for lineno, context in con.execute(query, data): - if context not in lineno_contexts_map[lineno]: - lineno_contexts_map[lineno].append(context) - return lineno_contexts_map + else: + query = ( + "select line.lineno, context.context " + "from line, context " + "where line.file_id = ? and line.context_id = context.id" + ) + data = [file_id] + context_ids = self._get_query_context_ids() + if context_ids is not None: + ids_array = ', '.join('?'*len(context_ids)) + query += " and line.context_id in (" + ids_array + ")" + data += context_ids + for lineno, context in con.execute(query, data): + if context not in lineno_contexts_map[lineno]: + lineno_contexts_map[lineno].append(context) + return lineno_contexts_map def run_infos(self): return [] # TODO From 48a3cc0cacaf2eb4e044cb3a83c83b48cd1aa67e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 27 May 2019 17:28:58 -0400 Subject: [PATCH 516/952] Include the count of contexts in the HTML report --- coverage/html.py | 1 + coverage/htmlfiles/pyfile.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/coverage/html.py b/coverage/html.py index 61edcb5d9..203169b5f 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -101,6 +101,7 @@ def __init__(self, cov, config): 'escape': escape, 'pair': pair, 'title': title, + 'len': len, '__url__': coverage.__url__, '__version__': coverage.__version__, } diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html index 460c12b58..e85ee54c4 100644 --- a/coverage/htmlfiles/pyfile.html +++ b/coverage/htmlfiles/pyfile.html @@ -93,7 +93,7 @@

{% for line in lines -%}

{#-#} {% if line.contexts -%} - ctx{#-#} + {{ line.contexts|len }} ctx{#-#} {% for context in line.contexts -%} {{context}}{#-#} From 844a3410a2887278348a8b74c6682647ef2c729d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 10 Jun 2019 19:40:17 -0400 Subject: [PATCH 517/952] CHANGES about the context-based reporting --- CHANGES.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 29120b738..a7e7633b1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,18 @@ development at the same time, like 4.5.x and 5.0. Unreleased ---------- -- Nothing yet. +- Reporting on dynamic contexts. Big thanks to Stefan Richter for the + contribution. + + - The ``--contexts`` command-line option is available on the ``report`` and + ``html`` command. It's a comma-separated list of shell-style wildcards, + selecting the contexts to report on. Only contexts matching one of the + wildcards will be included in the report. + + - The ``--show-contexts`` command-line option for the ``html`` command adds + context information to each covered line. Hovering over the "ctx" marker + at the end of the line reveals a list of the contexts that covered the + line. .. _changes_50a5: From 9a9d05c462837d7a42948937bf75344dece1ffdc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 10 Jun 2019 19:48:07 -0400 Subject: [PATCH 518/952] This was a copy of an existing test. We need to write some new ones. --- tests/test_html.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/tests/test_html.py b/tests/test_html.py index c561f99d8..49049934d 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -92,26 +92,7 @@ def assert_correct_timestamp(self, html): class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML reports with shown contexts.""" - - def setUp(self): - super(HtmlWithContextsTest, self).setUp() - - # 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(setattr, coverage, "__version__", self.real_coverage_version) - - def test_html_created(self): - # Test basic HTML generation: files should be created. - self.create_initial_files() - self.run_coverage(htmlargs={'show_contexts': True}) - - self.assert_exists("htmlcov/index.html") - self.assert_exists("htmlcov/main_file_py.html") - self.assert_exists("htmlcov/helper1_py.html") - self.assert_exists("htmlcov/helper2_py.html") - self.assert_exists("htmlcov/style.css") - self.assert_exists("htmlcov/coverage_html.js") + # TODO: write some of these. class FileWriteTracker(object): From fc2e116628f4adde13dec59511091d7724fd88d7 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Tue, 11 Jun 2019 05:53:29 -0400 Subject: [PATCH 519/952] Update CHANGES.rst Fix my name. :-) You might also add Albertas Agejevas to the credits, since he did a lot of work here too. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a7e7633b1..db7b9c72e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,7 @@ development at the same time, like 4.5.x and 5.0. Unreleased ---------- -- Reporting on dynamic contexts. Big thanks to Stefan Richter for the +- Reporting on dynamic contexts. Big thanks to Stephan Richter for the contribution. - The ``--contexts`` command-line option is available on the ``report`` and From 34d0373f173310e133417d774db79603a57e3d24 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 10 Jun 2019 20:15:14 -0400 Subject: [PATCH 520/952] Fix two pylint warnings --- coverage/sqldata.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 2feca6e02..87efbf21c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -277,7 +277,8 @@ def add_arcs(self, arc_data): file_id = self._file_id(filename, add=True) data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] con.executemany( - "insert or ignore into arc (file_id, context_id, fromno, tono) values (?, ?, ?, ?)", + "insert or ignore into arc " + "(file_id, context_id, fromno, tono) values (?, ?, ?, ?)", data, ) @@ -559,7 +560,7 @@ def set_query_contexts(self, contexts=None): def _get_query_context_ids(self, contexts=None): if contexts is not None: - if not len(contexts): + if not contexts: return None self._start_using() with self._connect() as con: From cc008dd00a2197792ae55d68b46e760c630b1189 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 11 Jun 2019 06:51:49 -0400 Subject: [PATCH 521/952] Albertas Agejevas did a lot of the context reporting work --- CHANGES.rst | 4 ++-- CONTRIBUTORS.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index db7b9c72e..f3a4252b0 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,8 +20,8 @@ development at the same time, like 4.5.x and 5.0. Unreleased ---------- -- Reporting on dynamic contexts. Big thanks to Stephan Richter for the - contribution. +- Reporting on dynamic contexts. Big thanks to Stephan Richter and Albertas + Agejevas for the contribution. - The ``--contexts`` command-line option is available on the ``report`` and ``html`` command. It's a comma-separated list of shell-style wildcards, diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index af0e03bef..972e818bd 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -5,6 +5,7 @@ Other contributions, including writing code, updating docs, and submitting useful bug reports, have been made by: Adi Roiban +Albertas Agejevas Aleksi Torhamo Alex Gaynor Alex Groce From 4f6365167a0921335e1e365a3cff05a5907943dd Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 11 Jun 2019 08:29:28 -0400 Subject: [PATCH 522/952] Docs for --contexts and --show-contexts --- doc/cmd.rst | 18 ++++++++++++++---- doc/contexts.rst | 29 +++++++++++++++++++++-------- doc/python-coverage.1.txt | 13 +++++++++++-- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/doc/cmd.rst b/doc/cmd.rst index 3551e90dc..c6d5fe273 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -363,8 +363,12 @@ command line:: ------------------------------------------------------- TOTAL 76 10 87% -The ``--skip-covered`` switch will leave out any file with 100% coverage, -letting you focus on the files that still need attention. +The ``--skip-covered`` switch will skip any file with 100% coverage, letting +you focus on the files that still need attention. + +If you have :ref:`recorded contexts `, the ``--contexts`` option lets +you choose which contexts to report on. See :ref:`context_reporting` for +details. Other common reporting options are described above in :ref:`cmd_reporting`. @@ -409,8 +413,14 @@ is a data file that is used to speed up reporting the next time. If you generate a new report into the same directory, coverage.py will skip generating unchanged pages, making the process faster. -The ``--skip-covered`` switch will leave out any file with 100% coverage, -letting you focus on the files that still need attention. +The ``--skip-covered`` switch will skip any file with 100% coverage, letting +you focus on the files that still need attention. + +If you have :ref:`recorded contexts `, the ``--contexts`` option lets +you choose which contexts to report on, and the ``--show-contexts`` option will +annotate lines with the contexts that ran them. See :ref:`context_reporting` +for details. + .. _cmd_annotation: diff --git a/doc/contexts.rst b/doc/contexts.rst index f1796aae1..e8aa4ed49 100644 --- a/doc/contexts.rst +++ b/doc/contexts.rst @@ -44,10 +44,10 @@ the configuration file. Dynamic contexts ---------------- -Dynamic contexts are found during execution. They started from the question, -"what test ran this line?," but have been generalized to allow any kind of -context tracking. As execution proceeds, the dynamic context changes -to record the context of execution. Separate data is recorded for each +Dynamic contexts are found during execution. They are most commonly used to +answer the question "what test ran this line?," but have been generalized to +allow any kind of context tracking. As execution proceeds, the dynamic context +changes to record the context of execution. Separate data is recorded for each context, so that it can be analyzed later. There are three ways to enable dynamic contexts: @@ -68,7 +68,7 @@ The ``[run] dynamic_context`` setting has only one option now. Set it to Each test function you run will be considered a separate dynamic context, and coverage data will be segregated for each. A test function is any function -whose names starts with "test". +whose name starts with "test". If you have both a static context and a dynamic context, they are joined with a pipe symbol to be recorded as a single string. @@ -80,10 +80,23 @@ then the code run by the test runner before (and between) tests will be in the empty context. +.. _context_reporting: + Context reporting ----------------- -There is currently no support for using contexts during reporting. I'm -interested to `hear your ideas`__ for what would be useful. +The ``coverage report`` and ``coverage html`` commands both accept +``--contexts`` option, a comma-separated list of shell-style wildcards. The +report will be limited to the contexts that match one of those contexts. + +The ``coverage html`` command also has ``--show-contexts``. If set, the HTML +report will include an annotation on each covered line indicating the number of +contexts that executed the line. Hovering over the annotation displays a list +of the contexts. + + +Raw data +-------- -__ https://nedbatchelder.com/site/aboutned.html +For more advanced reporting or analysis, the .coverage data file is a SQLite +database. TODO: explain the schema. diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt index a415f0805..1a06bf2ed 100644 --- a/doc/python-coverage.1.txt +++ b/doc/python-coverage.1.txt @@ -8,7 +8,7 @@ measure code coverage of Python program execution :Author: Ned Batchelder :Author: |author| -:Date: 2018-05-28 +:Date: 2019-06-11 :Copyright: Apache 2.0 license, attribution and disclaimer required. :Manual section: 1 :Manual group: Coverage.py @@ -128,6 +128,9 @@ COMMAND REFERENCE Options: + \--contexts `PAT,...` + Only include contexts that match one of the patterns. + \-d `DIR`, --directory `DIR` Write the output files to `DIR`. @@ -137,6 +140,9 @@ COMMAND REFERENCE \-i, --ignore-errors Ignore errors while reading source files. + \--show-contexts + Annotate lines with the contexts that executed them. + \--skip-covered Skip files with 100% coverage. @@ -149,6 +155,9 @@ COMMAND REFERENCE Options: + \--contexts `PAT,...` + Only include contexts that match one of the patterns. + \--fail-under `MIN` Exit with a status of 2 if the total coverage is less than `MIN`. @@ -229,7 +238,7 @@ HISTORY The |command| command is a Python program which calls the ``coverage`` Python library to do all the work. -The library was originally developed by Gareth Rees, and is now developed +It was originally developed by Gareth Rees, and is now developed by Ned Batchelder and many others. This manual page was written by |author|. From f31295ef30be63ab48766958320207af1f6dde5d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 11 Jun 2019 10:06:35 -0400 Subject: [PATCH 523/952] Polishing substitute_variables --- coverage/misc.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/coverage/misc.py b/coverage/misc.py index e34bc0006..d87a7b892 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -260,7 +260,7 @@ def dollar_replace(m): strict = bool(m.group('strict')) if strict: if word not in variables: - msg = "Variable {} is undefined: {}".format(word, text) + msg = "Variable {} is undefined: {!r}".format(word, text) raise CoverageException(msg) return variables.get(word, m.group('defval') or '') @@ -271,8 +271,7 @@ def dollar_replace(m): { # or a {-wrapped word, (?P\w+) (?: - (?P\?) # with a strict marker - | + (?P\?) | # with a strict marker -(?P[^}]*) # or a default value )? } From affdd5e765f979dba88f612097a407fc35d56e25 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 11 Jun 2019 13:45:56 -0400 Subject: [PATCH 524/952] Add comments to the SQL schema, and keep them when creating the db --- coverage/sqldata.py | 50 +++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 87efbf21c..1cc64f248 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -25,51 +25,57 @@ from coverage.misc import CoverageException, file_be_gone -# Schema versions: -# 1: Released in 5.0a2 -# 2: Added contexts - SCHEMA_VERSION = 2 SCHEMA = """ -create table coverage_schema ( +-- One row, to record the version of the schema store in this db. +CREATE TABLE coverage_schema ( version integer + -- Schema versions: + -- 1: Released in 5.0a2 + -- 2: Added contexts in 5.0a3. This is schema 2. ); -create table meta ( - has_lines boolean, - has_arcs boolean, - sys_argv text +-- One row, to record some metadata about the data +CREATE TABLE meta ( + has_lines boolean, -- Is this data recording lines? + has_arcs boolean, -- .. or branches? + sys_argv text -- The coverage command line that recorded the data. ); -create table file ( +-- A row per file measured. +CREATE TABLE file ( id integer primary key, path text, unique(path) ); -create table context ( +-- A row per context measured. +CREATE TABLE context ( id integer primary key, context text, unique(context) ); -create table line ( - file_id integer, - context_id integer, - lineno integer, +-- If recording lines, a row per context per line executed. +CREATE TABLE line ( + file_id integer, -- foreign key to `file`. + context_id integer, -- foreign key to `context`. + lineno integer, -- the line number. unique(file_id, context_id, lineno) ); -create table arc ( - file_id integer, - context_id integer, - fromno integer, - tono integer, +-- If recording branches, a row per context per from/to line transition executed. +CREATE TABLE arc ( + file_id integer, -- foreign key to `file`. + context_id integer, -- foreign key to `context`. + fromno integer, -- line number jumped from. + tono integer, -- line number jumped to. unique(file_id, context_id, fromno, tono) ); -create table tracer ( +-- A row per file indicating the tracer used for that file. +CREATE TABLE tracer ( file_id integer primary key, tracer text ); @@ -121,7 +127,7 @@ def _create_db(self): self._dbs[get_thread_id()] = Sqlite(self._filename, self._debug) with self._dbs[get_thread_id()] as db: for stmt in SCHEMA.split(';'): - stmt = " ".join(stmt.strip().split()) + stmt = stmt.strip() if stmt: db.execute(stmt) db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) From 8ad4d1e4d1734a1c1a1a4c960a67ffab1d39528a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 11 Jun 2019 14:22:26 -0400 Subject: [PATCH 525/952] Edit the CHANGES description of context reporting --- CHANGES.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index f3a4252b0..da596a972 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,15 +23,14 @@ Unreleased - Reporting on dynamic contexts. Big thanks to Stephan Richter and Albertas Agejevas for the contribution. - - The ``--contexts`` command-line option is available on the ``report`` and - ``html`` command. It's a comma-separated list of shell-style wildcards, - selecting the contexts to report on. Only contexts matching one of the - wildcards will be included in the report. - - - The ``--show-contexts`` command-line option for the ``html`` command adds - context information to each covered line. Hovering over the "ctx" marker - at the end of the line reveals a list of the contexts that covered the - line. + - The ``--contexts`` option is available on the ``report`` and ``html`` + commands. It's a comma-separated list of shell-style wildcards, selecting + the contexts to report on. Only contexts matching one of the wildcards + will be included in the report. + + - The ``--show-contexts`` option for the ``html`` command adds context + information to each covered line. Hovering over the "ctx" marker at the + end of the line reveals a list of the contexts that covered the line. .. _changes_50a5: From c210ecb765bc60b2424df284d0c101dc0bfeaf4d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 12 Jun 2019 06:36:57 -0400 Subject: [PATCH 526/952] Dynamic contexts can be disabled with 'none' --- coverage/control.py | 7 ++++--- doc/contexts.rst | 2 ++ metacov.ini | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 552f7bc27..4cd1adada 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -349,13 +349,14 @@ def _init_for_start(self): # it for the main process. self.config.parallel = True - if self.config.dynamic_context is None: + dycon = self.config.dynamic_context + if not dycon or dycon == "none": context_switchers = [] - elif self.config.dynamic_context == "test_function": + elif dycon == "test_function": context_switchers = [should_start_context_test_function] else: raise CoverageException( - "Don't understand dynamic_context setting: {!r}".format(self.config.dynamic_context) + "Don't understand dynamic_context setting: {!r}".format(dycon) ) context_switchers.extend( diff --git a/doc/contexts.rst b/doc/contexts.rst index e8aa4ed49..6a1293fd4 100644 --- a/doc/contexts.rst +++ b/doc/contexts.rst @@ -79,6 +79,8 @@ this empty context. For example, if you are recording test names as contexts, then the code run by the test runner before (and between) tests will be in the empty context. +Dynamic contexts can be explicitly disabled by setting ``dynamic_context`` to +``none``. .. _context_reporting: diff --git a/metacov.ini b/metacov.ini index 80c508e9a..3d4b7cf7c 100644 --- a/metacov.ini +++ b/metacov.ini @@ -9,6 +9,7 @@ parallel = true source = ${COVERAGE_HOME?}/coverage ${COVERAGE_HOME?}/tests +dynamic_context = none [report] # We set a different pragma so our code won't be confused with test code. From 77dc1d8c258f0a3d7dc640356b30d1f173faf68e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 12 Jun 2019 08:15:56 -0400 Subject: [PATCH 527/952] Include the template source in the hash to make development easier --- coverage/html.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coverage/html.py b/coverage/html.py index 203169b5f..6cfb85ffb 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -105,7 +105,8 @@ def __init__(self, cov, config): '__url__': coverage.__url__, '__version__': coverage.__version__, } - self.source_tmpl = Templite(read_data("pyfile.html"), self.template_globals) + self.pyfile_html_source = read_data("pyfile.html") + self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) self.data = cov.get_data() @@ -125,13 +126,13 @@ def report(self, morfs): """ assert self.config.html_dir, "must give a directory for html reporting" - self.coverage.get_data().set_query_contexts(self.config.query_contexts) # Read the status data. self.status.read(self.config.html_dir) # Check that this run used the same settings as the last run. m = Hasher() m.update(self.config) + m.update(self.pyfile_html_source) these_settings = m.hexdigest() if self.status.settings_hash() != these_settings: self.status.reset() @@ -142,6 +143,7 @@ def report(self, morfs): self.extra_css = os.path.basename(self.config.extra_css) # Process all the files. + self.coverage.get_data().set_query_contexts(self.config.query_contexts) self.report_files(self.html_file, morfs, self.config.html_dir) if not self.all_files_nums: From 8397587f7ab3621e11c93cc1a3684843c0dc4176 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 13 Jun 2019 08:00:33 -0400 Subject: [PATCH 528/952] Use sass for css generation, to get nice hover highlights on lines --- .gitignore | 1 + Makefile | 7 + coverage/htmlfiles/style.css | 572 ++++++++++------------------------ coverage/htmlfiles/style.scss | 463 +++++++++++++++++++++++++++ 4 files changed, 629 insertions(+), 414 deletions(-) create mode 100644 coverage/htmlfiles/style.scss diff --git a/.gitignore b/.gitignore index ad5864979..3a9af66f8 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ setuptools-*.egg .cache .pytest_cache .hypothesis +.ruby-version # Stuff in the test directory. zipmods.zip diff --git a/Makefile b/Makefile index dc2c2f96b..d4b0270b6 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,13 @@ sterile: clean -docker image rm -f quay.io/pypa/manylinux1_i686 quay.io/pypa/manylinux1_x86_64 +CSS = coverage/htmlfiles/style.css +SCSS = coverage/htmlfiles/style.scss + +css: $(CSS) +$(CSS): $(SCSS) + sass --style=compact --sourcemap=none --no-cache $(SCSS) $@ + LINTABLE = coverage tests igor.py setup.py __main__.py lint: diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index 27f0e47ae..643183a3a 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -1,419 +1,163 @@ +@charset "UTF-8"; /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ /* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } -/* CSS styles for coverage.py. */ - -/* Page-wide styles */ -html, body, h1, h2, h3, p, table, td, th { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-weight: inherit; - font-style: inherit; - font-size: 100%; - font-family: inherit; - vertical-align: baseline; - } - -/* Set baseline grid to 16 pt. */ -body { - font-family: georgia, serif; - font-size: 1em; - } - -html>body { - font-size: 16px; - } - -/* Set base font size to 12/16 */ -p { - font-size: .75em; /* 12/16 */ - line-height: 1.33333333em; /* 16/12 */ - } - -table { - border-collapse: collapse; - } -td { - vertical-align: top; -} -table tr.hidden { - display: none !important; - } - -p#no_rows { - display: none; - font-size: 1.2em; - } - -a.nav { - text-decoration: none; - color: inherit; - } -a.nav:hover { - text-decoration: underline; - color: inherit; - } - -/* Page structure */ -#header { - background: #f8f8f8; - width: 100%; - border-bottom: 1px solid #eee; - } - -#source { - padding: 1em; - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - } - -.indexfile #footer { - margin: 1em 3em; - } - -.pyfile #footer { - margin: 1em 1em; - } - -#footer .content { - padding: 0; - font-size: 85%; - font-family: verdana, sans-serif; - color: #666666; - font-style: italic; - } - -#index { - margin: 1em 0 0 3em; - } +body { font-family: georgia, serif; font-size: 1em; } + +html > body { font-size: 16px; } + +p { font-size: .75em; line-height: 1.33333333em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.2em; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +#header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; } + +#source { padding: 1em; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } + +.indexfile #footer { margin: 1em 3em; } + +.pyfile #footer { margin: 1em 1em; } + +#footer .content { padding: 0; font-size: 85%; font-family: verdana, sans-serif; color: #666666; font-style: italic; } + +#index { margin: 1em 0 0 3em; } /* Header styles */ -#header .content { - padding: 1em 3em; - } - -h1 { - font-size: 1.25em; - display: inline-block; -} - -#filter_container { - display: inline-block; - float: right; - margin: 0 2em 0 0; -} -#filter_container input { - width: 10em; -} - -h2.stats { - margin-top: .5em; - font-size: 1em; -} -.stats span { - border: 1px solid; - padding: .1em .25em; - margin: 0 .1em; - cursor: pointer; - border-color: #999 #ccc #ccc #999; -} -.stats span.hide_run, .stats span.hide_exc, -.stats span.hide_mis, .stats span.hide_par, -.stats span.par.hide_run.hide_par { - border-color: #ccc #999 #999 #ccc; -} -.stats span.par.hide_run { - border-color: #999 #ccc #ccc #999; -} - -.stats span.run { - background: #ddffdd; -} -.stats span.exc { - background: #eeeeee; -} -.stats span.mis { - background: #ffdddd; -} -.stats span.hide_run { - background: #eeffee; -} -.stats span.hide_exc { - background: #f5f5f5; -} -.stats span.hide_mis { - background: #ffeeee; -} -.stats span.par { - background: #ffffaa; -} -.stats span.hide_par { - background: #ffffcc; -} - -/* Help panel */ -#keyboard_icon { - float: right; - margin: 5px; - cursor: pointer; -} - -.help_panel { - position: absolute; - background: #ffffcc; - padding: .5em; - border: 1px solid #883; - display: none; -} - -.indexfile .help_panel { - width: 20em; height: 4em; -} - -.pyfile .help_panel { - width: 16em; height: 8em; -} - -.help_panel .legend { - font-style: italic; - margin-bottom: 1em; -} - -#panel_icon { - float: right; - cursor: pointer; -} - -.keyhelp { - margin: .75em; -} - -.keyhelp .key { - border: 1px solid black; - border-color: #888 #333 #333 #888; - padding: .1em .35em; - font-family: monospace; - font-weight: bold; - background: #eee; -} - -/* Source file styles */ -.linenos p { - text-align: right; - margin: 0; - padding: 0 .5em; - color: #999999; - font-family: verdana, sans-serif; -} -.linenos p.highlight { - background: #ffdd00; - } -.linenos p a { - text-decoration: none; - color: #999999; - font-size: .8333em; /* 10/12 */ - line-height: 1em; - } -.linenos p a:hover { - text-decoration: underline; - color: #999999; - } - -td.text { - width: 100%; - } -.text p { - margin: 0; - padding: 0 0 0 .5em; - border-left: 2px solid #ffffff; - white-space: pre; - position: relative; - } - -.text p.mis { - background: #ffdddd; - border-left: 2px solid #ff0000; - } -.text p.run, .text p.run.hide_par { - background: #ddffdd; - border-left: 2px solid #00ff00; - } -.text p.exc { - background: #eeeeee; - border-left: 2px solid #808080; - } -.text p.par, .text p.par.hide_run { - background: #ffffaa; - border-left: 2px solid #eeee99; - } -.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, -.text p.hide_run.hide_par { - background: inherit; - } - -.text span.annotate { - font-family: georgia; - color: #666; - float: right; - padding-right: .5em; - } -.text p.hide_par span.annotate { - display: none; - } -.text span.annotate.long { - display: none; - } -.text p:hover span.annotate.long { - display: block; - max-width: 50%; - white-space: normal; - float: right; - position: absolute; - top: 1.75em; - right: 1em; - width: 30em; - height: auto; - color: #333; - background: #ffffcc; - border: 1px solid #888; - padding: .25em .5em; - z-index: 999; - border-radius: .2em; - box-shadow: #cccccc .2em .2em .2em; - } - -/* Syntax coloring */ -.text .com { - color: green; - font-style: italic; - line-height: 1px; - } -.text .key { - font-weight: bold; - line-height: 1px; - } -.text .str { - color: #000080; - } - -/* Line contexts */ -td.contexts p { - margin: 0; - padding: 0 .5em; - color: #999999; - font-family: verdana, sans-serif; - white-space: nowrap; - position: relative; - } -td.contexts p:hover { - background: #eee; - } -td.contexts p span.context-list { - display: none; - } -td.contexts p:hover span.context-list { - display: block; - min-width: 30em; - //max-width: 50%; - white-space: normal; - float: right; - position: absolute; - top: 1.75em; - right: 1em; - height: auto; - color: #333; - background: #ffffcc; - border: 1px solid #888; - padding: .25em .5em; - z-index: 999; - border-radius: .2em; - box-shadow: #cccccc .2em .2em .2em; - } -span.context-list span.context-line { - display: block; - } -td.contexts p span.context-button { - display: inline-block; - cursor: pointer; - font-size: .8333em; /* 10/12 */ - line-height: 1em; - } - -/* index styles */ -#index td, #index th { - text-align: right; - width: 5em; - padding: .25em .5em; - border-bottom: 1px solid #eee; - } -#index th { - font-style: italic; - color: #333; - border-bottom: 1px solid #ccc; - cursor: pointer; - } -#index th:hover { - background: #eee; - border-bottom: 1px solid #999; - } -#index td.left, #index th.left { - padding-left: 0; - } -#index td.right, #index th.right { - padding-right: 0; - } -#index th.headerSortDown, #index th.headerSortUp { - border-bottom: 1px solid #000; - white-space: nowrap; - background: #eee; - } -#index th.headerSortDown:after { - content: " ↓"; -} -#index th.headerSortUp:after { - content: " ↑"; -} -#index td.name, #index th.name { - text-align: left; - width: auto; - } -#index td.name a { - text-decoration: none; - color: #000; - } -#index tr.total, -#index tr.total_dynamic { - } -#index tr.total td, -#index tr.total_dynamic td { - font-weight: bold; - border-top: 1px solid #ccc; - border-bottom: none; - } -#index tr.file:hover { - background: #eeeeee; - } -#index tr.file:hover td.name { - text-decoration: underline; - color: #000; - } - -/* scroll marker styles */ -#scroll_marker { - position: fixed; - right: 0; - top: 0; - width: 16px; - height: 100%; - background: white; - border-left: 1px solid #eee; - will-change: transform; /* for faster scrolling of fixed element in Chrome */ - } - -#scroll_marker .marker { - background: #eedddd; - position: absolute; - min-height: 3px; - width: 100%; - } +#header .content { padding: 1em 3em; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { display: inline-block; float: right; margin: 0 2em 0 0; } + +#filter_container input { width: 10em; } + +h2.stats { margin-top: .5em; font-size: 1em; } + +.stats span { border: 1px solid; padding: .1em .25em; margin: 0 .1em; cursor: pointer; border-color: #999 #ccc #ccc #999; } + +.stats span.hide_run, .stats span.hide_exc, .stats span.hide_mis, .stats span.hide_par, .stats span.par.hide_run.hide_par { border-color: #ccc #999 #999 #ccc; } + +.stats span.par.hide_run { border-color: #999 #ccc #ccc #999; } + +.stats span.run { background: #ddffdd; } + +.stats span.exc { background: #eeeeee; } + +.stats span.mis { background: #ffdddd; } + +.stats span.hide_run { background: #eeffee; } + +.stats span.hide_exc { background: #f5f5f5; } + +.stats span.hide_mis { background: #ffeeee; } + +.stats span.par { background: #ffffaa; } + +.stats span.hide_par { background: #ffffcc; } + +#keyboard_icon { float: right; margin: 5px; cursor: pointer; } + +.help_panel { position: absolute; background: #ffffcc; padding: .5em; border: 1px solid #883; display: none; } + +.indexfile .help_panel { width: 20em; height: 4em; } + +.pyfile .help_panel { width: 16em; height: 8em; } + +.help_panel .legend { font-style: italic; margin-bottom: 1em; } + +#panel_icon { float: right; cursor: pointer; } + +.keyhelp { margin: .75em; } + +.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: monospace; font-weight: bold; background: #eee; } + +.linenos p { text-align: right; margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; } + +.linenos p.highlight { background: #ffdd00; } + +.linenos p a { text-decoration: none; color: #999999; font-size: .8333em; line-height: 1em; } + +.linenos p a:hover { text-decoration: underline; color: #999999; } + +td.text { width: 100%; } + +.text p { margin: 0; padding: 0 0 0 .5em; border-left: 2px solid #ffffff; white-space: pre; position: relative; } +.text p:hover { background: #f2f2f2; } +.text p.mis { background: #ffdddd; border-left: 2px solid #ff0000; } +.text p.mis:hover { background: #f2d2d2; } +.text p.run, .text p.run.hide_par { background: #ddffdd; border-left: 2px solid #00ff00; } +.text p.run:hover, .text p.run.hide_par:hover { background: #d2f2d2; } +.text p.exc { background: #eeeeee; border-left: 2px solid #808080; } +.text p.exc:hover { background: #e2e2e2; } +.text p.par, .text p.par.hide_run { background: #ffffaa; border-left: 2px solid #eeee99; } +.text p.par:hover, .text p.par.hide_run:hover { background: #f2f2a2; } +.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, .text p.hide_run.hide_par { background: inherit; } +.text p.hide_run:hover, .text p.hide_exc:hover, .text p.hide_mis:hover, .text p.hide_par:hover, .text p.hide_run.hide_par:hover { background: #f2f2f2; } + +.text span.annotate { font-family: georgia; color: #666; float: right; padding-right: .5em; } + +.text p.hide_par span.annotate { display: none; } + +.text span.annotate.long { display: none; } + +.text p:hover span.annotate.long { display: block; max-width: 50%; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; width: 30em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } + +.text .com { color: green; font-style: italic; line-height: 1px; } + +.text .key { font-weight: bold; line-height: 1px; } + +.text .str { color: #000080; } + +td.contexts p { margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; white-space: nowrap; position: relative; } + +td.contexts p:hover { background: #eee; } + +td.contexts p span.context-list { display: none; } + +td.contexts p:hover span.context-list { display: block; min-width: 30em; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } + +span.context-list span.context-line { display: block; } + +td.contexts p span.context-button { display: inline-block; cursor: pointer; font-size: .8333em; line-height: 1em; } + +#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } + +#index th { font-style: italic; color: #333; border-bottom: 1px solid #ccc; cursor: pointer; } + +#index th:hover { background: #eee; border-bottom: 1px solid #999; } + +#index td.left, #index th.left { padding-left: 0; } + +#index td.right, #index th.right { padding-right: 0; } + +#index th.headerSortDown, #index th.headerSortUp { border-bottom: 1px solid #000; white-space: nowrap; background: #eee; } + +#index th.headerSortDown:after { content: " ↓"; } + +#index th.headerSortUp:after { content: " ↑"; } + +#index td.name, #index th.name { text-align: left; width: auto; } + +#index td.name a { text-decoration: none; color: #000; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } + +#index tr.file:hover { background: #eeeeee; } + +#index tr.file:hover td.name { text-decoration: underline; color: #000; } + +#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: white; border-left: 1px solid #eee; will-change: transform; } + +#scroll_marker .marker { background: #eedddd; position: absolute; min-height: 3px; width: 100%; } diff --git a/coverage/htmlfiles/style.scss b/coverage/htmlfiles/style.scss new file mode 100644 index 000000000..7b7d9b66a --- /dev/null +++ b/coverage/htmlfiles/style.scss @@ -0,0 +1,463 @@ +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ + +// Ignore this comment, it's for the CSS output file: +/* Don't edit this .css file. Edit the .scss file instead! */ + +// CSS styles for coverage.py. + +// Page-wide styles +html, body, h1, h2, h3, p, table, td, th { + margin: 0; + padding: 0; + border: 0; + outline: 0; + font-weight: inherit; + font-style: inherit; + font-size: 100%; + font-family: inherit; + vertical-align: baseline; + } + +// Set baseline grid to 16 pt. +body { + font-family: georgia, serif; + font-size: 1em; + } + +html>body { + font-size: 16px; + } + +// Set base font size to 12/16 +p { + font-size: .75em; // 12/16 + line-height: 1.33333333em; // 16/12 + } + +table { + border-collapse: collapse; + } +td { + vertical-align: top; +} +table tr.hidden { + display: none !important; + } + +p#no_rows { + display: none; + font-size: 1.2em; + } + +a.nav { + text-decoration: none; + color: inherit; + } +a.nav:hover { + text-decoration: underline; + color: inherit; + } + +// Page structure +#header { + background: #f8f8f8; + width: 100%; + border-bottom: 1px solid #eee; + } + +#source { + padding: 1em; + font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; + } + +.indexfile #footer { + margin: 1em 3em; + } + +.pyfile #footer { + margin: 1em 1em; + } + +#footer .content { + padding: 0; + font-size: 85%; + font-family: verdana, sans-serif; + color: #666666; + font-style: italic; + } + +#index { + margin: 1em 0 0 3em; + } + +/* Header styles */ +#header .content { + padding: 1em 3em; + } + +h1 { + font-size: 1.25em; + display: inline-block; +} + +#filter_container { + display: inline-block; + float: right; + margin: 0 2em 0 0; +} +#filter_container input { + width: 10em; +} + +h2.stats { + margin-top: .5em; + font-size: 1em; +} +.stats span { + border: 1px solid; + padding: .1em .25em; + margin: 0 .1em; + cursor: pointer; + border-color: #999 #ccc #ccc #999; +} +.stats span.hide_run, .stats span.hide_exc, +.stats span.hide_mis, .stats span.hide_par, +.stats span.par.hide_run.hide_par { + border-color: #ccc #999 #999 #ccc; +} +.stats span.par.hide_run { + border-color: #999 #ccc #ccc #999; +} + +.stats span.run { + background: #ddffdd; +} +.stats span.exc { + background: #eeeeee; +} +.stats span.mis { + background: #ffdddd; +} +.stats span.hide_run { + background: #eeffee; +} +.stats span.hide_exc { + background: #f5f5f5; +} +.stats span.hide_mis { + background: #ffeeee; +} +.stats span.par { + background: #ffffaa; +} +.stats span.hide_par { + background: #ffffcc; +} + +// Help panel +#keyboard_icon { + float: right; + margin: 5px; + cursor: pointer; +} + +.help_panel { + position: absolute; + background: #ffffcc; + padding: .5em; + border: 1px solid #883; + display: none; +} + +.indexfile .help_panel { + width: 20em; height: 4em; +} + +.pyfile .help_panel { + width: 16em; height: 8em; +} + +.help_panel .legend { + font-style: italic; + margin-bottom: 1em; +} + +#panel_icon { + float: right; + cursor: pointer; +} + +.keyhelp { + margin: .75em; +} + +.keyhelp .key { + border: 1px solid black; + border-color: #888 #333 #333 #888; + padding: .1em .35em; + font-family: monospace; + font-weight: bold; + background: #eee; +} + +// Source file styles +.linenos p { + text-align: right; + margin: 0; + padding: 0 .5em; + color: #999999; + font-family: verdana, sans-serif; +} +.linenos p.highlight { + background: #ffdd00; + } +.linenos p a { + text-decoration: none; + color: #999999; + font-size: .8333em; // 10/12 + line-height: 1em; + } +.linenos p a:hover { + text-decoration: underline; + color: #999999; + } + +td.text { + width: 100%; + } + +$mis-color: #ffdddd; +$run-color: #ddffdd; +$exc-color: #eeeeee; +$par-color: #ffffaa; + +$hover-dark-amt: 95%; +$plain-hover-color: mix(#ffffff, #000, $hover-dark-amt); +$mis-hover-color: mix($mis-color, #000, $hover-dark-amt); +$run-hover-color: mix($run-color, #000, $hover-dark-amt); +$exc-hover-color: mix($exc-color, #000, $hover-dark-amt); +$par-hover-color: mix($par-color, #000, $hover-dark-amt); + +.text p { + margin: 0; + padding: 0 0 0 .5em; + border-left: 2px solid #ffffff; + white-space: pre; + position: relative; + + &:hover { + background: $plain-hover-color; + } + + &.mis { + background: $mis-color; + border-left: 2px solid #ff0000; + + &:hover { + background: $mis-hover-color; + } + } + + &.run, &.run.hide_par { + background: $run-color; + border-left: 2px solid #00ff00; + + &:hover { + background: $run-hover-color; + } + } + + &.exc { + background: $exc-color; + border-left: 2px solid #808080; + + &:hover { + background: $exc-hover-color; + } + } + + &.par, &.par.hide_run { + background: $par-color; + border-left: 2px solid #eeee99; + + &:hover { + background: $par-hover-color; + } + } + + &.hide_run, &.hide_exc, &.hide_mis, &.hide_par, &.hide_run.hide_par { + background: inherit; + + &:hover { + background: $plain-hover-color; + } + } +} + + +.text span.annotate { + font-family: georgia; + color: #666; + float: right; + padding-right: .5em; + } +.text p.hide_par span.annotate { + display: none; + } +.text span.annotate.long { + display: none; + } +.text p:hover span.annotate.long { + display: block; + max-width: 50%; + white-space: normal; + float: right; + position: absolute; + top: 1.75em; + right: 1em; + width: 30em; + height: auto; + color: #333; + background: #ffffcc; + border: 1px solid #888; + padding: .25em .5em; + z-index: 999; + border-radius: .2em; + box-shadow: #cccccc .2em .2em .2em; + } + +// Syntax coloring +.text .com { + color: green; + font-style: italic; + line-height: 1px; + } +.text .key { + font-weight: bold; + line-height: 1px; + } +.text .str { + color: #000080; + } + +// Line contexts +td.contexts p { + margin: 0; + padding: 0 .5em; + color: #999999; + font-family: verdana, sans-serif; + white-space: nowrap; + position: relative; + } +td.contexts p:hover { + background: #eee; + } +td.contexts p span.context-list { + display: none; + } +td.contexts p:hover span.context-list { + display: block; + min-width: 30em; + //max-width: 50%; + white-space: normal; + float: right; + position: absolute; + top: 1.75em; + right: 1em; + height: auto; + color: #333; + background: #ffffcc; + border: 1px solid #888; + padding: .25em .5em; + z-index: 999; + border-radius: .2em; + box-shadow: #cccccc .2em .2em .2em; + } +span.context-list span.context-line { + display: block; + } +td.contexts p span.context-button { + display: inline-block; + cursor: pointer; + font-size: .8333em; // 10/12 + line-height: 1em; + } + +// index styles +#index td, #index th { + text-align: right; + width: 5em; + padding: .25em .5em; + border-bottom: 1px solid #eee; + } +#index th { + font-style: italic; + color: #333; + border-bottom: 1px solid #ccc; + cursor: pointer; + } +#index th:hover { + background: #eee; + border-bottom: 1px solid #999; + } +#index td.left, #index th.left { + padding-left: 0; + } +#index td.right, #index th.right { + padding-right: 0; + } +#index th.headerSortDown, #index th.headerSortUp { + border-bottom: 1px solid #000; + white-space: nowrap; + background: #eee; + } +#index th.headerSortDown:after { + content: " ↓"; +} +#index th.headerSortUp:after { + content: " ↑"; +} +#index td.name, #index th.name { + text-align: left; + width: auto; + } +#index td.name a { + text-decoration: none; + color: #000; + } +#index tr.total, +#index tr.total_dynamic { + } +#index tr.total td, +#index tr.total_dynamic td { + font-weight: bold; + border-top: 1px solid #ccc; + border-bottom: none; + } +#index tr.file:hover { + background: #eeeeee; + } +#index tr.file:hover td.name { + text-decoration: underline; + color: #000; + } + +// scroll marker styles +#scroll_marker { + position: fixed; + right: 0; + top: 0; + width: 16px; + height: 100%; + background: white; + border-left: 1px solid #eee; + will-change: transform; // for faster scrolling of fixed element in Chrome + } + +#scroll_marker .marker { + background: #eedddd; + position: absolute; + min-height: 3px; + width: 100%; + } From f9ed325deb8f25fe808a7038902f8e30191a854e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 13 Jun 2019 08:13:13 -0400 Subject: [PATCH 529/952] Update the test output css files too --- Makefile | 1 + coverage/htmlfiles/style.scss | 7 +- tests/gold/html/styled/style.css | 572 +++++++++---------------------- 3 files changed, 164 insertions(+), 416 deletions(-) diff --git a/Makefile b/Makefile index d4b0270b6..9c2a971c9 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,7 @@ SCSS = coverage/htmlfiles/style.scss css: $(CSS) $(CSS): $(SCSS) sass --style=compact --sourcemap=none --no-cache $(SCSS) $@ + cp $@ tests/gold/html/styled LINTABLE = coverage tests igor.py setup.py __main__.py diff --git a/coverage/htmlfiles/style.scss b/coverage/htmlfiles/style.scss index 7b7d9b66a..bc5a1503c 100644 --- a/coverage/htmlfiles/style.scss +++ b/coverage/htmlfiles/style.scss @@ -1,11 +1,14 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ /* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ +// CSS styles for coverage.py HTML reports. + +// When you edit this file, you need to run "make css" to get the CSS file +// generated, and then check in both the .scss and the .css files. + // Ignore this comment, it's for the CSS output file: /* Don't edit this .css file. Edit the .scss file instead! */ -// CSS styles for coverage.py. - // Page-wide styles html, body, h1, h2, h3, p, table, td, th { margin: 0; diff --git a/tests/gold/html/styled/style.css b/tests/gold/html/styled/style.css index 27f0e47ae..643183a3a 100644 --- a/tests/gold/html/styled/style.css +++ b/tests/gold/html/styled/style.css @@ -1,419 +1,163 @@ +@charset "UTF-8"; /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ /* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } -/* CSS styles for coverage.py. */ - -/* Page-wide styles */ -html, body, h1, h2, h3, p, table, td, th { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-weight: inherit; - font-style: inherit; - font-size: 100%; - font-family: inherit; - vertical-align: baseline; - } - -/* Set baseline grid to 16 pt. */ -body { - font-family: georgia, serif; - font-size: 1em; - } - -html>body { - font-size: 16px; - } - -/* Set base font size to 12/16 */ -p { - font-size: .75em; /* 12/16 */ - line-height: 1.33333333em; /* 16/12 */ - } - -table { - border-collapse: collapse; - } -td { - vertical-align: top; -} -table tr.hidden { - display: none !important; - } - -p#no_rows { - display: none; - font-size: 1.2em; - } - -a.nav { - text-decoration: none; - color: inherit; - } -a.nav:hover { - text-decoration: underline; - color: inherit; - } - -/* Page structure */ -#header { - background: #f8f8f8; - width: 100%; - border-bottom: 1px solid #eee; - } - -#source { - padding: 1em; - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - } - -.indexfile #footer { - margin: 1em 3em; - } - -.pyfile #footer { - margin: 1em 1em; - } - -#footer .content { - padding: 0; - font-size: 85%; - font-family: verdana, sans-serif; - color: #666666; - font-style: italic; - } - -#index { - margin: 1em 0 0 3em; - } +body { font-family: georgia, serif; font-size: 1em; } + +html > body { font-size: 16px; } + +p { font-size: .75em; line-height: 1.33333333em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.2em; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +#header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; } + +#source { padding: 1em; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } + +.indexfile #footer { margin: 1em 3em; } + +.pyfile #footer { margin: 1em 1em; } + +#footer .content { padding: 0; font-size: 85%; font-family: verdana, sans-serif; color: #666666; font-style: italic; } + +#index { margin: 1em 0 0 3em; } /* Header styles */ -#header .content { - padding: 1em 3em; - } - -h1 { - font-size: 1.25em; - display: inline-block; -} - -#filter_container { - display: inline-block; - float: right; - margin: 0 2em 0 0; -} -#filter_container input { - width: 10em; -} - -h2.stats { - margin-top: .5em; - font-size: 1em; -} -.stats span { - border: 1px solid; - padding: .1em .25em; - margin: 0 .1em; - cursor: pointer; - border-color: #999 #ccc #ccc #999; -} -.stats span.hide_run, .stats span.hide_exc, -.stats span.hide_mis, .stats span.hide_par, -.stats span.par.hide_run.hide_par { - border-color: #ccc #999 #999 #ccc; -} -.stats span.par.hide_run { - border-color: #999 #ccc #ccc #999; -} - -.stats span.run { - background: #ddffdd; -} -.stats span.exc { - background: #eeeeee; -} -.stats span.mis { - background: #ffdddd; -} -.stats span.hide_run { - background: #eeffee; -} -.stats span.hide_exc { - background: #f5f5f5; -} -.stats span.hide_mis { - background: #ffeeee; -} -.stats span.par { - background: #ffffaa; -} -.stats span.hide_par { - background: #ffffcc; -} - -/* Help panel */ -#keyboard_icon { - float: right; - margin: 5px; - cursor: pointer; -} - -.help_panel { - position: absolute; - background: #ffffcc; - padding: .5em; - border: 1px solid #883; - display: none; -} - -.indexfile .help_panel { - width: 20em; height: 4em; -} - -.pyfile .help_panel { - width: 16em; height: 8em; -} - -.help_panel .legend { - font-style: italic; - margin-bottom: 1em; -} - -#panel_icon { - float: right; - cursor: pointer; -} - -.keyhelp { - margin: .75em; -} - -.keyhelp .key { - border: 1px solid black; - border-color: #888 #333 #333 #888; - padding: .1em .35em; - font-family: monospace; - font-weight: bold; - background: #eee; -} - -/* Source file styles */ -.linenos p { - text-align: right; - margin: 0; - padding: 0 .5em; - color: #999999; - font-family: verdana, sans-serif; -} -.linenos p.highlight { - background: #ffdd00; - } -.linenos p a { - text-decoration: none; - color: #999999; - font-size: .8333em; /* 10/12 */ - line-height: 1em; - } -.linenos p a:hover { - text-decoration: underline; - color: #999999; - } - -td.text { - width: 100%; - } -.text p { - margin: 0; - padding: 0 0 0 .5em; - border-left: 2px solid #ffffff; - white-space: pre; - position: relative; - } - -.text p.mis { - background: #ffdddd; - border-left: 2px solid #ff0000; - } -.text p.run, .text p.run.hide_par { - background: #ddffdd; - border-left: 2px solid #00ff00; - } -.text p.exc { - background: #eeeeee; - border-left: 2px solid #808080; - } -.text p.par, .text p.par.hide_run { - background: #ffffaa; - border-left: 2px solid #eeee99; - } -.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, -.text p.hide_run.hide_par { - background: inherit; - } - -.text span.annotate { - font-family: georgia; - color: #666; - float: right; - padding-right: .5em; - } -.text p.hide_par span.annotate { - display: none; - } -.text span.annotate.long { - display: none; - } -.text p:hover span.annotate.long { - display: block; - max-width: 50%; - white-space: normal; - float: right; - position: absolute; - top: 1.75em; - right: 1em; - width: 30em; - height: auto; - color: #333; - background: #ffffcc; - border: 1px solid #888; - padding: .25em .5em; - z-index: 999; - border-radius: .2em; - box-shadow: #cccccc .2em .2em .2em; - } - -/* Syntax coloring */ -.text .com { - color: green; - font-style: italic; - line-height: 1px; - } -.text .key { - font-weight: bold; - line-height: 1px; - } -.text .str { - color: #000080; - } - -/* Line contexts */ -td.contexts p { - margin: 0; - padding: 0 .5em; - color: #999999; - font-family: verdana, sans-serif; - white-space: nowrap; - position: relative; - } -td.contexts p:hover { - background: #eee; - } -td.contexts p span.context-list { - display: none; - } -td.contexts p:hover span.context-list { - display: block; - min-width: 30em; - //max-width: 50%; - white-space: normal; - float: right; - position: absolute; - top: 1.75em; - right: 1em; - height: auto; - color: #333; - background: #ffffcc; - border: 1px solid #888; - padding: .25em .5em; - z-index: 999; - border-radius: .2em; - box-shadow: #cccccc .2em .2em .2em; - } -span.context-list span.context-line { - display: block; - } -td.contexts p span.context-button { - display: inline-block; - cursor: pointer; - font-size: .8333em; /* 10/12 */ - line-height: 1em; - } - -/* index styles */ -#index td, #index th { - text-align: right; - width: 5em; - padding: .25em .5em; - border-bottom: 1px solid #eee; - } -#index th { - font-style: italic; - color: #333; - border-bottom: 1px solid #ccc; - cursor: pointer; - } -#index th:hover { - background: #eee; - border-bottom: 1px solid #999; - } -#index td.left, #index th.left { - padding-left: 0; - } -#index td.right, #index th.right { - padding-right: 0; - } -#index th.headerSortDown, #index th.headerSortUp { - border-bottom: 1px solid #000; - white-space: nowrap; - background: #eee; - } -#index th.headerSortDown:after { - content: " ↓"; -} -#index th.headerSortUp:after { - content: " ↑"; -} -#index td.name, #index th.name { - text-align: left; - width: auto; - } -#index td.name a { - text-decoration: none; - color: #000; - } -#index tr.total, -#index tr.total_dynamic { - } -#index tr.total td, -#index tr.total_dynamic td { - font-weight: bold; - border-top: 1px solid #ccc; - border-bottom: none; - } -#index tr.file:hover { - background: #eeeeee; - } -#index tr.file:hover td.name { - text-decoration: underline; - color: #000; - } - -/* scroll marker styles */ -#scroll_marker { - position: fixed; - right: 0; - top: 0; - width: 16px; - height: 100%; - background: white; - border-left: 1px solid #eee; - will-change: transform; /* for faster scrolling of fixed element in Chrome */ - } - -#scroll_marker .marker { - background: #eedddd; - position: absolute; - min-height: 3px; - width: 100%; - } +#header .content { padding: 1em 3em; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { display: inline-block; float: right; margin: 0 2em 0 0; } + +#filter_container input { width: 10em; } + +h2.stats { margin-top: .5em; font-size: 1em; } + +.stats span { border: 1px solid; padding: .1em .25em; margin: 0 .1em; cursor: pointer; border-color: #999 #ccc #ccc #999; } + +.stats span.hide_run, .stats span.hide_exc, .stats span.hide_mis, .stats span.hide_par, .stats span.par.hide_run.hide_par { border-color: #ccc #999 #999 #ccc; } + +.stats span.par.hide_run { border-color: #999 #ccc #ccc #999; } + +.stats span.run { background: #ddffdd; } + +.stats span.exc { background: #eeeeee; } + +.stats span.mis { background: #ffdddd; } + +.stats span.hide_run { background: #eeffee; } + +.stats span.hide_exc { background: #f5f5f5; } + +.stats span.hide_mis { background: #ffeeee; } + +.stats span.par { background: #ffffaa; } + +.stats span.hide_par { background: #ffffcc; } + +#keyboard_icon { float: right; margin: 5px; cursor: pointer; } + +.help_panel { position: absolute; background: #ffffcc; padding: .5em; border: 1px solid #883; display: none; } + +.indexfile .help_panel { width: 20em; height: 4em; } + +.pyfile .help_panel { width: 16em; height: 8em; } + +.help_panel .legend { font-style: italic; margin-bottom: 1em; } + +#panel_icon { float: right; cursor: pointer; } + +.keyhelp { margin: .75em; } + +.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: monospace; font-weight: bold; background: #eee; } + +.linenos p { text-align: right; margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; } + +.linenos p.highlight { background: #ffdd00; } + +.linenos p a { text-decoration: none; color: #999999; font-size: .8333em; line-height: 1em; } + +.linenos p a:hover { text-decoration: underline; color: #999999; } + +td.text { width: 100%; } + +.text p { margin: 0; padding: 0 0 0 .5em; border-left: 2px solid #ffffff; white-space: pre; position: relative; } +.text p:hover { background: #f2f2f2; } +.text p.mis { background: #ffdddd; border-left: 2px solid #ff0000; } +.text p.mis:hover { background: #f2d2d2; } +.text p.run, .text p.run.hide_par { background: #ddffdd; border-left: 2px solid #00ff00; } +.text p.run:hover, .text p.run.hide_par:hover { background: #d2f2d2; } +.text p.exc { background: #eeeeee; border-left: 2px solid #808080; } +.text p.exc:hover { background: #e2e2e2; } +.text p.par, .text p.par.hide_run { background: #ffffaa; border-left: 2px solid #eeee99; } +.text p.par:hover, .text p.par.hide_run:hover { background: #f2f2a2; } +.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, .text p.hide_run.hide_par { background: inherit; } +.text p.hide_run:hover, .text p.hide_exc:hover, .text p.hide_mis:hover, .text p.hide_par:hover, .text p.hide_run.hide_par:hover { background: #f2f2f2; } + +.text span.annotate { font-family: georgia; color: #666; float: right; padding-right: .5em; } + +.text p.hide_par span.annotate { display: none; } + +.text span.annotate.long { display: none; } + +.text p:hover span.annotate.long { display: block; max-width: 50%; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; width: 30em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } + +.text .com { color: green; font-style: italic; line-height: 1px; } + +.text .key { font-weight: bold; line-height: 1px; } + +.text .str { color: #000080; } + +td.contexts p { margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; white-space: nowrap; position: relative; } + +td.contexts p:hover { background: #eee; } + +td.contexts p span.context-list { display: none; } + +td.contexts p:hover span.context-list { display: block; min-width: 30em; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } + +span.context-list span.context-line { display: block; } + +td.contexts p span.context-button { display: inline-block; cursor: pointer; font-size: .8333em; line-height: 1em; } + +#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } + +#index th { font-style: italic; color: #333; border-bottom: 1px solid #ccc; cursor: pointer; } + +#index th:hover { background: #eee; border-bottom: 1px solid #999; } + +#index td.left, #index th.left { padding-left: 0; } + +#index td.right, #index th.right { padding-right: 0; } + +#index th.headerSortDown, #index th.headerSortUp { border-bottom: 1px solid #000; white-space: nowrap; background: #eee; } + +#index th.headerSortDown:after { content: " ↓"; } + +#index th.headerSortUp:after { content: " ↑"; } + +#index td.name, #index th.name { text-align: left; width: auto; } + +#index td.name a { text-decoration: none; color: #000; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } + +#index tr.file:hover { background: #eeeeee; } + +#index tr.file:hover td.name { text-decoration: underline; color: #000; } + +#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: white; border-left: 1px solid #eee; will-change: transform; } + +#scroll_marker .marker { background: #eedddd; position: absolute; min-height: 3px; width: 100%; } From 5914ae7eedf654eb1a89d44f71fedce9626530cb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 13 Jun 2019 20:06:19 -0400 Subject: [PATCH 530/952] More sass goodness --- coverage/htmlfiles/style.css | 28 +-- coverage/htmlfiles/style.scss | 347 ++++++++++++++++--------------- tests/gold/html/styled/style.css | 28 +-- 3 files changed, 179 insertions(+), 224 deletions(-) diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index 643183a3a..839669528 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -19,7 +19,6 @@ table tr.hidden { display: none !important; } p#no_rows { display: none; font-size: 1.2em; } a.nav { text-decoration: none; color: inherit; } - a.nav:hover { text-decoration: underline; color: inherit; } #header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; } @@ -40,31 +39,20 @@ a.nav:hover { text-decoration: underline; color: inherit; } h1 { font-size: 1.25em; display: inline-block; } #filter_container { display: inline-block; float: right; margin: 0 2em 0 0; } - #filter_container input { width: 10em; } h2.stats { margin-top: .5em; font-size: 1em; } .stats span { border: 1px solid; padding: .1em .25em; margin: 0 .1em; cursor: pointer; border-color: #999 #ccc #ccc #999; } - .stats span.hide_run, .stats span.hide_exc, .stats span.hide_mis, .stats span.hide_par, .stats span.par.hide_run.hide_par { border-color: #ccc #999 #999 #ccc; } - .stats span.par.hide_run { border-color: #999 #ccc #ccc #999; } - .stats span.run { background: #ddffdd; } - .stats span.exc { background: #eeeeee; } - .stats span.mis { background: #ffdddd; } - .stats span.hide_run { background: #eeffee; } - .stats span.hide_exc { background: #f5f5f5; } - .stats span.hide_mis { background: #ffeeee; } - .stats span.par { background: #ffffaa; } - .stats span.hide_par { background: #ffffcc; } #keyboard_icon { float: right; margin: 5px; cursor: pointer; } @@ -80,15 +68,11 @@ h2.stats { margin-top: .5em; font-size: 1em; } #panel_icon { float: right; cursor: pointer; } .keyhelp { margin: .75em; } - .keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: monospace; font-weight: bold; background: #eee; } .linenos p { text-align: right; margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; } - .linenos p.highlight { background: #ffdd00; } - .linenos p a { text-decoration: none; color: #999999; font-size: .8333em; line-height: 1em; } - .linenos p a:hover { text-decoration: underline; color: #999999; } td.text { width: 100%; } @@ -107,31 +91,22 @@ td.text { width: 100%; } .text p.hide_run:hover, .text p.hide_exc:hover, .text p.hide_mis:hover, .text p.hide_par:hover, .text p.hide_run.hide_par:hover { background: #f2f2f2; } .text span.annotate { font-family: georgia; color: #666; float: right; padding-right: .5em; } - .text p.hide_par span.annotate { display: none; } - .text span.annotate.long { display: none; } - .text p:hover span.annotate.long { display: block; max-width: 50%; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; width: 30em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } .text .com { color: green; font-style: italic; line-height: 1px; } - .text .key { font-weight: bold; line-height: 1px; } - .text .str { color: #000080; } td.contexts p { margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; white-space: nowrap; position: relative; } - td.contexts p:hover { background: #eee; } - td.contexts p span.context-list { display: none; } - td.contexts p:hover span.context-list { display: block; min-width: 30em; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } +td.contexts p span.context-button { display: inline-block; cursor: pointer; font-size: .8333em; line-height: 1em; } span.context-list span.context-line { display: block; } -td.contexts p span.context-button { display: inline-block; cursor: pointer; font-size: .8333em; line-height: 1em; } - #index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } #index th { font-style: italic; color: #333; border-bottom: 1px solid #ccc; cursor: pointer; } @@ -159,5 +134,4 @@ td.contexts p span.context-button { display: inline-block; cursor: pointer; font #index tr.file:hover td.name { text-decoration: underline; color: #000; } #scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: white; border-left: 1px solid #eee; will-change: transform; } - #scroll_marker .marker { background: #eedddd; position: absolute; min-height: 3px; width: 100%; } diff --git a/coverage/htmlfiles/style.scss b/coverage/htmlfiles/style.scss index bc5a1503c..168d68780 100644 --- a/coverage/htmlfiles/style.scss +++ b/coverage/htmlfiles/style.scss @@ -20,67 +20,68 @@ html, body, h1, h2, h3, p, table, td, th { font-size: 100%; font-family: inherit; vertical-align: baseline; - } +} // Set baseline grid to 16 pt. body { font-family: georgia, serif; font-size: 1em; - } +} html>body { font-size: 16px; - } +} // Set base font size to 12/16 p { font-size: .75em; // 12/16 line-height: 1.33333333em; // 16/12 - } +} table { border-collapse: collapse; - } +} td { vertical-align: top; } table tr.hidden { display: none !important; - } +} p#no_rows { display: none; font-size: 1.2em; - } +} a.nav { text-decoration: none; color: inherit; + + &:hover { + text-decoration: underline; + color: inherit; } -a.nav:hover { - text-decoration: underline; - color: inherit; - } +} // Page structure #header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; - } +} #source { padding: 1em; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - } +} .indexfile #footer { margin: 1em 3em; - } +} .pyfile #footer { margin: 1em 1em; - } +} #footer .content { padding: 0; @@ -88,16 +89,16 @@ a.nav:hover { font-family: verdana, sans-serif; color: #666666; font-style: italic; - } +} #index { margin: 1em 0 0 3em; - } +} /* Header styles */ #header .content { padding: 1em 3em; - } +} h1 { font-size: 1.25em; @@ -108,9 +109,10 @@ h1 { display: inline-block; float: right; margin: 0 2em 0 0; -} -#filter_container input { - width: 10em; + + input { + width: 10em; + } } h2.stats { @@ -123,39 +125,38 @@ h2.stats { margin: 0 .1em; cursor: pointer; border-color: #999 #ccc #ccc #999; -} -.stats span.hide_run, .stats span.hide_exc, -.stats span.hide_mis, .stats span.hide_par, -.stats span.par.hide_run.hide_par { - border-color: #ccc #999 #999 #ccc; -} -.stats span.par.hide_run { - border-color: #999 #ccc #ccc #999; -} -.stats span.run { - background: #ddffdd; -} -.stats span.exc { - background: #eeeeee; -} -.stats span.mis { - background: #ffdddd; -} -.stats span.hide_run { - background: #eeffee; -} -.stats span.hide_exc { - background: #f5f5f5; -} -.stats span.hide_mis { - background: #ffeeee; -} -.stats span.par { - background: #ffffaa; -} -.stats span.hide_par { - background: #ffffcc; + &.hide_run, &.hide_exc, &.hide_mis, &.hide_par, &.par.hide_run.hide_par { + border-color: #ccc #999 #999 #ccc; + } + &.par.hide_run { + border-color: #999 #ccc #ccc #999; + } + + &.run { + background: #ddffdd; + } + &.exc { + background: #eeeeee; + } + &.mis { + background: #ffdddd; + } + &.hide_run { + background: #eeffee; + } + &.hide_exc { + background: #f5f5f5; + } + &.hide_mis { + background: #ffeeee; + } + &.par { + background: #ffffaa; + } + &.hide_par { + background: #ffffcc; + } } // Help panel @@ -193,15 +194,15 @@ h2.stats { .keyhelp { margin: .75em; -} -.keyhelp .key { - border: 1px solid black; - border-color: #888 #333 #333 #888; - padding: .1em .35em; - font-family: monospace; - font-weight: bold; - background: #eee; + .key { + border: 1px solid black; + border-color: #888 #333 #333 #888; + padding: .1em .35em; + font-family: monospace; + font-weight: bold; + background: #eee; + } } // Source file styles @@ -211,24 +212,25 @@ h2.stats { padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; -} -.linenos p.highlight { - background: #ffdd00; - } -.linenos p a { - text-decoration: none; - color: #999999; - font-size: .8333em; // 10/12 - line-height: 1em; + + &.highlight { + background: #ffdd00; } -.linenos p a:hover { - text-decoration: underline; - color: #999999; + a { + text-decoration: none; + color: #999999; + font-size: .8333em; // 10/12 + line-height: 1em; + &:hover { + text-decoration: underline; + color: #999999; + } } +} td.text { width: 100%; - } +} $mis-color: #ffdddd; $run-color: #ddffdd; @@ -299,93 +301,99 @@ $par-hover-color: mix($par-color, #000, $hover-dark-amt); } -.text span.annotate { - font-family: georgia; - color: #666; - float: right; - padding-right: .5em; - } -.text p.hide_par span.annotate { - display: none; - } -.text span.annotate.long { - display: none; - } -.text p:hover span.annotate.long { - display: block; - max-width: 50%; - white-space: normal; - float: right; - position: absolute; - top: 1.75em; - right: 1em; - width: 30em; - height: auto; - color: #333; - background: #ffffcc; - border: 1px solid #888; - padding: .25em .5em; - z-index: 999; - border-radius: .2em; - box-shadow: #cccccc .2em .2em .2em; +.text { + span.annotate { + font-family: georgia; + color: #666; + float: right; + padding-right: .5em; + } + p.hide_par span.annotate { + display: none; + } + span.annotate.long { + display: none; + } + p:hover span.annotate.long { + display: block; + max-width: 50%; + white-space: normal; + float: right; + position: absolute; + top: 1.75em; + right: 1em; + width: 30em; + height: auto; + color: #333; + background: #ffffcc; + border: 1px solid #888; + padding: .25em .5em; + z-index: 999; + border-radius: .2em; + box-shadow: #cccccc .2em .2em .2em; } +} // Syntax coloring -.text .com { - color: green; - font-style: italic; - line-height: 1px; +.text { + .com { + color: green; + font-style: italic; + line-height: 1px; } -.text .key { - font-weight: bold; - line-height: 1px; + .key { + font-weight: bold; + line-height: 1px; } -.text .str { - color: #000080; + .str { + color: #000080; } +} // Line contexts -td.contexts p { - margin: 0; - padding: 0 .5em; - color: #999999; - font-family: verdana, sans-serif; - white-space: nowrap; - position: relative; - } -td.contexts p:hover { - background: #eee; - } -td.contexts p span.context-list { - display: none; - } -td.contexts p:hover span.context-list { - display: block; - min-width: 30em; - //max-width: 50%; - white-space: normal; - float: right; - position: absolute; - top: 1.75em; - right: 1em; - height: auto; - color: #333; - background: #ffffcc; - border: 1px solid #888; - padding: .25em .5em; - z-index: 999; - border-radius: .2em; - box-shadow: #cccccc .2em .2em .2em; +td.contexts { + p { + margin: 0; + padding: 0 .5em; + color: #999999; + font-family: verdana, sans-serif; + white-space: nowrap; + position: relative; + &:hover { + background: #eee; + } + span.context-list { + display: none; + } + &:hover span.context-list { + display: block; + min-width: 30em; + white-space: normal; + float: right; + position: absolute; + top: 1.75em; + right: 1em; + height: auto; + color: #333; + background: #ffffcc; + border: 1px solid #888; + padding: .25em .5em; + z-index: 999; + border-radius: .2em; + box-shadow: #cccccc .2em .2em .2em; + } + span.context-button { + display: inline-block; + cursor: pointer; + font-size: .8333em; // 10/12 + line-height: 1em; + } } +} + span.context-list span.context-line { display: block; - } -td.contexts p span.context-button { - display: inline-block; - cursor: pointer; - font-size: .8333em; // 10/12 - line-height: 1em; - } +} // index styles #index td, #index th { @@ -393,28 +401,28 @@ td.contexts p span.context-button { width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; - } +} #index th { font-style: italic; color: #333; border-bottom: 1px solid #ccc; cursor: pointer; - } +} #index th:hover { background: #eee; border-bottom: 1px solid #999; - } +} #index td.left, #index th.left { padding-left: 0; - } +} #index td.right, #index th.right { padding-right: 0; - } +} #index th.headerSortDown, #index th.headerSortUp { border-bottom: 1px solid #000; white-space: nowrap; background: #eee; - } +} #index th.headerSortDown:after { content: " ↓"; } @@ -424,27 +432,26 @@ td.contexts p span.context-button { #index td.name, #index th.name { text-align: left; width: auto; - } +} #index td.name a { text-decoration: none; color: #000; - } -#index tr.total, -#index tr.total_dynamic { - } +} +#index tr.total, #index tr.total_dynamic { } // ??? + #index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; - } +} #index tr.file:hover { background: #eeeeee; - } +} #index tr.file:hover td.name { text-decoration: underline; color: #000; - } +} // scroll marker styles #scroll_marker { @@ -456,11 +463,11 @@ td.contexts p span.context-button { background: white; border-left: 1px solid #eee; will-change: transform; // for faster scrolling of fixed element in Chrome - } -#scroll_marker .marker { - background: #eedddd; - position: absolute; - min-height: 3px; - width: 100%; + .marker { + background: #eedddd; + position: absolute; + min-height: 3px; + width: 100%; } +} diff --git a/tests/gold/html/styled/style.css b/tests/gold/html/styled/style.css index 643183a3a..839669528 100644 --- a/tests/gold/html/styled/style.css +++ b/tests/gold/html/styled/style.css @@ -19,7 +19,6 @@ table tr.hidden { display: none !important; } p#no_rows { display: none; font-size: 1.2em; } a.nav { text-decoration: none; color: inherit; } - a.nav:hover { text-decoration: underline; color: inherit; } #header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; } @@ -40,31 +39,20 @@ a.nav:hover { text-decoration: underline; color: inherit; } h1 { font-size: 1.25em; display: inline-block; } #filter_container { display: inline-block; float: right; margin: 0 2em 0 0; } - #filter_container input { width: 10em; } h2.stats { margin-top: .5em; font-size: 1em; } .stats span { border: 1px solid; padding: .1em .25em; margin: 0 .1em; cursor: pointer; border-color: #999 #ccc #ccc #999; } - .stats span.hide_run, .stats span.hide_exc, .stats span.hide_mis, .stats span.hide_par, .stats span.par.hide_run.hide_par { border-color: #ccc #999 #999 #ccc; } - .stats span.par.hide_run { border-color: #999 #ccc #ccc #999; } - .stats span.run { background: #ddffdd; } - .stats span.exc { background: #eeeeee; } - .stats span.mis { background: #ffdddd; } - .stats span.hide_run { background: #eeffee; } - .stats span.hide_exc { background: #f5f5f5; } - .stats span.hide_mis { background: #ffeeee; } - .stats span.par { background: #ffffaa; } - .stats span.hide_par { background: #ffffcc; } #keyboard_icon { float: right; margin: 5px; cursor: pointer; } @@ -80,15 +68,11 @@ h2.stats { margin-top: .5em; font-size: 1em; } #panel_icon { float: right; cursor: pointer; } .keyhelp { margin: .75em; } - .keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: monospace; font-weight: bold; background: #eee; } .linenos p { text-align: right; margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; } - .linenos p.highlight { background: #ffdd00; } - .linenos p a { text-decoration: none; color: #999999; font-size: .8333em; line-height: 1em; } - .linenos p a:hover { text-decoration: underline; color: #999999; } td.text { width: 100%; } @@ -107,31 +91,22 @@ td.text { width: 100%; } .text p.hide_run:hover, .text p.hide_exc:hover, .text p.hide_mis:hover, .text p.hide_par:hover, .text p.hide_run.hide_par:hover { background: #f2f2f2; } .text span.annotate { font-family: georgia; color: #666; float: right; padding-right: .5em; } - .text p.hide_par span.annotate { display: none; } - .text span.annotate.long { display: none; } - .text p:hover span.annotate.long { display: block; max-width: 50%; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; width: 30em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } .text .com { color: green; font-style: italic; line-height: 1px; } - .text .key { font-weight: bold; line-height: 1px; } - .text .str { color: #000080; } td.contexts p { margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; white-space: nowrap; position: relative; } - td.contexts p:hover { background: #eee; } - td.contexts p span.context-list { display: none; } - td.contexts p:hover span.context-list { display: block; min-width: 30em; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } +td.contexts p span.context-button { display: inline-block; cursor: pointer; font-size: .8333em; line-height: 1em; } span.context-list span.context-line { display: block; } -td.contexts p span.context-button { display: inline-block; cursor: pointer; font-size: .8333em; line-height: 1em; } - #index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } #index th { font-style: italic; color: #333; border-bottom: 1px solid #ccc; cursor: pointer; } @@ -159,5 +134,4 @@ td.contexts p span.context-button { display: inline-block; cursor: pointer; font #index tr.file:hover td.name { text-decoration: underline; color: #000; } #scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: white; border-left: 1px solid #eee; will-change: transform; } - #scroll_marker .marker { background: #eedddd; position: absolute; min-height: 3px; width: 100%; } From cb1b65a4ad1b8302304706b1775401d165486d02 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 14 Jun 2019 06:50:25 -0400 Subject: [PATCH 531/952] Make it easier for me to measure test_function contexts --- igor.py | 5 +++-- metacov.ini | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/igor.py b/igor.py index b0ef6876d..1bb7d19ff 100644 --- a/igor.py +++ b/igor.py @@ -75,7 +75,7 @@ def should_skip(tracer): """Is there a reason to skip these tests?""" if tracer == "py": # $set_env.py: COVERAGE_NO_PYTRACER - Don't run the tests under the Python tracer. - skipper = os.environ.get("COVERAGE_NO_PYTRACER") + skipper = os.environ.get("COVERAGE_NO_PYTRACER") or os.environ.get("COVERAGE_CONTEXT") else: # $set_env.py: COVERAGE_NO_CTRACER - Don't run the tests under the C tracer. skipper = os.environ.get("COVERAGE_NO_CTRACER") @@ -174,7 +174,8 @@ def do_combine_html(): cov.load() cov.combine() cov.save() - cov.html_report() + show_contexts = bool(os.environ.get('COVERAGE_CONTEXT')) + cov.html_report(show_contexts=show_contexts) cov.xml_report() diff --git a/metacov.ini b/metacov.ini index 3d4b7cf7c..d00019f85 100644 --- a/metacov.ini +++ b/metacov.ini @@ -7,9 +7,10 @@ branch = true data_file = ${COVERAGE_METAFILE?} parallel = true source = - ${COVERAGE_HOME?}/coverage - ${COVERAGE_HOME?}/tests -dynamic_context = none + ${COVERAGE_HOME-.}/coverage + ${COVERAGE_HOME-.}/tests +# $set_env.py: COVERAGE_CONTEXT - set to 'test_function' for who-tests-what +dynamic_context = ${COVERAGE_CONTEXT-none} [report] # We set a different pragma so our code won't be confused with test code. From 730ef8b6047bcbe8b92675b5a8bcbe1060585d16 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 15 Jun 2019 15:32:43 -0400 Subject: [PATCH 532/952] Reporter base class shouldn't be responsible for directories --- coverage/annotate.py | 6 ++++-- coverage/html.py | 7 +++++-- coverage/report.py | 11 ++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/coverage/annotate.py b/coverage/annotate.py index 1d2280046..3380dccc3 100644 --- a/coverage/annotate.py +++ b/coverage/annotate.py @@ -8,7 +8,7 @@ import re from coverage.files import flat_rootname -from coverage.misc import isolate_module +from coverage.misc import ensure_dir, isolate_module from coverage.report import Reporter os = isolate_module(os) @@ -49,8 +49,9 @@ def report(self, morfs, directory=None): See `coverage.report()` for arguments. """ + self.directory = directory self.coverage.get_data() - self.report_files(self.annotate_file, morfs, directory) + self.report_files(self.annotate_file, morfs) def annotate_file(self, fr, analysis): """Annotate a single file. @@ -63,6 +64,7 @@ def annotate_file(self, fr, analysis): excluded = sorted(analysis.excluded) if self.directory: + ensure_dir(self.directory) dest_file = os.path.join(self.directory, flat_rootname(fr.relative_filename())) if dest_file.endswith("_py"): dest_file = dest_file[:-3] + ".py" diff --git a/coverage/html.py b/coverage/html.py index 6cfb85ffb..629f7d6a2 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -15,7 +15,7 @@ from coverage.backward import iitems from coverage.data import add_data_to_hash from coverage.files import flat_rootname -from coverage.misc import CoverageException, file_be_gone, Hasher, isolate_module +from coverage.misc import CoverageException, ensure_dir, file_be_gone, Hasher, isolate_module from coverage.report import Reporter from coverage.results import Numbers from coverage.templite import Templite @@ -142,9 +142,11 @@ def report(self, morfs): if self.config.extra_css: self.extra_css = os.path.basename(self.config.extra_css) + self.directory = self.config.html_dir + # Process all the files. self.coverage.get_data().set_query_contexts(self.config.query_contexts) - self.report_files(self.html_file, morfs, self.config.html_dir) + self.report_files(self.html_file, morfs) if not self.all_files_nums: raise CoverageException("No data to report.") @@ -182,6 +184,7 @@ def html_file(self, fr, analysis): """Generate an HTML file for one source file.""" rootname = flat_rootname(fr.relative_filename()) html_filename = rootname + ".html" + ensure_dir(self.directory) html_path = os.path.join(self.directory, html_filename) # Get the numbers for this file. diff --git a/coverage/report.py b/coverage/report.py index 6f87bbf2b..c694d79b9 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -7,7 +7,7 @@ import warnings from coverage.files import prep_patterns, FnmatchMatcher -from coverage.misc import CoverageException, NoSource, NotPython, ensure_dir, isolate_module +from coverage.misc import CoverageException, NoSource, NotPython, isolate_module os = isolate_module(os) @@ -25,10 +25,6 @@ def __init__(self, coverage, config): self.coverage = coverage self.config = config - # The directory into which to place the report, used by some derived - # classes. - self.directory = None - # Our method find_file_reporters used to set an attribute that other # code could read. That's been refactored away, but some third parties # were using that attribute. We'll continue to support it in a noisy @@ -65,7 +61,7 @@ def find_file_reporters(self, morfs): self._file_reporters = sorted(reporters) return self._file_reporters - def report_files(self, report_fn, morfs, directory=None): + def report_files(self, report_fn, morfs): """Run a reporting function on a number of morfs. `report_fn` is called for each relative morf in `morfs`. It is called @@ -82,9 +78,6 @@ def report_files(self, report_fn, morfs, directory=None): if not file_reporters: raise CoverageException("No data to report.") - self.directory = directory - ensure_dir(self.directory) - for fr in file_reporters: try: report_fn(fr, self.coverage._analyze(fr)) From 591ac5b27974e10386559f54b7c5a81e69e5210d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 15 Jun 2019 15:33:21 -0400 Subject: [PATCH 533/952] Remove a property that has been deprecated for three years --- coverage/report.py | 10 ---------- tests/test_api.py | 19 ------------------- 2 files changed, 29 deletions(-) diff --git a/coverage/report.py b/coverage/report.py index c694d79b9..4f81d99ce 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -4,7 +4,6 @@ """Reporter foundation for coverage.py.""" import os -import warnings from coverage.files import prep_patterns, FnmatchMatcher from coverage.misc import CoverageException, NoSource, NotPython, isolate_module @@ -31,15 +30,6 @@ def __init__(self, coverage, config): # way for now. self._file_reporters = [] - @property - def file_reporters(self): - """Keep .file_reporters working for private-grabbing tools.""" - warnings.warn( - "Report.file_reporters will no longer be available in Coverage.py 4.2", - DeprecationWarning, - ) - return self._file_reporters - def find_file_reporters(self, morfs): """Find the FileReporters we'll report on. diff --git a/tests/test_api.py b/tests/test_api.py index eb13896d6..a034c8286 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -8,14 +8,12 @@ import os.path import sys import textwrap -import warnings import coverage from coverage import env from coverage.backward import StringIO, import_local_file from coverage.data import line_counts from coverage.misc import CoverageException -from coverage.report import Reporter from tests.coveragetest import CoverageTest, CoverageTestMethodsMixin, TESTS_DIR, UsingModulesMixin @@ -904,20 +902,3 @@ def test_pytestcov_parallel(self): def test_pytestcov_parallel_append(self): self.pretend_to_be_pytestcov(append=True) - - -class ReporterDeprecatedAttributeTest(CoverageTest): - """Test that Reporter.file_reporters has been deprecated.""" - - run_in_temp_dir = False - - def test_reporter_file_reporters(self): - rep = Reporter(None, None) - - with warnings.catch_warnings(record=True) as warns: - warnings.simplefilter("always") - # Accessing this attribute will raise a DeprecationWarning. - rep.file_reporters # pylint: disable=pointless-statement - - self.assertEqual(len(warns), 1) - self.assertTrue(issubclass(warns[0].category, DeprecationWarning)) From 3a04a88e88a92882ef92cf6778e42eaf3d4001f4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 16 Jun 2019 15:38:26 -0400 Subject: [PATCH 534/952] Refactor text reporting to use the same code paths as other reports --- CHANGES.rst | 6 +++ coverage/cmdline.py | 7 ++- coverage/report.py | 3 +- coverage/summary.py | 110 ++++++++++++++++++------------------------ doc/cmd.rst | 5 ++ tests/test_summary.py | 25 +++++----- 6 files changed, 78 insertions(+), 78 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index da596a972..6b9bdede3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -32,6 +32,12 @@ Unreleased information to each covered line. Hovering over the "ctx" marker at the end of the line reveals a list of the contexts that covered the line. +- Error handling during reporting has changed slightly. All reporting methods + now behave the same. The ``--ignore-errors`` option keeps errors from + stopping the reporting, but files that couldn't parse as Python will always + be reported as warnings. As with other warnings, you can suppress them with + the ``[run] disable_warnings`` configuration setting. + .. _changes_50a5: diff --git a/coverage/cmdline.py b/coverage/cmdline.py index a6a72c325..c3d011d94 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -20,7 +20,7 @@ from coverage.data import line_counts from coverage.debug import info_formatter, info_header from coverage.execfile import PyRunner -from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource +from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource, output_encoding from coverage.results import should_fail_under @@ -788,7 +788,10 @@ def main(argv=None): status = ERR except BaseCoverageException as err: # A controlled error inside coverage.py: print the message to the user. - print(err) + msg = err.args[0] + if env.PY2: + msg = msg.encode(output_encoding()) + print(msg) status = ERR except SystemExit as err: # The user called `sys.exit()`. Exit with their argument, if any. diff --git a/coverage/report.py b/coverage/report.py index 4f81d99ce..2675cf0fe 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -81,6 +81,7 @@ def report_files(self, report_fn, morfs): # should_be_python() method. if fr.should_be_python(): if self.config.ignore_errors: - self.coverage._warn("Could not parse Python file {0}".format(fr.filename)) + msg = "Could not parse Python file {0}".format(fr.filename) + self.coverage._warn(msg, slug="couldnt-parse") else: raise diff --git a/coverage/summary.py b/coverage/summary.py index 9e0ccbfd9..5b197f605 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -8,7 +8,7 @@ from coverage import env from coverage.report import Reporter from coverage.results import Numbers -from coverage.misc import NotPython, CoverageException, output_encoding, StopEverything +from coverage.misc import NotPython, CoverageException, output_encoding class SummaryReporter(Reporter): @@ -16,8 +16,19 @@ class SummaryReporter(Reporter): def __init__(self, coverage, config): super(SummaryReporter, self).__init__(coverage, config) - data = coverage.get_data() - self.branches = data.has_arcs() + self.branches = coverage.get_data().has_arcs() + self.outfile = None + self.fr_analysis = [] + self.skipped_count = 0 + self.total = Numbers() + self.fmt_err = u"%s %s: %s" + + def writeout(self, line): + """Write a line to the output, adding a newline.""" + if env.PY2: + line = line.encode(output_encoding()) + self.outfile.write(line.rstrip()) + self.outfile.write("\n") def report(self, morfs, outfile=None): """Writes a report summarizing coverage statistics per module. @@ -26,53 +37,13 @@ def report(self, morfs, outfile=None): for native strings (bytes on Python 2, Unicode on Python 3). """ - if outfile is None: - outfile = sys.stdout - - def writeout(line): - """Write a line to the output, adding a newline.""" - if env.PY2: - line = line.encode(output_encoding()) - outfile.write(line.rstrip()) - outfile.write("\n") - - fr_analysis = [] - skipped_count = 0 - total = Numbers() - - fmt_err = u"%s %s: %s" + self.outfile = outfile or sys.stdout self.coverage.get_data().set_query_contexts(self.config.query_contexts) - for fr in self.find_file_reporters(morfs): - try: - analysis = self.coverage._analyze(fr) - nums = analysis.numbers - total += nums - - if self.config.skip_covered: - # Don't report on 100% files. - no_missing_lines = (nums.n_missing == 0) - no_missing_branches = (nums.n_partial_branches == 0) - if no_missing_lines and no_missing_branches: - skipped_count += 1 - continue - fr_analysis.append((fr, analysis)) - except StopEverything: - # Don't report this on single files, it's a systemic problem. - raise - except Exception: - report_it = not self.config.ignore_errors - if report_it: - typ, msg = sys.exc_info()[:2] - # NotPython is only raised by PythonFileReporter, which has a - # should_be_python() method. - if issubclass(typ, NotPython) and not fr.should_be_python(): - report_it = False - if report_it: - writeout(fmt_err % (fr.relative_filename(), typ.__name__, msg)) + self.report_files(self.report_one_file, morfs) # Prepare the formatting strings, header, and column sorting. - max_name = max([len(fr.relative_filename()) for (fr, analysis) in fr_analysis] + [5]) + max_name = max([len(fr.relative_filename()) for (fr, analysis) in self.fr_analysis] + [5]) fmt_name = u"%%- %ds " % max_name fmt_skip_covered = u"\n%s file%s skipped due to complete coverage." @@ -94,15 +65,15 @@ def writeout(line): column_order.update(dict(branch=3, brpart=4)) # Write the header - writeout(header) - writeout(rule) + self.writeout(header) + self.writeout(rule) # `lines` is a list of pairs, (line text, line values). The line text # is a string that will be printed, and line values is a tuple of # sortable values. lines = [] - for (fr, analysis) in fr_analysis: + for (fr, analysis) in self.fr_analysis: try: nums = analysis.numbers @@ -125,7 +96,7 @@ def writeout(line): if typ is NotPython and not fr.should_be_python(): report_it = False if report_it: - writeout(fmt_err % (fr.relative_filename(), typ.__name__, msg)) + self.writeout(self.fmt_err % (fr.relative_filename(), typ.__name__, msg)) # Sort the lines and write them out. if getattr(self.config, 'sort', None): @@ -135,24 +106,39 @@ def writeout(line): lines.sort(key=lambda l: (l[1][position], l[0])) for line in lines: - writeout(line[0]) + self.writeout(line[0]) # Write a TOTAl line if we had more than one file. - if total.n_files > 1: - writeout(rule) - args = ("TOTAL", total.n_statements, total.n_missing) + if self.total.n_files > 1: + self.writeout(rule) + args = ("TOTAL", self.total.n_statements, self.total.n_missing) if self.branches: - args += (total.n_branches, total.n_partial_branches) - args += (total.pc_covered_str,) + args += (self.total.n_branches, self.total.n_partial_branches) + args += (self.total.pc_covered_str,) if self.config.show_missing: args += ("",) - writeout(fmt_coverage % args) + self.writeout(fmt_coverage % args) # Write other final lines. - if not total.n_files and not skipped_count: + if not self.total.n_files and not self.skipped_count: raise CoverageException("No data to report.") - if self.config.skip_covered and skipped_count: - writeout(fmt_skip_covered % (skipped_count, 's' if skipped_count > 1 else '')) - - return total.n_statements and total.pc_covered + if self.config.skip_covered and self.skipped_count: + self.writeout( + fmt_skip_covered % (self.skipped_count, 's' if self.skipped_count > 1 else '') + ) + + return self.total.n_statements and self.total.pc_covered + + def report_one_file(self, fr, analysis): + """Report on just one file, the callback from report().""" + nums = analysis.numbers + self.total += nums + + no_missing_lines = (nums.n_missing == 0) + no_missing_branches = (nums.n_partial_branches == 0) + if self.config.skip_covered and no_missing_lines and no_missing_branches: + # Don't report on 100% files. + self.skipped_count += 1 + else: + self.fr_analysis.append((fr, analysis)) diff --git a/doc/cmd.rst b/doc/cmd.rst index c6d5fe273..427384932 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -143,6 +143,11 @@ Warnings During execution, coverage.py may warn you about conditions it detects that could affect the measurement process. The possible warnings include: +* "Could not parse Python file XXX (couldnt-parse)" + + During reporting, a file was thought to be Python, but it couldn't be parsed + as Python. + * "Trace function changed, measurement is likely wrong: XXX (trace-changed)" Coverage measurement depends on a Python setting called the trace function. diff --git a/tests/test_summary.py b/tests/test_summary.py index eed77dba7..f7824ce4c 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -426,13 +426,11 @@ def foo(): def test_report_skip_covered_no_data(self): report = self.report_from_command("coverage report --skip-covered") - # Name Stmts Miss Branch BrPart Cover - # ------------------------------------------- # No data to report. - self.assertEqual(self.line_count(report), 3, report) + self.assertEqual(self.line_count(report), 1, report) squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[2], "No data to report.") + self.assertEqual(squeezed[0], "No data to report.") def test_report_precision(self): self.make_file(".coveragerc", """\ @@ -487,7 +485,7 @@ def test_dotpy_not_python(self): 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 + # Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # Name Stmts Miss Cover # ---------------------------- # No data to report. @@ -498,8 +496,8 @@ def test_dotpy_not_python(self): # The actual error message varies version to version errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg) self.assertEqual( + "Couldn't parse 'mycode.py' as Python source: 'error' at line 1", errmsg, - "mycode.py NotPython: Couldn't parse 'mycode.py' as Python source: 'error' at line 1" ) def test_accenteddotpy_not_python(self): @@ -514,7 +512,7 @@ def test_accenteddotpy_not_python(self): 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 + # Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # Name Stmts Miss Cover # ---------------------------- # No data to report. @@ -524,27 +522,28 @@ def test_accenteddotpy_not_python(self): errmsg = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", errmsg) # The actual error message varies version to version 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" - ) + expected = u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1" if env.PY2: expected = expected.encode(output_encoding()) self.assertEqual(expected, errmsg) def test_dotpy_not_python_ignored(self): # We run a .py file, and when reporting, we can't parse it as Python, - # but we've said to ignore errors, so there's no error reported. + # but we've said to ignore errors, so there's no error reported, + # though we still get a warning. self.make_mycode() self.run_command("coverage run mycode.py") self.make_file("mycode.py", "This isn't python at all!") report = self.report_from_command("coverage report -i mycode.py") + # Coverage.py warning: Could not parse Python file blah_blah/mycode.py (couldnt-parse) # Name Stmts Miss Cover # ---------------------------- + # No data to report. - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 4) self.assertIn('No data to report.', report) + self.assertIn('(couldnt-parse)', report) def test_dothtml_not_python(self): # We run a .html file, and when reporting, we can't parse it as From 237ac7a9518d6764a4af5a203d7e07caa7f5891a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 16 Jun 2019 17:00:16 -0400 Subject: [PATCH 535/952] Get rid of Reporter base class --- coverage/annotate.py | 10 ++-- coverage/html.py | 12 +++-- coverage/report.py | 122 +++++++++++++++--------------------------- coverage/summary.py | 10 ++-- coverage/xmlreport.py | 10 ++-- 5 files changed, 67 insertions(+), 97 deletions(-) diff --git a/coverage/annotate.py b/coverage/annotate.py index 3380dccc3..35b213608 100644 --- a/coverage/annotate.py +++ b/coverage/annotate.py @@ -9,12 +9,12 @@ from coverage.files import flat_rootname from coverage.misc import ensure_dir, isolate_module -from coverage.report import Reporter +from coverage.report import get_analysis_to_report os = isolate_module(os) -class AnnotateReporter(Reporter): +class AnnotateReporter(object): """Generate annotated source files showing line coverage. This reporter creates annotated copies of the measured source files. Each @@ -37,7 +37,8 @@ class AnnotateReporter(Reporter): """ def __init__(self, coverage, config): - super(AnnotateReporter, self).__init__(coverage, config) + self.coverage = coverage + self.config = config self.directory = None blank_re = re.compile(r"\s*(#|$)") @@ -51,7 +52,8 @@ def report(self, morfs, directory=None): """ self.directory = directory self.coverage.get_data() - self.report_files(self.annotate_file, morfs) + for fr, analysis in get_analysis_to_report(self.coverage, self.config, morfs): + self.annotate_file(fr, analysis) def annotate_file(self, fr, analysis): """Annotate a single file. diff --git a/coverage/html.py b/coverage/html.py index 629f7d6a2..627a19a05 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -16,7 +16,7 @@ from coverage.data import add_data_to_hash from coverage.files import flat_rootname from coverage.misc import CoverageException, ensure_dir, file_be_gone, Hasher, isolate_module -from coverage.report import Reporter +from coverage.report import get_analysis_to_report from coverage.results import Numbers from coverage.templite import Templite @@ -74,7 +74,7 @@ def write_html(fname, html): fout.write(html.encode('ascii', 'xmlcharrefreplace')) -class HtmlReporter(Reporter): +class HtmlReporter(object): """HTML reporting.""" # These files will be copied from the htmlfiles directory to the output @@ -92,7 +92,8 @@ class HtmlReporter(Reporter): ] def __init__(self, cov, config): - super(HtmlReporter, self).__init__(cov, config) + self.coverage = cov + self.config = config self.directory = None title = self.config.html_title if env.PY2: @@ -108,7 +109,7 @@ def __init__(self, cov, config): self.pyfile_html_source = read_data("pyfile.html") self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) - self.data = cov.get_data() + self.data = self.coverage.get_data() self.files = [] self.all_files_nums = [] @@ -146,7 +147,8 @@ def report(self, morfs): # Process all the files. self.coverage.get_data().set_query_contexts(self.config.query_contexts) - self.report_files(self.html_file, morfs) + for fr, analysis in get_analysis_to_report(self.coverage, self.config, morfs): + self.html_file(fr, analysis) if not self.all_files_nums: raise CoverageException("No data to report.") diff --git a/coverage/report.py b/coverage/report.py index 2675cf0fe..2c2e0198c 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -3,85 +3,47 @@ """Reporter foundation for coverage.py.""" -import os - from coverage.files import prep_patterns, FnmatchMatcher -from coverage.misc import CoverageException, NoSource, NotPython, isolate_module - -os = isolate_module(os) - - -class Reporter(object): - """A base class for all reporters.""" - - def __init__(self, coverage, config): - """Create a reporter. - - `coverage` is the coverage instance. `config` is an instance of - CoverageConfig, for controlling all sorts of behavior. - - """ - self.coverage = coverage - self.config = config - - # Our method find_file_reporters used to set an attribute that other - # code could read. That's been refactored away, but some third parties - # were using that attribute. We'll continue to support it in a noisy - # way for now. - self._file_reporters = [] - - def find_file_reporters(self, morfs): - """Find the FileReporters we'll report on. - - `morfs` is a list of modules or file names. - - Returns a list of FileReporters. - - """ - reporters = self.coverage._get_file_reporters(morfs) - - if self.config.report_include: - matcher = FnmatchMatcher(prep_patterns(self.config.report_include)) - reporters = [fr for fr in reporters if matcher.match(fr.filename)] - - if self.config.report_omit: - matcher = FnmatchMatcher(prep_patterns(self.config.report_omit)) - reporters = [fr for fr in reporters if not matcher.match(fr.filename)] - - self._file_reporters = sorted(reporters) - return self._file_reporters - - def report_files(self, report_fn, morfs): - """Run a reporting function on a number of morfs. - - `report_fn` is called for each relative morf in `morfs`. It is called - as:: - - report_fn(file_reporter, analysis) - - where `file_reporter` is the `FileReporter` for the morf, and - `analysis` is the `Analysis` for the morf. - - """ - file_reporters = self.find_file_reporters(morfs) - - if not file_reporters: - raise CoverageException("No data to report.") - - for fr in file_reporters: - try: - report_fn(fr, self.coverage._analyze(fr)) - except NoSource: - if not self.config.ignore_errors: +from coverage.misc import CoverageException, NoSource, NotPython + + +def get_analysis_to_report(coverage, config, morfs): + """Get the files to report on. + + For each morf in `morfs`, if it should be reported on (based on the omit + and include configuration options), yield a pair, the `FileReporter` and + `Analysis` for the morf. + + """ + file_reporters = coverage._get_file_reporters(morfs) + + if config.report_include: + matcher = FnmatchMatcher(prep_patterns(config.report_include)) + file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)] + + if config.report_omit: + matcher = FnmatchMatcher(prep_patterns(config.report_omit)) + file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)] + + if not file_reporters: + raise CoverageException("No data to report.") + + for fr in sorted(file_reporters): + try: + analysis = coverage._analyze(fr) + except NoSource: + if not config.ignore_errors: + raise + except NotPython: + # Only report errors for .py files, and only if we didn't + # explicitly suppress those errors. + # NotPython is only raised by PythonFileReporter, which has a + # should_be_python() method. + if fr.should_be_python(): + if config.ignore_errors: + msg = "Could not parse Python file {0}".format(fr.filename) + coverage._warn(msg, slug="couldnt-parse") + else: raise - except NotPython: - # Only report errors for .py files, and only if we didn't - # explicitly suppress those errors. - # NotPython is only raised by PythonFileReporter, which has a - # should_be_python() method. - if fr.should_be_python(): - if self.config.ignore_errors: - msg = "Could not parse Python file {0}".format(fr.filename) - self.coverage._warn(msg, slug="couldnt-parse") - else: - raise + else: + yield (fr, analysis) diff --git a/coverage/summary.py b/coverage/summary.py index 5b197f605..6e711089f 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -6,16 +6,17 @@ import sys from coverage import env -from coverage.report import Reporter +from coverage.report import get_analysis_to_report from coverage.results import Numbers from coverage.misc import NotPython, CoverageException, output_encoding -class SummaryReporter(Reporter): +class SummaryReporter(object): """A reporter for writing the summary report.""" def __init__(self, coverage, config): - super(SummaryReporter, self).__init__(coverage, config) + self.coverage = coverage + self.config = config self.branches = coverage.get_data().has_arcs() self.outfile = None self.fr_analysis = [] @@ -40,7 +41,8 @@ def report(self, morfs, outfile=None): self.outfile = outfile or sys.stdout self.coverage.get_data().set_query_contexts(self.config.query_contexts) - self.report_files(self.report_one_file, morfs) + for fr, analysis in get_analysis_to_report(self.coverage, self.config, morfs): + self.report_one_file(fr, analysis) # Prepare the formatting strings, header, and column sorting. max_name = max([len(fr.relative_filename()) for (fr, analysis) in self.fr_analysis] + [5]) diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 8ecdc24a2..a414ed40d 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -14,7 +14,7 @@ from coverage import __url__, __version__, files from coverage.backward import iitems from coverage.misc import isolate_module -from coverage.report import Reporter +from coverage.report import get_analysis_to_report os = isolate_module(os) @@ -30,11 +30,12 @@ def rate(hit, num): return "%.4g" % (float(hit) / num) -class XmlReporter(Reporter): +class XmlReporter(object): """A reporter for writing Cobertura-style XML coverage results.""" def __init__(self, coverage, config): - super(XmlReporter, self).__init__(coverage, config) + self.coverage = coverage + self.config = config self.source_paths = set() if config.source: @@ -71,7 +72,8 @@ def report(self, morfs, outfile=None): xcoverage.appendChild(self.xml_out.createComment(" Based on %s " % DTD_URL)) # Call xml_file for each file in the data. - self.report_files(self.xml_file, morfs) + for fr, analysis in get_analysis_to_report(self.coverage, self.config, morfs): + self.xml_file(fr, analysis) xsources = self.xml_out.createElement("sources") xcoverage.appendChild(xsources) From e26a9237bae636635440061094cd65ffc40c3cf6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 17 Jun 2019 06:59:24 -0400 Subject: [PATCH 536/952] I'll update dev versions myself --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8b7840b0f..a12897b67 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt +# # Tell Travis what to do # https://travis-ci.com/nedbat/coveragepy @@ -10,7 +13,6 @@ python: - '3.5' - '3.6' - '3.7' - - '3.8-dev' - 'pypy2.7-6.0' - 'pypy3.5-6.0' From 12b024767da29b459755ea827e97867d4011c2e9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 17 Jun 2019 09:54:51 -0400 Subject: [PATCH 537/952] Refactor for better testability --- coverage/html.py | 98 ++++++++++++++++++---------------- coverage/htmlfiles/pyfile.html | 4 +- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/coverage/html.py b/coverage/html.py index 627a19a05..0045637bc 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -98,6 +98,12 @@ def __init__(self, cov, config): title = self.config.html_title if env.PY2: title = title.decode("utf8") + + if self.config.extra_css: + self.extra_css = os.path.basename(self.config.extra_css) + else: + self.extra_css = None + self.template_globals = { 'escape': escape, 'pair': pair, @@ -105,6 +111,8 @@ def __init__(self, cov, config): 'len': len, '__url__': coverage.__url__, '__version__': coverage.__version__, + 'time_stamp': datetime.datetime.now().strftime('%Y-%m-%d %H:%M'), + 'extra_css': self.extra_css, } self.pyfile_html_source = read_data("pyfile.html") self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) @@ -115,9 +123,7 @@ def __init__(self, cov, config): self.all_files_nums = [] self.has_arcs = self.data.has_arcs() self.status = HtmlStatus() - self.extra_css = None self.totals = Numbers() - self.time_stamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') def report(self, morfs): """Generate an HTML report for `morfs`. @@ -125,8 +131,6 @@ def report(self, morfs): `morfs` is a list of modules or file names. """ - assert self.config.html_dir, "must give a directory for html reporting" - # Read the status data. self.status.read(self.config.html_dir) @@ -139,10 +143,6 @@ def report(self, morfs): self.status.reset() self.status.set_settings_hash(these_settings) - # The user may have extra CSS they want copied. - if self.config.extra_css: - self.extra_css = os.path.basename(self.config.extra_css) - self.directory = self.config.html_dir # Process all the files. @@ -175,10 +175,10 @@ def make_local_static_report_files(self): os.path.join(self.directory, self.extra_css) ) - def file_hash(self, source, fr): + def file_hash(self, fr): """Compute a hash that changes if the file needs to be re-reported.""" m = Hasher() - m.update(source) + m.update(fr.source().encode('utf-8')) add_data_to_hash(self.data, fr.filename, m) return m.hexdigest() @@ -202,10 +202,8 @@ def html_file(self, fr, analysis): file_be_gone(html_path) return - source = fr.source() - # Find out if the file on disk is already correct. - this_hash = self.file_hash(source.encode('utf-8'), fr) + this_hash = self.file_hash(fr) that_hash = self.status.file_hash(rootname) if this_hash == that_hash: # Nothing has changed to require the file to be reported again. @@ -214,6 +212,34 @@ def html_file(self, fr, analysis): self.status.set_file_hash(rootname, this_hash) + # Write the HTML page for this file. + file_data = self.data_for_file(fr, analysis) + for ldata in file_data['lines']: + # Build the HTML for the line. + html = [] + for tok_type, tok_text in ldata['tokens']: + if tok_type == "ws": + html.append(escape(tok_text)) + else: + tok_html = escape(tok_text) or ' ' + html.append( + '{}'.format(tok_type, tok_html) + ) + ldata['html'] = ''.join(html) + + html = self.source_tmpl.render(file_data) + write_html(html_path, html) + + # Save this file's information for the index file. + index_info = { + 'nums': nums, + 'html_filename': html_filename, + 'relative_filename': fr.relative_filename(), + } + self.files.append(index_info) + self.status.set_index_info(rootname, index_info) + + def data_for_file(self, fr, analysis): if self.has_arcs: missing_branch_arcs = analysis.missing_branch_arcs() arcs_executed = analysis.arcs_executed() @@ -231,7 +257,7 @@ def html_file(self, fr, analysis): lines = [] - for lineno, line in enumerate(fr.source_token_lines(), start=1): + for lineno, tokens in enumerate(fr.source_token_lines(), start=1): # Figure out how to mark this line. line_class = [] annotate_html = "" @@ -268,53 +294,33 @@ def html_file(self, fr, analysis): elif lineno in analysis.statements: line_class.append(c_run) - # Build the HTML for the line. - html = [] - for tok_type, tok_text in line: - if tok_type == "ws": - html.append(escape(tok_text)) - else: - tok_html = escape(tok_text) or ' ' - html.append( - '%s' % (tok_type, tok_html) - ) + if self.config.show_contexts: + contexts = sorted(filter(None, contexts_by_lineno[lineno])) + else: + contexts = None lines.append({ - 'html': ''.join(html), + 'tokens': tokens, 'number': lineno, 'class': ' '.join(line_class) or "pln", - 'contexts': \ - (sorted(filter(None, contexts_by_lineno[lineno])) or None) - if self.config.show_contexts else None, + 'contexts': contexts, 'annotate': annotate_html, 'annotate_long': annotate_long, }) - # Write the HTML page for this file. - html = self.source_tmpl.render({ + file_data = { 'c_exc': c_exc, 'c_mis': c_mis, 'c_par': c_par, 'c_run': c_run, 'has_arcs': self.has_arcs, 'show_contexts': self.config.show_contexts, - 'extra_css': self.extra_css, - 'fr': fr, - 'nums': nums, - 'lines': lines, - 'time_stamp': self.time_stamp, - }) - - write_html(html_path, html) - - # Save this file's information for the index file. - index_info = { - 'nums': nums, - 'html_filename': html_filename, 'relative_filename': fr.relative_filename(), + 'nums': analysis.numbers, + 'lines': lines, } - self.files.append(index_info) - self.status.set_index_info(rootname, index_info) + + return file_data def index_file(self): """Write the index.html file for this report.""" @@ -324,10 +330,8 @@ def index_file(self): html = index_tmpl.render({ 'has_arcs': self.has_arcs, - 'extra_css': self.extra_css, 'files': self.files, 'totals': self.totals, - 'time_stamp': self.time_stamp, }) write_html(os.path.join(self.directory, "index.html"), html) diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html index e85ee54c4..33f1fbb1b 100644 --- a/coverage/htmlfiles/pyfile.html +++ b/coverage/htmlfiles/pyfile.html @@ -8,7 +8,7 @@ {# IE8 rounds line-height incorrectly, and adding this emulateIE7 line makes it right! #} {# http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/7684445e-f080-4d8f-8529-132763348e21 #} - Coverage for {{fr.relative_filename|escape}}: {{nums.pc_covered_str}}% + Coverage for {{relative_filename|escape}}: {{nums.pc_covered_str}}% {% if extra_css %} @@ -25,7 +25,7 @@

@@ -54,18 +54,18 @@

-

1

+

1

2

-

3

+

3

4

-

5

+

5

-

if 1 < 2: 

+

if 1 < 2: 

# Needed a < to look at HTML entities. 

-

a = 3 

+

a = 3 

else: 

-

a = 4 

+

a = 4 

diff --git a/tests/gold/html/b_branch/b_py.html b/tests/gold/html/b_branch/b_py.html index a21175eba..c9197c596 100644 --- a/tests/gold/html/b_branch/b_py.html +++ b/tests/gold/html/b_branch/b_py.html @@ -22,10 +22,10 @@

Coverage for b.py : Show keyboard shortcuts

17 statements   - 14 run - 3 missing - 0 excluded - 4 partial + 14 run + 3 missing + 0 excluded + 4 partial

@@ -55,62 +55,62 @@

-

1

+

1

2

-

3

-

4

+

3

+

4

5

-

6

+

6

7

-

8

+

8

9

-

10

+

10

11

-

12

-

13

+

12

+

13

14

-

15

+

15

16

-

17

-

18

+

17

+

18

19

-

20

-

21

+

20

+

21

22

-

23

-

24

-

25

+

23

+

24

+

25

26

-

27

+

27

-

def one(x): 

+

def one(x): 

# This will be a branch that misses the else. 

-

3 ↛ 6line 3 didn't jump to line 6, because the condition on line 3 was never false if x < 2: 

-

a = 3 

+

3 ↛ 6line 3 didn't jump to line 6, because the condition on line 3 was never false if x < 2: 

+

a = 3 

else: 

-

a = 4 

+

a = 4 

 

-

one(1) 

+

one(1) 

 

-

def two(x): 

+

def two(x): 

# A missed else that branches to "exit" 

-

12 ↛ exitline 12 didn't return from function 'two', because the condition on line 12 was never false if x: 

-

a = 5 

+

12 ↛ exitline 12 didn't return from function 'two', because the condition on line 12 was never false if x: 

+

a = 5 

 

-

two(1) 

+

two(1) 

 

-

def three(): 

-

try: 

+

def three(): 

+

try: 

# This if has two branches, *neither* one taken. 

-

20 ↛ 21,   20 ↛ 232 missed branches: 1) line 20 didn't jump to line 21, because the condition on line 20 was never true, 2) line 20 didn't jump to line 23, because the condition on line 20 was never false if name_error_this_variable_doesnt_exist: 

-

a = 1 

+

20 ↛ 21,   20 ↛ 232 missed branches: 1) line 20 didn't jump to line 21, because the condition on line 20 was never true, 2) line 20 didn't jump to line 23, because the condition on line 20 was never false if name_error_this_variable_doesnt_exist: 

+

a = 1 

else: 

-

a = 2 

-

except: 

-

pass 

+

a = 2 

+

except: 

+

pass 

 

-

three() 

+

three() 

diff --git a/tests/gold/html/bom/2/bom_py.html b/tests/gold/html/bom/2/bom_py.html index 14f25413b..5cab96221 100644 --- a/tests/gold/html/bom/2/bom_py.html +++ b/tests/gold/html/bom/2/bom_py.html @@ -22,9 +22,9 @@

Coverage for bom.py : Show keyboard shortcuts

7 statements   - 5 run - 2 missing - 0 excluded + 5 run + 2 missing + 0 excluded

@@ -55,29 +55,29 @@

1

-

2

+

2

3

-

4

+

4

5

-

6

-

7

-

8

+

6

+

7

+

8

9

-

10

-

11

+

10

+

11

# A Python source file in utf-8, with BOM. 

-

math = "3×4 = 12, ÷2 = 6±0" 

+

math = "3×4 = 12, ÷2 = 6±0" 

 

-

import sys 

+

import sys 

 

-

if sys.version_info >= (3, 0): 

-

assert len(math) == 18 

-

assert len(math.encode('utf-8')) == 21 

+

if sys.version_info >= (3, 0): 

+

assert len(math) == 18 

+

assert len(math.encode('utf-8')) == 21 

else: 

-

assert len(math) == 21 

-

assert len(math.decode('utf-8')) == 18 

+

assert len(math) == 21 

+

assert len(math.decode('utf-8')) == 18 

diff --git a/tests/gold/html/bom/bom_py.html b/tests/gold/html/bom/bom_py.html index 78d7f7b71..3395c3bdc 100644 --- a/tests/gold/html/bom/bom_py.html +++ b/tests/gold/html/bom/bom_py.html @@ -22,9 +22,9 @@

Coverage for bom.py : Show keyboard shortcuts

7 statements   - 5 run - 2 missing - 0 excluded + 5 run + 2 missing + 0 excluded

@@ -55,29 +55,29 @@

1

-

2

+

2

3

-

4

+

4

5

-

6

-

7

-

8

+

6

+

7

+

8

9

-

10

-

11

+

10

+

11

# A Python source file in utf-8, with BOM. 

-

math = "3×4 = 12, ÷2 = 6±0" 

+

math = "3×4 = 12, ÷2 = 6±0" 

 

-

import sys 

+

import sys 

 

-

if sys.version_info >= (3, 0): 

-

assert len(math) == 18 

-

assert len(math.encode('utf-8')) == 21 

+

if sys.version_info >= (3, 0): 

+

assert len(math) == 18 

+

assert len(math.encode('utf-8')) == 21 

else: 

-

assert len(math) == 21 

-

assert len(math.decode('utf-8')) == 18 

+

assert len(math) == 21 

+

assert len(math.decode('utf-8')) == 18 

diff --git a/tests/gold/html/isolatin1/isolatin1_py.html b/tests/gold/html/isolatin1/isolatin1_py.html index e8ad244b6..7fb22be2b 100644 --- a/tests/gold/html/isolatin1/isolatin1_py.html +++ b/tests/gold/html/isolatin1/isolatin1_py.html @@ -22,9 +22,9 @@

Coverage for isolatin1.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -57,15 +57,15 @@

1

2

3

-

4

-

5

+

4

+

5

# -*- coding: iso8859-1 -*- 

# A Python source file in another encoding. 

 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

assert len(math) == 18 

+

math = "3×4 = 12, ÷2 = 6±0" 

+

assert len(math) == 18 

diff --git a/tests/gold/html/omit_1/m1_py.html b/tests/gold/html/omit_1/m1_py.html index 05b2bd494..238e4cbcd 100644 --- a/tests/gold/html/omit_1/m1_py.html +++ b/tests/gold/html/omit_1/m1_py.html @@ -22,9 +22,9 @@

Coverage for m1.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

diff --git a/tests/gold/html/omit_1/m2_py.html b/tests/gold/html/omit_1/m2_py.html index 056e7af1f..c2cf387aa 100644 --- a/tests/gold/html/omit_1/m2_py.html +++ b/tests/gold/html/omit_1/m2_py.html @@ -22,9 +22,9 @@

Coverage for m2.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m2a = 1 

-

m2b = 2 

+

m2a = 1 

+

m2b = 2 

diff --git a/tests/gold/html/omit_1/m3_py.html b/tests/gold/html/omit_1/m3_py.html index 428527b2a..f0d59a83e 100644 --- a/tests/gold/html/omit_1/m3_py.html +++ b/tests/gold/html/omit_1/m3_py.html @@ -22,9 +22,9 @@

Coverage for m3.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

diff --git a/tests/gold/html/omit_1/main_py.html b/tests/gold/html/omit_1/main_py.html index 3fbc4af76..d9474b0b4 100644 --- a/tests/gold/html/omit_1/main_py.html +++ b/tests/gold/html/omit_1/main_py.html @@ -22,9 +22,9 @@

Coverage for main.py : Show keyboard shortcuts

8 statements   - 8 run - 0 missing - 0 excluded + 8 run + 0 missing + 0 excluded

@@ -54,28 +54,28 @@

-

1

-

2

-

3

+

1

+

2

+

3

4

-

5

-

6

+

5

+

6

7

-

8

-

9

-

10

+

8

+

9

+

10

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

 

-

a = 5 

-

b = 6 

+

a = 5 

+

b = 6 

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

diff --git a/tests/gold/html/omit_2/m2_py.html b/tests/gold/html/omit_2/m2_py.html index 056e7af1f..c2cf387aa 100644 --- a/tests/gold/html/omit_2/m2_py.html +++ b/tests/gold/html/omit_2/m2_py.html @@ -22,9 +22,9 @@

Coverage for m2.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m2a = 1 

-

m2b = 2 

+

m2a = 1 

+

m2b = 2 

diff --git a/tests/gold/html/omit_2/m3_py.html b/tests/gold/html/omit_2/m3_py.html index 428527b2a..f0d59a83e 100644 --- a/tests/gold/html/omit_2/m3_py.html +++ b/tests/gold/html/omit_2/m3_py.html @@ -22,9 +22,9 @@

Coverage for m3.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

diff --git a/tests/gold/html/omit_2/main_py.html b/tests/gold/html/omit_2/main_py.html index 3fbc4af76..d9474b0b4 100644 --- a/tests/gold/html/omit_2/main_py.html +++ b/tests/gold/html/omit_2/main_py.html @@ -22,9 +22,9 @@

Coverage for main.py : Show keyboard shortcuts

8 statements   - 8 run - 0 missing - 0 excluded + 8 run + 0 missing + 0 excluded

@@ -54,28 +54,28 @@

-

1

-

2

-

3

+

1

+

2

+

3

4

-

5

-

6

+

5

+

6

7

-

8

-

9

-

10

+

8

+

9

+

10

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

 

-

a = 5 

-

b = 6 

+

a = 5 

+

b = 6 

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

diff --git a/tests/gold/html/omit_3/m3_py.html b/tests/gold/html/omit_3/m3_py.html index 428527b2a..f0d59a83e 100644 --- a/tests/gold/html/omit_3/m3_py.html +++ b/tests/gold/html/omit_3/m3_py.html @@ -22,9 +22,9 @@

Coverage for m3.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

diff --git a/tests/gold/html/omit_3/main_py.html b/tests/gold/html/omit_3/main_py.html index 3fbc4af76..d9474b0b4 100644 --- a/tests/gold/html/omit_3/main_py.html +++ b/tests/gold/html/omit_3/main_py.html @@ -22,9 +22,9 @@

Coverage for main.py : Show keyboard shortcuts

8 statements   - 8 run - 0 missing - 0 excluded + 8 run + 0 missing + 0 excluded

@@ -54,28 +54,28 @@

-

1

-

2

-

3

+

1

+

2

+

3

4

-

5

-

6

+

5

+

6

7

-

8

-

9

-

10

+

8

+

9

+

10

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

 

-

a = 5 

-

b = 6 

+

a = 5 

+

b = 6 

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

diff --git a/tests/gold/html/omit_4/m1_py.html b/tests/gold/html/omit_4/m1_py.html index 05b2bd494..238e4cbcd 100644 --- a/tests/gold/html/omit_4/m1_py.html +++ b/tests/gold/html/omit_4/m1_py.html @@ -22,9 +22,9 @@

Coverage for m1.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

diff --git a/tests/gold/html/omit_4/m3_py.html b/tests/gold/html/omit_4/m3_py.html index 428527b2a..f0d59a83e 100644 --- a/tests/gold/html/omit_4/m3_py.html +++ b/tests/gold/html/omit_4/m3_py.html @@ -22,9 +22,9 @@

Coverage for m3.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m3a = 1 

-

m3b = 2 

+

m3a = 1 

+

m3b = 2 

diff --git a/tests/gold/html/omit_4/main_py.html b/tests/gold/html/omit_4/main_py.html index 3fbc4af76..d9474b0b4 100644 --- a/tests/gold/html/omit_4/main_py.html +++ b/tests/gold/html/omit_4/main_py.html @@ -22,9 +22,9 @@

Coverage for main.py : Show keyboard shortcuts

8 statements   - 8 run - 0 missing - 0 excluded + 8 run + 0 missing + 0 excluded

@@ -54,28 +54,28 @@

-

1

-

2

-

3

+

1

+

2

+

3

4

-

5

-

6

+

5

+

6

7

-

8

-

9

-

10

+

8

+

9

+

10

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

 

-

a = 5 

-

b = 6 

+

a = 5 

+

b = 6 

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

diff --git a/tests/gold/html/omit_5/m1_py.html b/tests/gold/html/omit_5/m1_py.html index 05b2bd494..238e4cbcd 100644 --- a/tests/gold/html/omit_5/m1_py.html +++ b/tests/gold/html/omit_5/m1_py.html @@ -22,9 +22,9 @@

Coverage for m1.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -54,12 +54,12 @@

-

1

-

2

+

1

+

2

-

m1a = 1 

-

m1b = 2 

+

m1a = 1 

+

m1b = 2 

diff --git a/tests/gold/html/omit_5/main_py.html b/tests/gold/html/omit_5/main_py.html index 3fbc4af76..d9474b0b4 100644 --- a/tests/gold/html/omit_5/main_py.html +++ b/tests/gold/html/omit_5/main_py.html @@ -22,9 +22,9 @@

Coverage for main.py : Show keyboard shortcuts

8 statements   - 8 run - 0 missing - 0 excluded + 8 run + 0 missing + 0 excluded

@@ -54,28 +54,28 @@

-

1

-

2

-

3

+

1

+

2

+

3

4

-

5

-

6

+

5

+

6

7

-

8

-

9

-

10

+

8

+

9

+

10

-

import m1 

-

import m2 

-

import m3 

+

import m1 

+

import m2 

+

import m3 

 

-

a = 5 

-

b = 6 

+

a = 5 

+

b = 6 

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

+

assert m1.m1a == 1 

+

assert m2.m2a == 1 

+

assert m3.m3a == 1 

diff --git a/tests/gold/html/other/blah_blah_other_py.html b/tests/gold/html/other/blah_blah_other_py.html index 36e3653d7..215ee7f1c 100644 --- a/tests/gold/html/other/blah_blah_other_py.html +++ b/tests/gold/html/other/blah_blah_other_py.html @@ -22,9 +22,9 @@

Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/cov Show keyboard shortcuts

1 statements   - 1 run - 0 missing - 0 excluded + 1 run + 0 missing + 0 excluded

@@ -57,13 +57,13 @@

1

2

3

-

4

+

4

# A file in another directory. We're checking that it ends up in the 

# HTML report. 

 

-

print("This is the other src!") 

+

print("This is the other src!") 

diff --git a/tests/gold/html/other/here_py.html b/tests/gold/html/other/here_py.html index 8efa027a5..962b04c0b 100644 --- a/tests/gold/html/other/here_py.html +++ b/tests/gold/html/other/here_py.html @@ -22,9 +22,9 @@

Coverage for here.py : Show keyboard shortcuts

4 statements   - 3 run - 1 missing - 0 excluded + 3 run + 1 missing + 0 excluded

@@ -54,20 +54,20 @@

-

1

+

1

2

-

3

-

4

+

3

+

4

5

-

6

+

6

-

import other 

+

import other 

 

-

if 1 < 2: 

-

h = 3 

+

if 1 < 2: 

+

h = 3 

else: 

-

h = 4 

+

h = 4 

diff --git a/tests/gold/html/partial/partial_py.html b/tests/gold/html/partial/partial_py.html index 96d02f2f5..1638b761d 100644 --- a/tests/gold/html/partial/partial_py.html +++ b/tests/gold/html/partial/partial_py.html @@ -22,10 +22,10 @@

Coverage for partial.py : Show keyboard shortcuts

7 statements   - 7 run - 0 missing - 1 excluded - 1 partial + 7 run + 0 missing + 1 excluded + 1 partial

@@ -56,41 +56,41 @@

1

-

2

+

2

3

-

4

-

5

+

4

+

5

6

-

7

-

8

+

7

+

8

9

10

11

12

13

-

14

+

14

15

-

16

-

17

+

16

+

17

# partial branches and excluded lines 

-

a = 6 

+

a = 6 

 

-

4 ↛ 7line 4 didn't jump to line 7, because the condition on line 4 was never falsewhile "no peephole".upper(): # t4 

-

break 

+

4 ↛ 7line 4 didn't jump to line 7, because the condition on line 4 was never falsewhile "no peephole".upper(): # t4 

+

break 

 

-

while a: # pragma: no branch 

-

break 

+

while a: # pragma: no branch 

+

break 

 

if 0: 

never_happen() 

 

if 1: 

-

a = 21 

+

a = 21 

 

-

if a == 23: 

-

raise AssertionError("Can't") 

+

if a == 23: 

+

raise AssertionError("Can't") 

diff --git a/tests/gold/html/styled/a_py.html b/tests/gold/html/styled/a_py.html index dd569b1ba..f6f183b3d 100644 --- a/tests/gold/html/styled/a_py.html +++ b/tests/gold/html/styled/a_py.html @@ -23,9 +23,9 @@

Coverage for a.py : Show keyboard shortcuts

3 statements   - 2 run - 1 missing - 0 excluded + 2 run + 1 missing + 0 excluded

@@ -55,18 +55,18 @@

-

1

+

1

2

-

3

+

3

4

-

5

+

5

-

if 1 < 2: 

+

if 1 < 2: 

# Needed a < to look at HTML entities. 

-

a = 3 

+

a = 3 

else: 

-

a = 4 

+

a = 4 

diff --git a/tests/gold/html/styled/style.css b/tests/gold/html/styled/style.css index 839669528..e6e6dc423 100644 --- a/tests/gold/html/styled/style.css +++ b/tests/gold/html/styled/style.css @@ -43,27 +43,26 @@ h1 { font-size: 1.25em; display: inline-block; } h2.stats { margin-top: .5em; font-size: 1em; } -.stats span { border: 1px solid; padding: .1em .25em; margin: 0 .1em; cursor: pointer; border-color: #999 #ccc #ccc #999; } -.stats span.hide_run, .stats span.hide_exc, .stats span.hide_mis, .stats span.hide_par, .stats span.par.hide_run.hide_par { border-color: #ccc #999 #999 #ccc; } -.stats span.par.hide_run { border-color: #999 #ccc #ccc #999; } -.stats span.run { background: #ddffdd; } -.stats span.exc { background: #eeeeee; } -.stats span.mis { background: #ffdddd; } -.stats span.hide_run { background: #eeffee; } -.stats span.hide_exc { background: #f5f5f5; } -.stats span.hide_mis { background: #ffeeee; } -.stats span.par { background: #ffffaa; } -.stats span.hide_par { background: #ffffcc; } +.stats span { border: 1px solid; border-radius: .1em; padding: .1em .5em; margin: 0 .1em; cursor: pointer; border-color: #ccc #999 #999 #ccc; } +.stats span.run { background: #eeffee; } +.stats span.run.show_run { border-color: #999 #ccc #ccc #999; background: #ddffdd; } +.stats span.mis { background: #ffeeee; } +.stats span.mis.show_mis { border-color: #999 #ccc #ccc #999; background: #ffdddd; } +.stats span.exc { background: #f7f7f7; } +.stats span.exc.show_exc { border-color: #999 #ccc #ccc #999; background: #eeeeee; } +.stats span.par { background: #ffffd5; } +.stats span.par.show_par { border-color: #999 #ccc #ccc #999; background: #ffffaa; } -#keyboard_icon { float: right; margin: 5px; cursor: pointer; } - -.help_panel { position: absolute; background: #ffffcc; padding: .5em; border: 1px solid #883; display: none; } +.text p.show_par span.annotate.long, td.contexts p span.context-list, .help_panel { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; color: #333; padding: .25em .5em; } -.indexfile .help_panel { width: 20em; height: 4em; } +.text p.show_par span.annotate.long, td.contexts p span.context-list { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } -.pyfile .help_panel { width: 16em; height: 8em; } +#keyboard_icon { float: right; margin: 5px; cursor: pointer; } +.help_panel { padding: .5em; border: 1px solid #883; } .help_panel .legend { font-style: italic; margin-bottom: 1em; } +.indexfile .help_panel { width: 20em; height: 4em; } +.pyfile .help_panel { width: 16em; height: 8em; } #panel_icon { float: right; cursor: pointer; } @@ -79,59 +78,47 @@ td.text { width: 100%; } .text p { margin: 0; padding: 0 0 0 .5em; border-left: 2px solid #ffffff; white-space: pre; position: relative; } .text p:hover { background: #f2f2f2; } -.text p.mis { background: #ffdddd; border-left: 2px solid #ff0000; } -.text p.mis:hover { background: #f2d2d2; } -.text p.run, .text p.run.hide_par { background: #ddffdd; border-left: 2px solid #00ff00; } -.text p.run:hover, .text p.run.hide_par:hover { background: #d2f2d2; } -.text p.exc { background: #eeeeee; border-left: 2px solid #808080; } -.text p.exc:hover { background: #e2e2e2; } -.text p.par, .text p.par.hide_run { background: #ffffaa; border-left: 2px solid #eeee99; } -.text p.par:hover, .text p.par.hide_run:hover { background: #f2f2a2; } -.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, .text p.hide_run.hide_par { background: inherit; } -.text p.hide_run:hover, .text p.hide_exc:hover, .text p.hide_mis:hover, .text p.hide_par:hover, .text p.hide_run.hide_par:hover { background: #f2f2f2; } - -.text span.annotate { font-family: georgia; color: #666; float: right; padding-right: .5em; } -.text p.hide_par span.annotate { display: none; } -.text span.annotate.long { display: none; } -.text p:hover span.annotate.long { display: block; max-width: 50%; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; width: 30em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } - +.text p.run { border-left: 2px solid #00ff00; } +.text p.run.show_run { background: #ddffdd; } +.text p.run.show_run:hover { background: #d2f2d2; } +.text p.mis { border-left: 2px solid #ff0000; } +.text p.mis.show_mis { background: #ffdddd; } +.text p.mis.show_mis:hover { background: #f2d2d2; } +.text p.exc { border-left: 2px solid #808080; } +.text p.exc.show_exc { background: #eeeeee; } +.text p.exc.show_exc:hover { background: #e2e2e2; } +.text p.par { border-left: 2px solid #eeee99; } +.text p.par.show_par { background: #ffffaa; } +.text p.par.show_par:hover { background: #f2f2a2; } + +.text span.annotate { font-family: georgia; color: #666; float: right; padding-right: .5em; display: none; } +.text p.show_par span.annotate { display: inline; } +.text p.show_par span.annotate.long { max-width: 50%; width: 30em; } +.text p.show_par:hover span.annotate.long { display: block; } .text .com { color: green; font-style: italic; line-height: 1px; } .text .key { font-weight: bold; line-height: 1px; } .text .str { color: #000080; } td.contexts p { margin: 0; padding: 0 .5em; color: #999999; font-family: verdana, sans-serif; white-space: nowrap; position: relative; } td.contexts p:hover { background: #eee; } -td.contexts p span.context-list { display: none; } -td.contexts p:hover span.context-list { display: block; min-width: 30em; white-space: normal; float: right; position: absolute; top: 1.75em; right: 1em; height: auto; color: #333; background: #ffffcc; border: 1px solid #888; padding: .25em .5em; z-index: 999; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; } +td.contexts p span.context-list { min-width: 30em; } +td.contexts p span.context-list span.context-line { display: block; } +td.contexts p:hover span.context-list { display: block; } td.contexts p span.context-button { display: inline-block; cursor: pointer; font-size: .8333em; line-height: 1em; } -span.context-list span.context-line { display: block; } - #index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } - -#index th { font-style: italic; color: #333; border-bottom: 1px solid #ccc; cursor: pointer; } - -#index th:hover { background: #eee; border-bottom: 1px solid #999; } - #index td.left, #index th.left { padding-left: 0; } - #index td.right, #index th.right { padding-right: 0; } - +#index td.name, #index th.name { text-align: left; width: auto; } +#index th { font-style: italic; color: #333; border-bottom: 1px solid #ccc; cursor: pointer; } +#index th:hover { background: #eee; border-bottom: 1px solid #999; } #index th.headerSortDown, #index th.headerSortUp { border-bottom: 1px solid #000; white-space: nowrap; background: #eee; } - #index th.headerSortDown:after { content: " ↓"; } - #index th.headerSortUp:after { content: " ↑"; } - -#index td.name, #index th.name { text-align: left; width: auto; } - #index td.name a { text-decoration: none; color: #000; } - #index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } - #index tr.file:hover { background: #eeeeee; } - #index tr.file:hover td.name { text-decoration: underline; color: #000; } #scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: white; border-left: 1px solid #eee; will-change: transform; } -#scroll_marker .marker { background: #eedddd; position: absolute; min-height: 3px; width: 100%; } +#scroll_marker .marker { background: #ddd; position: absolute; min-height: 3px; width: 100%; } diff --git a/tests/gold/html/unicode/unicode_py.html b/tests/gold/html/unicode/unicode_py.html index 174a9a270..caad05ab2 100644 --- a/tests/gold/html/unicode/unicode_py.html +++ b/tests/gold/html/unicode/unicode_py.html @@ -22,9 +22,9 @@

Coverage for unicode.py : Show keyboard shortcuts

2 statements   - 2 run - 0 missing - 0 excluded + 2 run + 0 missing + 0 excluded

@@ -57,15 +57,15 @@

1

2

3

-

4

-

5

+

4

+

5

# -*- coding: utf-8 -*- 

# A Python source file with exotic characters. 

 

-

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

-

surrogate = "db40,dd00: x󠄀" 

+

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

+

surrogate = "db40,dd00: x󠄀" 

diff --git a/tests/js/index.html b/tests/js/index.html index 744014bc3..d9eb5c038 100644 --- a/tests/js/index.html +++ b/tests/js/index.html @@ -4,18 +4,12 @@ Coverage.py Javascript Test Suite - + - -
- + diff --git a/tests/js/tests.js b/tests/js/tests.js index 7bd3b9ca9..a7eb11fcc 100644 --- a/tests/js/tests.js +++ b/tests/js/tests.js @@ -15,10 +15,12 @@ function raw_selection_is(assert, sel, check_highlight) { assert.equal(coverage.sel_begin, beg); assert.equal(coverage.sel_end, end); if (check_highlight) { - assert.equal(coverage.code_container().find(".highlight").length, end-beg); + assert.equal($(".linenos .highlight").length, end-beg); } } +// The spec is a list of "rbw" letters, indicating colors of successive lines. +// We set the show_r and show_b classes for r and b. function build_fixture(spec) { var i, data; $("#fixture-template").tmpl().appendTo("#qunit-fixture"); diff --git a/tests/test_html.py b/tests/test_html.py index 4509e52fb..8e9430ce1 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -922,12 +922,12 @@ def test_partial(self): compare_html(gold_path("html/partial"), "out") contains( "out/partial_py.html", - '

', - '

', + '

', + '

', # The "if 0" and "if 1" statements are optimized away. '

', # The "raise AssertionError" is excluded by regex in the .ini. - '

', + '

', ) contains( "out/index.html", From 0911fe5e8986dc63c3e9fce8815b4730d02f1d01 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Jul 2019 13:05:37 -0400 Subject: [PATCH 563/952] Revert "Remove a copy of a private macro, and just use it" #809 This reverts commit b777c96f885b8d91b5339940a31a6a8ec4bfa3f9. --- coverage/ctracer/tracer.c | 2 +- coverage/ctracer/util.h | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index d497a94d1..7d639112d 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -541,7 +541,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) /* Make the frame right in case settrace(gettrace()) happens. */ Py_INCREF(self); - Py_XSETREF(frame->f_trace, (PyObject*)self); + My_XSETREF(frame->f_trace, (PyObject*)self); /* A call event is really a "start frame" event, and can happen for * re-entering a generator also. f_lasti is -1 for a true call, and a diff --git a/coverage/ctracer/util.h b/coverage/ctracer/util.h index cb8aceb93..5cba9b309 100644 --- a/coverage/ctracer/util.h +++ b/coverage/ctracer/util.h @@ -44,6 +44,14 @@ #endif /* Py3k */ +// Undocumented, and not in all 2.7.x, so our own copy of it. +#define My_XSETREF(op, op2) \ + do { \ + PyObject *_py_tmp = (PyObject *)(op); \ + (op) = (op2); \ + Py_XDECREF(_py_tmp); \ + } while (0) + /* The values returned to indicate ok or error. */ #define RET_OK 0 #define RET_ERROR -1 From e5a59865b038ac575c4f8ca9e79c5b4802018ff1 Mon Sep 17 00:00:00 2001 From: Dan Hemberger Date: Thu, 30 May 2019 12:38:09 -0700 Subject: [PATCH 564/952] Return to the original directory after exec'ing a file If the file that is exec'd chdirs to a directory that doesn't exist at the end of the execution, then we will fail to connect to the SQLite database (due to a failing `os.getcwd` command). We can easily fix this if we ensure we are in a directory that exists after executing the foreign code. Returning to the original directory seems to be a sensible choice. --- coverage/execfile.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coverage/execfile.py b/coverage/execfile.py index fbd0228c3..4edbc8ac9 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -193,7 +193,11 @@ def run(self): # Execute the code object. try: + # Return to the original directory in case the test code exits in + # a non-existent directory. + cwd = os.getcwd() exec(code, main_mod.__dict__) + os.chdir(cwd) except SystemExit: # pylint: disable=try-except-raise # The user called sys.exit(). Just pass it along to the upper # layers, where it will be handled. From 9ae35a5f6a4fed487ef6c14edd6b4eb4f5c1b7f0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Jul 2019 17:17:07 -0400 Subject: [PATCH 565/952] Add tests of bug #806, and ensure it's fixed even if the program ends with an exception --- CHANGES.rst | 4 ++++ CONTRIBUTORS.txt | 1 + coverage/execfile.py | 9 +++++---- tests/test_process.py | 30 ++++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4fb6e1128..52ec07610 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -41,6 +41,9 @@ Unreleased be reported as warnings. As with other warnings, you can suppress them with the ``[run] disable_warnings`` configuration setting. +- Coverage.py no longer fails if the user program deletes its current + directory, closing `issue 806`_. Thanks, Dan Hemberger. + - The scrollbar markers in the HTML report now accurately show the highlighted lines, regardless of what categories of line are highlighted. @@ -50,6 +53,7 @@ Unreleased - The deprecated `Reporter.file_reporters` property has been removed. .. _ShiningPanda: https://wiki.jenkins.io/display/JENKINS/ShiningPanda+Plugin +.. _issue 806: https://github.com/nedbat/coveragepy/pull/806 .. _changes_50a5: diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 972e818bd..06eef4f79 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -33,6 +33,7 @@ Christine Lytwynec Christoph Zwerschke Conrad Ho Cosimo Lupo +Dan Hemberger Dan Riti Dan Wandschneider Danek Duvall diff --git a/coverage/execfile.py b/coverage/execfile.py index 4edbc8ac9..972d8f1b3 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -192,12 +192,11 @@ def run(self): raise CoverageException(msg.format(filename=self.arg0, exc=exc)) # Execute the code object. + # Return to the original directory in case the test code exits in + # a non-existent directory. + cwd = os.getcwd() try: - # Return to the original directory in case the test code exits in - # a non-existent directory. - cwd = os.getcwd() exec(code, main_mod.__dict__) - os.chdir(cwd) except SystemExit: # pylint: disable=try-except-raise # The user called sys.exit(). Just pass it along to the upper # layers, where it will be handled. @@ -236,6 +235,8 @@ def run(self): raise ExceptionDuringRun(typ, err, tb.tb_next) else: sys.exit(1) + finally: + os.chdir(cwd) def run_python_module(args): diff --git a/tests/test_process.py b/tests/test_process.py index 0979cc2ca..a5303338e 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1293,6 +1293,36 @@ def test_accented_directory(self): self.assertEqual(out, report_expected) +class YankedDirectoryTest(CoverageTest): + """Tests of what happens when the current directory is deleted.""" + + BUG_806 = """\ + import os + import sys + import tempfile + + tmpdir = tempfile.mkdtemp() + os.chdir(tmpdir) + os.rmdir(tmpdir) + print(sys.argv[1]) + """ + + def test_removing_directory(self): + self.make_file("bug806.py", self.BUG_806) + out = self.run_command("coverage run bug806.py noerror") + self.assertEqual(out, "noerror\n") + + def test_removing_directory_with_error(self): + self.make_file("bug806.py", self.BUG_806) + out = self.run_command("coverage run bug806.py") + self.assertEqual(out, textwrap.dedent("""\ + Traceback (most recent call last): + File "bug806.py", line 8, in + print(sys.argv[1]) + IndexError: list index out of range + """)) + + def possible_pth_dirs(): """Produce a sequence of directories for trying to write .pth files.""" # First look through sys.path, and if we find a .pth file, then it's a good From 2c4c254a32e27c24b7714d541bd41eef97c375b5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 6 Jul 2019 18:34:48 -0400 Subject: [PATCH 566/952] Can't run the delete-my-directory tests on Windows --- tests/test_process.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_process.py b/tests/test_process.py index a5303338e..000330ad7 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1296,6 +1296,11 @@ def test_accented_directory(self): class YankedDirectoryTest(CoverageTest): """Tests of what happens when the current directory is deleted.""" + def setUp(self): + if env.WINDOWS: + self.skipTest("Windows can't delete the directory in use.") + super(YankedDirectoryTest, self).setUp() + BUG_806 = """\ import os import sys From 48f996ff8dcccfefe0922fafc070b36f980c231e Mon Sep 17 00:00:00 2001 From: Min ho Kim Date: Mon, 8 Jul 2019 00:55:15 +1000 Subject: [PATCH 567/952] Fix typo --- coverage/control.py | 2 +- coverage/data.py | 2 +- coverage/sqldata.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index a4e21295f..823169d13 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -131,7 +131,7 @@ def __init__( This can also be a list of these strings. If `check_preimported` is true, then when coverage is started, the - aleady-imported files will be checked to see if they should be measured + already-imported files will be checked to see if they should be measured by coverage. Importing measured files before coverage is started can mean that code is missed. diff --git a/coverage/data.py b/coverage/data.py index 3d3647bb3..bcb418b88 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -442,7 +442,7 @@ def add_run_info(self, **kwargs): def touch_file(self, filename, plugin_name=""): """Ensure that `filename` appears in the data, empty if needed. - `plugin_name` is the name of the plugin resposible for this file. It is used + `plugin_name` is the name of the plugin responsible for this file. It is used to associate the right filereporter, etc. """ if self._debug.should('dataop'): diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 03a352ba2..cc871950c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -331,7 +331,7 @@ def add_file_tracers(self, file_tracers): def touch_file(self, filename, plugin_name=""): """Ensure that `filename` appears in the data, empty if needed. - `plugin_name` is the name of the plugin resposible for this file. It is used + `plugin_name` is the name of the plugin responsible for this file. It is used to associate the right filereporter, etc. """ self._start_using() @@ -680,7 +680,7 @@ def connect(self): # It can happen that Python switches threads while the tracer writes # data. The second thread will also try to write to the data, # effectively causing a nested context. However, given the indempotent - # nature of the tracer operations, sharing a conenction among threads + # nature of the tracer operations, sharing a connection among threads # is not a problem. self.con = sqlite3.connect(filename, check_same_thread=False) From 9c160feb307c85e8fbc4aae10ff7f33311c5f8d6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 7 Jul 2019 07:06:08 -0400 Subject: [PATCH 568/952] Rough-hewn use of ox_profile --- coverage/cmdline.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 354ae8c20..fdab7d931 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -799,3 +799,22 @@ def main(argv=None): else: status = None return status + +# Profiling using ox_profile. Install it from GitHub: +# pip install git+https://github.com/emin63/ox_profile.git +# +# $set_env.py: COVERAGE_PROFILE - Set to use ox_profile. +_profile = os.environ.get("COVERAGE_PROFILE", "") +if _profile: # pragma: debugging + from ox_profile.core.launchers import SimpleLauncher # pylint: disable=import-error + original_main = main + + def main(argv=None): # pylint: disable=function-redefined + """A wrapper around main that profiles.""" + try: + profiler = SimpleLauncher.launch() + return original_main(argv) + finally: + data, _ = profiler.query(re_filter='coverage', max_records=100) + print(profiler.show(query=data, limit=100, sep='', col='')) + profiler.cancel() From 0fc49e317fefef7ce745d98a1454651ae535e2ac Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 7 Jul 2019 07:34:01 -0400 Subject: [PATCH 569/952] Clarify SqliteDb --- coverage/sqldata.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 03a352ba2..34dd65c85 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -92,6 +92,7 @@ def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._choose_filename() self._file_map = {} + # Maps thread ids to SqliteDb objects. self._dbs = {} self._pid = os.getpid() @@ -124,7 +125,7 @@ def _reset(self): def _create_db(self): if self._debug.should('dataio'): self._debug.write("Creating data file {!r}".format(self._filename)) - self._dbs[get_thread_id()] = Sqlite(self._filename, self._debug) + self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug) with self._dbs[get_thread_id()] as db: for stmt in SCHEMA.split(';'): stmt = stmt.strip() @@ -139,7 +140,7 @@ def _create_db(self): def _open_db(self): if self._debug.should('dataio'): self._debug.write("Opening data file {!r}".format(self._filename)) - self._dbs[get_thread_id()] = Sqlite(self._filename, self._debug) + self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug) with self._dbs[get_thread_id()] as db: try: schema_version, = db.execute("select version from coverage_schema").fetchone() @@ -164,6 +165,7 @@ def _open_db(self): self._file_map[path] = id def _connect(self): + """Get the SqliteDb object to use.""" if get_thread_id() not in self._dbs: if os.path.exists(self._filename): self._open_db() @@ -664,7 +666,7 @@ def run_infos(self): return [] # TODO -class Sqlite(SimpleReprMixin): +class SqliteDb(SimpleReprMixin): def __init__(self, filename, debug): self.debug = debug if debug.should('sql') else None self.filename = filename From 4b43eff1377e818db2c42521d98bf2a0973b1cb6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 7 Jul 2019 09:48:29 -0400 Subject: [PATCH 570/952] Log connections properly --- coverage/sqldata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 34dd65c85..b8efd9362 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -671,8 +671,6 @@ def __init__(self, filename, debug): self.debug = debug if debug.should('sql') else None self.filename = filename self.nest = 0 - if self.debug: - self.debug.write("Connecting to {!r}".format(filename)) def connect(self): # SQLite on Windows on py2 won't open a file if the filename argument @@ -684,6 +682,8 @@ def connect(self): # effectively causing a nested context. However, given the indempotent # nature of the tracer operations, sharing a conenction among threads # is not a problem. + if self.debug: + self.debug.write("Connecting to {!r}".format(self.filename)) self.con = sqlite3.connect(filename, check_same_thread=False) # This pragma makes writing faster. It disables rollbacks, but we never need them. From dade9db58673d4f712ba5c50ed66d1c67a7e1d5d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 7 Jul 2019 07:40:24 -0400 Subject: [PATCH 571/952] Avoid useless or redundant db operations. Faster. Moving operations into the "with self._connect" means less opening and closing of the database. Returning early if there is no data to write avoids writing empty contexts. --- CHANGES.rst | 2 ++ coverage/control.py | 5 +++-- coverage/sqldata.py | 20 +++++++++++++++++--- tests/test_api.py | 15 --------------- tests/test_debug.py | 10 ++++++---- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 52ec07610..ec5fea2d9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -32,6 +32,8 @@ Unreleased information to each covered line. Hovering over the "ctx" marker at the end of the line reveals a list of the contexts that covered the line. +- Dynamic contexts with no data are no longer written to the database. + - Added the classmethod :meth:`Coverage.current` to get the latest started `Coverage` instance. diff --git a/coverage/control.py b/coverage/control.py index a4e21295f..1001a8d6c 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -648,8 +648,9 @@ def _post_save_work(self): self._warn("No data was collected.", slug="no-data-collected") # Find files that were never executed at all. - for file_path, plugin_name in self._inorout.find_unexecuted_files(): - self._data.touch_file(file_path, plugin_name) + if self._data: + for file_path, plugin_name in self._inorout.find_unexecuted_files(): + self._data.touch_file(file_path, plugin_name) if self.config.note: self._data.add_run_info(note=self.config.note) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index b8efd9362..9d44db113 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -255,8 +255,10 @@ def add_lines(self, line_data): )) self._start_using() self._choose_lines_or_arcs(lines=True) - self._set_context_id() + if not line_data: + return with self._connect() as con: + self._set_context_id() for filename, linenos in iitems(line_data): file_id = self._file_id(filename, add=True) data = [(file_id, self._current_context_id, lineno) for lineno in linenos] @@ -279,8 +281,10 @@ def add_arcs(self, arc_data): )) self._start_using() self._choose_lines_or_arcs(arcs=True) - self._set_context_id() + if not arc_data: + return with self._connect() as con: + self._set_context_id() for filename, arcs in iitems(arc_data): file_id = self._file_id(filename, add=True) data = [(file_id, self._current_context_id, fromno, tono) for fromno, tono in arcs] @@ -307,6 +311,10 @@ def add_file_tracers(self, file_tracers): `file_tracers` is { filename: plugin_name, ... } """ + if self._debug.should('dataop'): + self._debug.write("Adding file tracers: %d files" % (len(file_tracers),)) + if not file_tracers: + return self._start_using() with self._connect() as con: for filename, plugin_name in iitems(file_tracers): @@ -667,6 +675,12 @@ def run_infos(self): class SqliteDb(SimpleReprMixin): + """A simple abstraction over a SQLite database. + + Use as a context manager to get an object you can call + execute or executemany on. + + """ def __init__(self, filename, debug): self.debug = debug if debug.should('sql') else None self.filename = filename @@ -679,7 +693,7 @@ def connect(self): filename = os.path.relpath(self.filename) # It can happen that Python switches threads while the tracer writes # data. The second thread will also try to write to the data, - # effectively causing a nested context. However, given the indempotent + # effectively causing a nested context. However, given the idempotent # nature of the tracer operations, sharing a conenction among threads # is not a problem. if self.debug: diff --git a/tests/test_api.py b/tests/test_api.py index 920cd9ad9..301257dc4 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -326,21 +326,6 @@ def test_two_getdata_only_warn_once(self): 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"]) diff --git a/tests/test_debug.py b/tests/test_debug.py index 4eaba92e0..351ef9193 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -136,16 +136,18 @@ def test_debug_callers(self): 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. last_line = out_lines.splitlines()[-1] + + # The details of what to expect on the stack are empirical, and can change + # as the code changes. This test is here to ensure that the debug code + # continues working. It's ok to adjust these details over time. from coverage.data import STORAGE if STORAGE == "json": self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") else: - self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Creating data file") - self.assertRegex(last_line, r"\s+_create_db : .*coverage[/\\]sqldata.py @\d+$") + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Adding file tracers: 0 files") + self.assertRegex(last_line, r"\s+add_file_tracers : .*coverage[/\\]sqldata.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) From 9bc6b93805a5f20a87211a315d00503eddab66dc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 8 Jul 2019 07:51:24 -0400 Subject: [PATCH 572/952] SQLite will keep comments that are inside, not outside This way, ".schema" in the SQLite prompt will show the comments for the tables. --- coverage/sqldata.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index a182d8291..af1c837ad 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -28,45 +28,45 @@ SCHEMA_VERSION = 2 SCHEMA = """ --- One row, to record the version of the schema store in this db. CREATE TABLE coverage_schema ( + -- One row, to record the version of the schema store in this db. version integer -- Schema versions: -- 1: Released in 5.0a2 -- 2: Added contexts in 5.0a3. This is schema 2. ); --- One row, to record some metadata about the data CREATE TABLE meta ( + -- One row, to record some metadata about the data has_lines boolean, -- Is this data recording lines? has_arcs boolean, -- .. or branches? sys_argv text -- The coverage command line that recorded the data. ); --- A row per file measured. CREATE TABLE file ( + -- A row per file measured. id integer primary key, path text, unique(path) ); --- A row per context measured. CREATE TABLE context ( + -- A row per context measured. id integer primary key, context text, unique(context) ); --- If recording lines, a row per context per line executed. CREATE TABLE line ( + -- If recording lines, a row per context per line executed. file_id integer, -- foreign key to `file`. context_id integer, -- foreign key to `context`. lineno integer, -- the line number. unique(file_id, context_id, lineno) ); --- If recording branches, a row per context per from/to line transition executed. CREATE TABLE arc ( + -- If recording branches, a row per context per from/to line transition executed. file_id integer, -- foreign key to `file`. context_id integer, -- foreign key to `context`. fromno integer, -- line number jumped from. @@ -74,8 +74,8 @@ unique(file_id, context_id, fromno, tono) ); --- A row per file indicating the tracer used for that file. CREATE TABLE tracer ( + -- A row per file indicating the tracer used for that file. file_id integer primary key, tracer text ); From e1e474f5dda602551fbc0cbd30e441e194d9421f Mon Sep 17 00:00:00 2001 From: Matt Bachmann Date: Tue, 9 Jul 2019 23:12:27 -0400 Subject: [PATCH 573/952] Isolate the os module to protect from aggressive mocking interfearing with database operations --- coverage/sqldata.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index af1c837ad..6d618170d 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -22,8 +22,9 @@ from coverage.data import filename_suffix from coverage.debug import NoDebugging, SimpleReprMixin from coverage.files import PathAliases -from coverage.misc import CoverageException, file_be_gone +from coverage.misc import CoverageException, file_be_gone, isolate_module +os = isolate_module(os) SCHEMA_VERSION = 2 From 6b226d85f5191cd27b20ad27caded8b407772a02 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 9 Jul 2019 16:22:51 -0400 Subject: [PATCH 574/952] Remove the JSON data code --- CHANGES.rst | 3 + coverage/data.py | 717 +----------------------------------------- coverage/misc.py | 14 + coverage/sqldata.py | 74 ++++- lab/gendata.py | 42 --- tests/coveragetest.py | 5 +- tests/test_context.py | 5 - tests/test_data.py | 130 +------- tests/test_debug.py | 9 +- tests/test_html.py | 1 - 10 files changed, 102 insertions(+), 898 deletions(-) delete mode 100644 lab/gendata.py diff --git a/CHANGES.rst b/CHANGES.rst index ec5fea2d9..7438771da 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -34,6 +34,9 @@ Unreleased - Dynamic contexts with no data are no longer written to the database. +- SQLite data storage is now faster. There's no longer a reason to keep the + JSON data file code, so it has been removed. + - Added the classmethod :meth:`Coverage.current` to get the latest started `Coverage` instance. diff --git a/coverage/data.py b/coverage/data.py index bcb418b88..82bf1d41c 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -1,667 +1,20 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -"""Coverage data for coverage.py.""" +"""Coverage data for coverage.py. -import collections -import glob -import itertools -import json -import optparse -import os -import os.path -import random -import re -import socket - -from coverage import env -from coverage.backward import iitems, string_class -from coverage.debug import NoDebugging -from coverage.files import PathAliases -from coverage.misc import CoverageException, file_be_gone, isolate_module - -os = isolate_module(os) - - -def filename_suffix(suffix): - if suffix is True: - # If data_suffix was a simple true value, then make a suffix with - # plenty of distinguishing information. We do this here in - # `save()` at the last minute so that the pid will be correct even - # if the process forks. - dice = random.Random(os.urandom(8)).randint(0, 999999) - suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) - return suffix - - -class CoverageJsonData(object): - """Manages collected coverage data, including file storage. - - This class is the public supported API to the data coverage.py collects - during program execution. It includes information about what code was - executed. It does not include information from the analysis phase, to - determine what lines could have been executed, or what lines were not - executed. - - .. note:: - - The file format is not documented or guaranteed. It will change in - the future, in possibly complicated ways. Do not read coverage.py - data files directly. Use this API to avoid disruption. - - There are a number of kinds of data that can be collected: - - * **lines**: the line numbers of source lines that were executed. - These are always available. - - * **arcs**: pairs of source and destination line numbers for transitions - between source lines. These are only available if branch coverage was - used. - - * **file tracer names**: the module names of the file tracer plugins that - handled each file in the data. - - * **run information**: information about the program execution. This is - written during "coverage run", and then accumulated during "coverage - combine". - - Lines, arcs, and file tracer names are stored for each source file. File - names in this API are case-sensitive, even on platforms with - case-insensitive file systems. - - A data file is associated with the data when the :class:`CoverageData` - is created. - - To read a coverage.py data file, use :meth:`read`. You can then - access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, - or :meth:`file_tracer`. Run information is available with - :meth:`run_infos`. - - The :meth:`has_arcs` method indicates whether arc data is available. You - can get a list of the files in the data with :meth:`measured_files`. - A summary of the line data is available from :meth:`line_counts`. As with - most Python containers, you can determine if there is any data at all by - using this object as a boolean value. - - Most data files will be created by coverage.py itself, but you can use - methods here to create data files if you like. The :meth:`add_lines`, - :meth:`add_arcs`, and :meth:`add_file_tracers` methods add data, in ways - that are convenient for coverage.py. The :meth:`add_run_info` method adds - key-value pairs to the run information. - - To add a source file without any measured data, use :meth:`touch_file`. - - Write the data to its file with :meth:`write`. - - You can clear the data in memory with :meth:`erase`. Two data collections - can be combined by using :meth:`update` on one :class:`CoverageData`, - passing it the other. - - """ - - # The data file format is JSON, with these keys: - # - # * lines: a dict mapping file names to lists of line numbers - # executed:: - # - # { "file1": [17,23,45], "file2": [1,2,3], ... } - # - # * arcs: a dict mapping file names to lists of line number pairs:: - # - # { "file1": [[17,23], [17,25], [25,26]], ... } - # - # * file_tracers: a dict mapping file names to plugin names:: - # - # { "file1": "django.coverage", ... } - # - # * runs: a list of dicts of information about the coverage.py runs - # contributing to the data:: - # - # [ { "brief_sys": "CPython 2.7.10 Darwin" }, ... ] - # - # Only one of `lines` or `arcs` will be present: with branch coverage, data - # is stored as arcs. Without branch coverage, it is stored as lines. The - # line data is easily recovered from the arcs: it is all the first elements - # of the pairs that are greater than zero. - - def __init__(self, basename=None, suffix=None, warn=None, debug=None): - """Create a CoverageData. - - `warn` is the warning function to use. - - `basename` is the name of the file to use for storing data. - - `debug` is a `DebugControl` object for writing debug messages. - - """ - self._warn = warn - self._debug = debug or NoDebugging() - self.filename = os.path.abspath(basename or ".coverage") - self.suffix = suffix - - # A map from canonical Python source file name to a dictionary in - # which there's an entry for each line number that has been - # executed: - # - # { 'filename1.py': [12, 47, 1001], ... } - # - self._lines = None - - # A map from canonical Python source file name to a dictionary with an - # entry for each pair of line numbers forming an arc: - # - # { 'filename1.py': [(12,14), (47,48), ... ], ... } - # - self._arcs = None - - # A map from canonical source file name to a plugin module name: - # - # { 'filename1.py': 'django.coverage', ... } - # - self._file_tracers = {} - - # A list of dicts of information about the coverage.py runs. - self._runs = [] - - def __repr__(self): - return "<{klass} lines={lines} arcs={arcs} tracers={tracers} runs={runs}>".format( - klass=self.__class__.__name__, - lines="None" if self._lines is None else "{{{0}}}".format(len(self._lines)), - arcs="None" if self._arcs is None else "{{{0}}}".format(len(self._arcs)), - tracers="{{{0}}}".format(len(self._file_tracers)), - runs="[{0}]".format(len(self._runs)), - ) - - ## - ## Reading data - ## - - def set_query_contexts(self, contexts=None): - """Set the query contexts. - - No-op, since contexts are not supported for this data format. - """ - pass - - def has_arcs(self): - """Does this data have arcs? - - Arc data is only available if branch coverage was used during - collection. - - Returns a boolean. - - """ - return self._has_arcs() - - def lines(self, filename, contexts=None): - """Get the list of lines executed for a file. - - If the file was not measured, returns None. A file might be measured, - and have no lines executed, in which case an empty list is returned. - - If the file was executed, returns a list of integers, the line numbers - executed in the file. The list is in no particular order. - - `contexts` is ignored, since contexts are not supported for this data - format. - """ - if self._arcs is not None: - arcs = self._arcs.get(filename) - if arcs is not None: - all_lines = itertools.chain.from_iterable(arcs) - return list(set(l for l in all_lines if l > 0)) - elif self._lines is not None: - return self._lines.get(filename) - return None - - def arcs(self, filename, contexts=None): - """Get the list of arcs executed for a file. - - If the file was not measured, returns None. A file might be measured, - and have no arcs executed, in which case an empty list is returned. - - If the file was executed, returns a list of 2-tuples of integers. Each - pair is a starting line number and an ending line number for a - transition from one line to another. The list is in no particular - order. - - Negative numbers have special meaning. If the starting line number is - -N, it represents an entry to the code object that starts at line N. - If the ending ling number is -N, it's an exit from the code object that - starts at line N. - - `contexts` is ignored, since contexts are not supported for this data - format. - """ - if self._arcs is not None: - if filename in self._arcs: - return self._arcs[filename] - return None - - def file_tracer(self, filename): - """Get the plugin name of the file tracer for a file. - - Returns the name of the plugin that handles this file. If the file was - measured, but didn't use a plugin, then "" is returned. If the file - was not measured, then None is returned. - - """ - # Because the vast majority of files involve no plugin, we don't store - # them explicitly in self._file_tracers. Check the measured data - # instead to see if it was a known file with no plugin. - if filename in (self._arcs or self._lines or {}): - return self._file_tracers.get(filename, "") - return None - - def contexts_by_lineno(self, filename): - return collections.defaultdict(list) - - def run_infos(self): - """Return the list of dicts of run information. - - For data collected during a single run, this will be a one-element - list. If data has been combined, there will be one element for each - original data file. - - """ - return self._runs - - def measured_files(self): - """A set of all files that had been measured.""" - return set(self._arcs or self._lines or {}) - - def __nonzero__(self): - return bool(self._lines or self._arcs) - - __bool__ = __nonzero__ - - def read(self): - """Read the coverage data. - - It is fine for the file to not exist, in which case no data is read. - - """ - if os.path.exists(self.filename): - self._read_file(self.filename) - - def _read_fileobj(self, file_obj): - """Read the coverage data from the given file object. - - Should only be used on an empty CoverageData object. - - """ - data = self._read_raw_data(file_obj) - - self._lines = self._arcs = None - - if 'lines' in data: - self._lines = data['lines'] - if 'arcs' in data: - self._arcs = dict( - (fname, [tuple(pair) for pair in arcs]) - for fname, arcs in iitems(data['arcs']) - ) - self._file_tracers = data.get('file_tracers', {}) - self._runs = data.get('runs', []) - - self._validate() - - def _read_file(self, filename): - """Read the coverage data from `filename` into this object.""" - if self._debug.should('dataio'): - self._debug.write("Reading data from %r" % (filename,)) - try: - with self._open_for_reading(filename) as f: - self._read_fileobj(f) - except Exception as exc: - raise CoverageException( - "Couldn't read data from '%s': %s: %s" % ( - filename, exc.__class__.__name__, exc, - ) - ) - - _GO_AWAY = "!coverage.py: This is a private format, don't read it directly!" - - @classmethod - def _open_for_reading(cls, filename): - """Open a file appropriately for reading data.""" - return open(filename, "r") - - @classmethod - def _read_raw_data(cls, file_obj): - """Read the raw data from a file object.""" - go_away = file_obj.read(len(cls._GO_AWAY)) - if go_away != cls._GO_AWAY: - raise CoverageException("Doesn't seem to be a coverage.py data file") - return json.load(file_obj) - - @classmethod - def _read_raw_data_file(cls, filename): - """Read the raw data from a file, for debugging.""" - with cls._open_for_reading(filename) as f: - return cls._read_raw_data(f) +This file had the 4.x JSON data support, which is now gone. This file still +has storage-agnostic helpers, and is kept to avoid changing too many imports. +CoverageData is now defined in sqldata.py, and imported here to keep the +imports working. - ## - ## Writing data - ## - - def add_lines(self, line_data): - """Add measured line data. - - `line_data` is a dictionary mapping file names to dictionaries:: - - { filename: { lineno: None, ... }, ...} - - """ - if self._debug.should('dataop'): - self._debug.write("Adding lines: %d files, %d lines total" % ( - len(line_data), sum(len(lines) for lines in line_data.values()) - )) - if self._has_arcs(): - raise CoverageException("Can't add lines to existing arc data") - - if self._lines is None: - self._lines = {} - for filename, linenos in iitems(line_data): - if filename in self._lines: - new_linenos = set(self._lines[filename]) - new_linenos.update(linenos) - linenos = new_linenos - self._lines[filename] = list(linenos) - - self._validate() - - def add_arcs(self, arc_data): - """Add measured arc data. - - `arc_data` is a dictionary mapping file names to dictionaries:: - - { filename: { (l1,l2): None, ... }, ...} - - """ - if self._debug.should('dataop'): - self._debug.write("Adding arcs: %d files, %d arcs total" % ( - len(arc_data), sum(len(arcs) for arcs in arc_data.values()) - )) - if self._has_lines(): - raise CoverageException("Can't add arcs to existing line data") - - if self._arcs is None: - self._arcs = {} - for filename, arcs in iitems(arc_data): - if filename in self._arcs: - new_arcs = set(self._arcs[filename]) - new_arcs.update(arcs) - arcs = new_arcs - self._arcs[filename] = list(arcs) - - self._validate() - - def add_file_tracers(self, file_tracers): - """Add per-file plugin information. - - `file_tracers` is { filename: plugin_name, ... } - - """ - if self._debug.should('dataop'): - self._debug.write("Adding file tracers: %d files" % (len(file_tracers),)) - - existing_files = self._arcs or self._lines or {} - for filename, plugin_name in iitems(file_tracers): - if filename not in existing_files: - raise CoverageException( - "Can't add file tracer data for unmeasured file '%s'" % (filename,) - ) - existing_plugin = self._file_tracers.get(filename) - if existing_plugin is not None and plugin_name != existing_plugin: - raise CoverageException( - "Conflicting file tracer name for '%s': %r vs %r" % ( - filename, existing_plugin, plugin_name, - ) - ) - self._file_tracers[filename] = plugin_name - - self._validate() - - def add_run_info(self, **kwargs): - """Add information about the run. - - Keywords are arbitrary, and are stored in the run dictionary. Values - must be JSON serializable. You may use this function more than once, - but repeated keywords overwrite each other. - - """ - if self._debug.should('dataop'): - self._debug.write("Adding run info: %r" % (kwargs,)) - if not self._runs: - self._runs = [{}] - self._runs[0].update(kwargs) - self._validate() - - def touch_file(self, filename, plugin_name=""): - """Ensure that `filename` appears in the data, empty if needed. - - `plugin_name` is the name of the plugin responsible for this file. It is used - to associate the right filereporter, etc. - """ - if self._debug.should('dataop'): - self._debug.write("Touching %r" % (filename,)) - if not self._has_arcs() and not self._has_lines(): - raise CoverageException("Can't touch files in an empty CoverageData") - - if self._has_arcs(): - where = self._arcs - else: - where = self._lines - where.setdefault(filename, []) - if plugin_name: - # Set the tracer for this file - self._file_tracers[filename] = plugin_name - - self._validate() - - def set_context(self, context): - """Set the context. Not implemented for JSON storage.""" - if context: - raise CoverageException("JSON storage doesn't support contexts") - - def write(self): - """Write the collected coverage data to a file. - - `suffix` is a suffix to append to the base file name. This can be used - for multiple or parallel execution, so that many coverage data files - can exist simultaneously. A dot will be used to join the base name and - the suffix. - - """ - filename = self.filename - suffix = filename_suffix(self.suffix) - if suffix: - filename += "." + suffix - self._write_file(filename) - - def _write_fileobj(self, file_obj): - """Write the coverage data to `file_obj`.""" - - # Create the file data. - file_data = {} - - if self._has_arcs(): - file_data['arcs'] = self._arcs - - if self._has_lines(): - file_data['lines'] = self._lines - - if self._file_tracers: - file_data['file_tracers'] = self._file_tracers - - if self._runs: - file_data['runs'] = self._runs - - # Write the data to the file. - file_obj.write(self._GO_AWAY) - json.dump(file_data, file_obj, separators=(',', ':')) - - def _write_file(self, filename): - """Write the coverage data to `filename`.""" - if self._debug.should('dataio'): - self._debug.write("Writing data to %r" % (filename,)) - with open(filename, 'w') as fdata: - self._write_fileobj(fdata) - - def erase(self, parallel=False): - """Erase the data in this object. - - If `parallel` is true, then also deletes data files created from the - basename by parallel-mode. - - """ - self._lines = None - self._arcs = None - self._file_tracers = {} - self._runs = [] - self._validate() - - if self._debug.should('dataio'): - self._debug.write("Erasing data file %r" % (self.filename,)) - file_be_gone(self.filename) - if parallel: - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - pattern = os.path.join(os.path.abspath(data_dir), localdot) - for filename in glob.glob(pattern): - if self._debug.should('dataio'): - self._debug.write("Erasing parallel data file %r" % (filename,)) - file_be_gone(filename) - - def update(self, other_data, aliases=None): - """Update this data with data from another `CoverageData`. - - If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. - - """ - if self._has_lines() and other_data._has_arcs(): - raise CoverageException("Can't combine arc data with line data") - if self._has_arcs() and other_data._has_lines(): - raise CoverageException("Can't combine line data with arc data") - - aliases = aliases or PathAliases() - - # _file_tracers: only have a string, so they have to agree. - # Have to do these first, so that our examination of self._arcs and - # self._lines won't be confused by data updated from other_data. - for filename in other_data.measured_files(): - other_plugin = other_data.file_tracer(filename) - filename = aliases.map(filename) - this_plugin = self.file_tracer(filename) - if this_plugin is None: - if other_plugin: - self._file_tracers[filename] = other_plugin - elif this_plugin != other_plugin: - raise CoverageException( - "Conflicting file tracer name for '%s': %r vs %r" % ( - filename, this_plugin, other_plugin, - ) - ) - - # _runs: add the new runs to these runs. - self._runs.extend(other_data._runs) - - # _lines: merge dicts. - if other_data._has_lines(): - if self._lines is None: - self._lines = {} - for filename, file_lines in iitems(other_data._lines): - filename = aliases.map(filename) - if filename in self._lines: - lines = set(self._lines[filename]) - lines.update(file_lines) - file_lines = list(lines) - self._lines[filename] = file_lines - - # _arcs: merge dicts. - if other_data._has_arcs(): - if self._arcs is None: - self._arcs = {} - for filename, file_arcs in iitems(other_data._arcs): - filename = aliases.map(filename) - if filename in self._arcs: - arcs = set(self._arcs[filename]) - arcs.update(file_arcs) - file_arcs = list(arcs) - self._arcs[filename] = file_arcs - - self._validate() - - ## - ## Miscellaneous - ## - - def _validate(self): - """If we are in paranoid mode, validate that everything is right.""" - if env.TESTING: - self._validate_invariants() - - def _validate_invariants(self): - """Validate internal invariants.""" - # Only one of _lines or _arcs should exist. - assert not(self._has_lines() and self._has_arcs()), ( - "Shouldn't have both _lines and _arcs" - ) - - # _lines should be a dict of lists of ints. - if self._has_lines(): - for fname, lines in iitems(self._lines): - assert isinstance(fname, string_class), "Key in _lines shouldn't be %r" % (fname,) - assert all(isinstance(x, int) for x in lines), ( - "_lines[%r] shouldn't be %r" % (fname, lines) - ) - - # _arcs should be a dict of lists of pairs of ints. - if self._has_arcs(): - for fname, arcs in iitems(self._arcs): - assert isinstance(fname, string_class), "Key in _arcs shouldn't be %r" % (fname,) - assert all(isinstance(x, int) and isinstance(y, int) for x, y in arcs), ( - "_arcs[%r] shouldn't be %r" % (fname, arcs) - ) - - # _file_tracers should have only non-empty strings as values. - for fname, plugin in iitems(self._file_tracers): - assert isinstance(fname, string_class), ( - "Key in _file_tracers shouldn't be %r" % (fname,) - ) - assert plugin and isinstance(plugin, string_class), ( - "_file_tracers[%r] shoudn't be %r" % (fname, plugin) - ) - - # _runs should be a list of dicts. - for val in self._runs: - assert isinstance(val, dict) - for key in val: - assert isinstance(key, string_class), "Key in _runs shouldn't be %r" % (key,) - - ## - ## Internal - ## - - def _has_lines(self): - """Do we have data in self._lines?""" - return self._lines is not None - - def _has_arcs(self): - """Do we have data in self._arcs?""" - return self._arcs is not None +""" +import glob +import os.path -# $set_env.py: COVERAGE_STORAGE - The storage implementation to use: sql (default), or json. -STORAGE = os.environ.get("COVERAGE_STORAGE", "sql") -if STORAGE == "json": - CoverageData = CoverageJsonData -elif STORAGE == "sql": - from coverage.sqldata import CoverageSqliteData - CoverageData = CoverageSqliteData +from coverage.misc import CoverageException, file_be_gone +from coverage.sqldata import CoverageData def line_counts(data, fullpath=False): @@ -769,53 +122,3 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): if strict and not files_combined: raise CoverageException("No usable data files") - -def canonicalize_json_data(data): - """Canonicalize our JSON data so it can be compared.""" - for fname, lines in iitems(data.get('lines', {})): - data['lines'][fname] = sorted(lines) - for fname, arcs in iitems(data.get('arcs', {})): - data['arcs'][fname] = sorted(arcs) - - -def pretty_data(data): - """Format data as JSON, but as nicely as possible. - - Returns a string. - - """ - # Start with a basic JSON dump. - out = json.dumps(data, indent=4, sort_keys=True) - # But pairs of numbers shouldn't be split across lines... - out = re.sub(r"\[\s+(-?\d+),\s+(-?\d+)\s+]", r"[\1, \2]", out) - # Trailing spaces mess with tests, get rid of them. - out = re.sub(r"(?m)\s+$", "", out) - return out - - -def debug_main(args): - """Dump the raw data from data files. - - Run this as:: - - $ python -m coverage.data [FILE] - - """ - parser = optparse.OptionParser() - parser.add_option( - "-c", "--canonical", action="store_true", - help="Sort data into a canonical order", - ) - options, args = parser.parse_args(args) - - for filename in (args or [".coverage"]): - print("--- {0} ------------------------------".format(filename)) - data = CoverageData._read_raw_data_file(filename) - if options.canonical: - canonicalize_json_data(data) - print(pretty_data(data)) - - -if __name__ == '__main__': - import sys - debug_main(sys.argv[1:]) diff --git a/coverage/misc.py b/coverage/misc.py index ad7b834f7..00e88fdb8 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -9,7 +9,9 @@ import locale import os import os.path +import random import re +import socket import sys import types @@ -175,6 +177,18 @@ def output_encoding(outfile=None): return encoding +def filename_suffix(suffix): + """Compute a filename suffix for a data file.""" + if suffix is True: + # If data_suffix was a simple true value, then make a suffix with + # plenty of distinguishing information. We do this here in + # `save()` at the last minute so that the pid will be correct even + # if the process forks. + dice = random.Random(os.urandom(8)).randint(0, 999999) + suffix = "%s.%s.%06d" % (socket.gethostname(), os.getpid(), dice) + return suffix + + class Hasher(object): """Hashes Python data into md5.""" def __init__(self): diff --git a/coverage/sqldata.py b/coverage/sqldata.py index af1c837ad..2a380893f 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -4,8 +4,6 @@ """Sqlite coverage data.""" # TODO: get sys_info for data class, so we can see sqlite version etc -# TODO: get rid of skip_unless_data_storage_is -# TODO: get rid of "JSON message" and "SQL message" in the tests # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow # TODO: should writes be batched? @@ -19,10 +17,9 @@ import sys from coverage.backward import get_thread_id, iitems -from coverage.data import filename_suffix from coverage.debug import NoDebugging, SimpleReprMixin from coverage.files import PathAliases -from coverage.misc import CoverageException, file_be_gone +from coverage.misc import CoverageException, file_be_gone, filename_suffix SCHEMA_VERSION = 2 @@ -82,7 +79,72 @@ """ -class CoverageSqliteData(SimpleReprMixin): +class CoverageData(SimpleReprMixin): + """Manages collected coverage data, including file storage. + + TODO: This is the 4.x docstring. Update it for 5.0. + + This class is the public supported API to the data coverage.py collects + during program execution. It includes information about what code was + executed. It does not include information from the analysis phase, to + determine what lines could have been executed, or what lines were not + executed. + + .. note:: + + The file format is not documented or guaranteed. It will change in + the future, in possibly complicated ways. Do not read coverage.py + data files directly. Use this API to avoid disruption. + + There are a number of kinds of data that can be collected: + + * **lines**: the line numbers of source lines that were executed. + These are always available. + + * **arcs**: pairs of source and destination line numbers for transitions + between source lines. These are only available if branch coverage was + used. + + * **file tracer names**: the module names of the file tracer plugins that + handled each file in the data. + + * **run information**: information about the program execution. This is + written during "coverage run", and then accumulated during "coverage + combine". + + Lines, arcs, and file tracer names are stored for each source file. File + names in this API are case-sensitive, even on platforms with + case-insensitive file systems. + + A data file is associated with the data when the :class:`CoverageData` + is created. + + To read a coverage.py data file, use :meth:`read`. You can then + access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, + or :meth:`file_tracer`. Run information is available with + :meth:`run_infos`. + + The :meth:`has_arcs` method indicates whether arc data is available. You + can get a list of the files in the data with :meth:`measured_files`. + A summary of the line data is available from :meth:`line_counts`. As with + most Python containers, you can determine if there is any data at all by + using this object as a boolean value. + + Most data files will be created by coverage.py itself, but you can use + methods here to create data files if you like. The :meth:`add_lines`, + :meth:`add_arcs`, and :meth:`add_file_tracers` methods add data, in ways + that are convenient for coverage.py. The :meth:`add_run_info` method adds + key-value pairs to the run information. + + To add a source file without any measured data, use :meth:`touch_file`. + + Write the data to its file with :meth:`write`. + + You can clear the data in memory with :meth:`erase`. Two data collections + can be combined by using :meth:`update` on one :class:`CoverageData`, + passing it the other. + + """ def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._basename = os.path.abspath(basename or ".coverage") @@ -348,7 +410,7 @@ def touch_file(self, filename, plugin_name=""): if self._debug.should('dataop'): self._debug.write("Touching %r" % (filename,)) if not self._has_arcs and not self._has_lines: - raise CoverageException("Can't touch files in an empty CoverageSqliteData") + raise CoverageException("Can't touch files in an empty CoverageData") self._file_id(filename, add=True) if plugin_name: diff --git a/lab/gendata.py b/lab/gendata.py deleted file mode 100644 index 27ad4fda2..000000000 --- a/lab/gendata.py +++ /dev/null @@ -1,42 +0,0 @@ -# Run some timing tests of JsonData vs SqliteData. - -import random -import time - -from coverage.data import CoverageJsonData -from coverage.sqldata import CoverageSqliteData - -NUM_FILES = 1000 -NUM_LINES = 1000 - -def gen_data(cdata): - rnd = random.Random() - rnd.seed(17) - - def linenos(num_lines, prob): - return (n for n in range(num_lines) if random.random() < prob) - - start = time.time() - for i in range(NUM_FILES): - filename = "/src/foo/project/file{i}.py".format(i=i) - line_data = { filename: dict.fromkeys(linenos(NUM_LINES, .6)) } - cdata.add_lines(line_data) - - cdata.write() - end = time.time() - delta = end - start - return delta - -class DummyData: - def add_lines(self, line_data): - return - def write(self): - return - -overhead = gen_data(DummyData()) -jtime = gen_data(CoverageJsonData("gendata.json")) - overhead -stime = gen_data(CoverageSqliteData("gendata.db")) - overhead -print("Overhead: {overhead:.3f}s".format(overhead=overhead)) -print("JSON: {jtime:.3f}s".format(jtime=jtime)) -print("SQLite: {stime:.3f}s".format(stime=stime)) -print("{slower:.3f}x slower".format(slower=stime/jtime)) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index b06db896a..edb573569 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -25,7 +25,6 @@ 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.data import STORAGE from coverage.misc import arcz_to_arcs, StopEverything from tests.helpers import run_command, SuperModuleCleaner @@ -102,8 +101,8 @@ def setUp(self): def skip_unless_data_storage_is(self, storage): """Skip a test for tests that are particular about the storage implementation.""" - if STORAGE != storage: - self.skipTest("Not using {} for data storage".format(storage)) + assert storage == "json" + self.skipTest("Some features haven't been implemented in SQL yet.") def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. diff --git a/tests/test_context.py b/tests/test_context.py index 24e17069d..5d40e3393 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -18,10 +18,6 @@ class StaticContextTest(CoverageTest): """Tests of the static context.""" - def setUp(self): - super(StaticContextTest, self).setUp() - self.skip_unless_data_storage_is("sql") - def test_no_context(self): self.make_file("main.py", "a = 1") cov = coverage.Coverage() @@ -115,7 +111,6 @@ def setUp(self): if not env.C_TRACER: self.skipTest("Only the C tracer supports dynamic contexts") super(DynamicContextTest, self).setUp() - self.skip_unless_data_storage_is("sql") SOURCE = """\ def helper(lineno): diff --git a/tests/test_data.py b/tests/test_data.py index 4a3db93cd..bc988bcbc 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -4,17 +4,15 @@ """Tests for coverage.data""" import glob -import json import os import os.path -import re import sqlite3 import threading import mock -from coverage.data import CoverageData, debug_main, canonicalize_json_data, combine_parallel_data -from coverage.data import add_data_to_hash, line_counts, STORAGE +from coverage.data import CoverageData, combine_parallel_data +from coverage.data import add_data_to_hash, line_counts from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException @@ -107,9 +105,6 @@ def assert_arcs3_data(self, covdata): class CoverageDataTest(DataTestHelpers, CoverageTest): """Test cases for CoverageData.""" - # SQL data storage always has files on disk, even without .write(). - # We need to separate the tests so they don't clobber each other. - run_in_temp_dir = STORAGE == "sql" no_files_in_temp_dir = True def test_empty_data_is_false(self): @@ -185,7 +180,6 @@ def test_touch_file_with_arcs(self): self.assert_measured_files(covdata, MEASURED_FILES_3 + ['zzz.py']) def test_set_query_contexts(self): - self.skip_unless_data_storage_is("sql") covdata = CoverageData() covdata.set_context('test_a') covdata.add_lines(LINES_1) @@ -202,7 +196,6 @@ def test_no_lines_vs_unmeasured_file(self): self.assertIsNone(covdata.lines('no_such_file.py')) def test_lines_with_contexts(self): - self.skip_unless_data_storage_is("sql") covdata = CoverageData() covdata.set_context('test_a') covdata.add_lines(LINES_1) @@ -211,7 +204,6 @@ def test_lines_with_contexts(self): self.assertEqual(covdata.lines('a.py', contexts=['other*']), []) def test_contexts_by_lineno_with_lines(self): - self.skip_unless_data_storage_is("sql") covdata = CoverageData() covdata.set_context('test_a') covdata.add_lines(LINES_1) @@ -254,7 +246,6 @@ def test_no_arcs_vs_unmeasured_file(self): self.assertIsNone(covdata.arcs('no_such_file.py')) def test_arcs_with_contexts(self): - self.skip_unless_data_storage_is("sql") covdata = CoverageData() covdata.set_context('test_x') covdata.add_arcs(ARCS_3) @@ -265,7 +256,6 @@ def test_arcs_with_contexts(self): self.assertEqual(covdata.arcs('x.py', contexts=['other*']), []) def test_contexts_by_lineno_with_arcs(self): - self.skip_unless_data_storage_is("sql") covdata = CoverageData() covdata.set_context('test_x') covdata.add_arcs(ARCS_3) @@ -274,7 +264,6 @@ def test_contexts_by_lineno_with_arcs(self): {-1: ['test_x'], 1: ['test_x'], 2: ['test_x'], 3: ['test_x']}) def test_contexts_by_lineno_with_unknown_file(self): - self.skip_unless_data_storage_is("sql") covdata = CoverageData() self.assertDictEqual( covdata.contexts_by_lineno('xyz.py'), {}) @@ -562,17 +551,7 @@ def test_read_errors(self): covdata.read() self.assertFalse(covdata) - def test_read_json_errors(self): - self.skip_unless_data_storage_is("json") - self.make_file("misleading.dat", CoverageData._GO_AWAY + " this isn't JSON") - msg = r"Couldn't .* '.*[/\\]{0}': \S+" - with self.assertRaisesRegex(CoverageException, msg.format("misleading.dat")): - covdata = CoverageData("misleading.dat") - covdata.read() - self.assertFalse(covdata) - def test_read_sql_errors(self): - self.skip_unless_data_storage_is("sql") with sqlite3.connect("wrong_schema.db") as con: con.execute("create table coverage_schema (version integer)") con.execute("insert into coverage_schema (version) values (99)") @@ -590,51 +569,6 @@ def test_read_sql_errors(self): covdata.read() self.assertFalse(covdata) - def test_debug_main(self): - self.skip_unless_data_storage_is("json") - covdata1 = CoverageData(".coverage") - covdata1.add_lines(LINES_1) - covdata1.write() - debug_main([]) - - covdata2 = CoverageData("arcs.dat") - covdata2.add_arcs(ARCS_3) - covdata2.add_file_tracers({"y.py": "magic_plugin"}) - covdata2.add_run_info(version="v3.14", chunks=["z", "a"]) - covdata2.write() - - covdata3 = CoverageData("empty.dat") - covdata3.write() - debug_main(["arcs.dat", "empty.dat"]) - - expected = { - ".coverage": { - "lines": { - "a.py": [1, 2], - "b.py": [3], - }, - }, - "arcs.dat": { - "arcs": { - "x.py": [[-1, 1], [1, 2], [2, 3], [3, -1]], - "y.py": [[-1, 17], [17, 23], [23, -1]], - }, - "file_tracers": {"y.py": "magic_plugin"}, - "runs": [ - { - "chunks": ["z", "a"], - "version": "v3.14", - }, - ], - }, - "empty.dat": {}, - } - pieces = re.split(r"(?m)-+ ([\w.]+) -+$", self.stdout()) - for name, json_out in zip(pieces[1::2], pieces[2::2]): - json_got = json.loads(json_out) - canonicalize_json_data(json_got) - self.assertEqual(expected[name], json_got) - class CoverageDataFilesTest(DataTestHelpers, CoverageTest): """Tests of CoverageData file handling.""" @@ -670,14 +604,9 @@ def test_debug_output_with_debug_option(self): self.assertRegex( debug.get_output(), - r"(" # JSON output: - r"^Writing data to '.*\.coverage'\n" - r"Reading data from '.*\.coverage'\n$" - r"|" # SQL output: - r"Erasing data file '.*\.coverage'\n" + r"^Erasing data file '.*\.coverage'\n" r"Creating data file '.*\.coverage'\n" r"Opening data file '.*\.coverage'\n$" - r")" ) def test_debug_output_without_debug_option(self): @@ -766,59 +695,6 @@ def test_erasing_parallel(self): self.assert_file_count("datafile.*", 0) self.assert_exists(".coverage") - def read_json_data_file(self, fname): - """Read a JSON data file for testing the JSON directly.""" - self.skip_unless_data_storage_is("json") - with open(fname, 'r') as fdata: - go_away = fdata.read(len(CoverageData._GO_AWAY)) - self.assertEqual(go_away, CoverageData._GO_AWAY) - return json.load(fdata) - - def test_file_format(self): - # Write with CoverageData, then read the JSON explicitly. - covdata = CoverageData() - covdata.add_lines(LINES_1) - covdata.write() - - data = self.read_json_data_file(".coverage") - - lines = data['lines'] - self.assertCountEqual(lines.keys(), MEASURED_FILES_1) - self.assertCountEqual(lines['a.py'], A_PY_LINES_1) - self.assertCountEqual(lines['b.py'], B_PY_LINES_1) - # If not measuring branches, there's no arcs entry. - self.assertNotIn('arcs', data) - # If no file tracers were involved, there's no file_tracers entry. - self.assertNotIn('file_tracers', data) - - def test_file_format_with_arcs(self): - # Write with CoverageData, then read the JSON explicitly. - covdata = CoverageData() - covdata.add_arcs(ARCS_3) - covdata.write() - - data = self.read_json_data_file(".coverage") - - self.assertNotIn('lines', data) - arcs = data['arcs'] - self.assertCountEqual(arcs.keys(), MEASURED_FILES_3) - self.assertCountEqual(arcs['x.py'], map(list, X_PY_ARCS_3)) - self.assertCountEqual(arcs['y.py'], map(list, Y_PY_ARCS_3)) - # If no file tracers were involved, there's no file_tracers entry. - self.assertNotIn('file_tracers', data) - - def test_writing_to_other_file(self): - self.skipTest("This will be deleted!") # TODO - covdata = CoverageData(".otherfile") - covdata.add_lines(LINES_1) - covdata.write() - self.assert_doesnt_exist(".coverage") - self.assert_exists(".otherfile") - - covdata.write(suffix="extra") - self.assert_exists(".otherfile.extra") - self.assert_doesnt_exist(".coverage") - def test_combining_with_aliases(self): covdata1 = CoverageData(suffix='1') covdata1.add_lines({ diff --git a/tests/test_debug.py b/tests/test_debug.py index 351ef9193..7d4c0a165 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -141,13 +141,8 @@ def test_debug_callers(self): # The details of what to expect on the stack are empirical, and can change # as the code changes. This test is here to ensure that the debug code # continues working. It's ok to adjust these details over time. - from coverage.data import STORAGE - if STORAGE == "json": - self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") - self.assertRegex(last_line, r"\s+_write_file : .*coverage[/\\]data.py @\d+$") - else: - self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Adding file tracers: 0 files") - self.assertRegex(last_line, r"\s+add_file_tracers : .*coverage[/\\]sqldata.py @\d+$") + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Adding file tracers: 0 files") + self.assertRegex(last_line, r"\s+add_file_tracers : .*coverage[/\\]sqldata.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) diff --git a/tests/test_html.py b/tests/test_html.py index 8e9430ce1..3e5671133 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1050,7 +1050,6 @@ def setUp(self): if not env.C_TRACER: self.skipTest("Only the C tracer supports dynamic contexts") super(HtmlWithContextsTest, self).setUp() - self.skip_unless_data_storage_is("sql") SOURCE = """\ def helper(lineno): From 04a675ab3b115cd7b6d197341cb4a78087534773 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 10 Jul 2019 09:55:45 -0400 Subject: [PATCH 575/952] Document another environment variable --- coverage/control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coverage/control.py b/coverage/control.py index 250bc1375..7d9d9e911 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -948,6 +948,7 @@ def plugin_info(plugins): # Mega debugging... +# $set_env.py: COVERAGE_DEBUG_CALLS - Lots and lots of output about calls to Coverage. if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging from coverage.debug import decorate_methods, show_calls From 67c9a07c64210f66c1e3ef53b6ce1e51781193c4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 11 Jul 2019 12:45:44 -0400 Subject: [PATCH 576/952] CoverageData.dumps and loads for serialization --- CHANGES.rst | 8 ++++++++ coverage/sqldata.py | 29 +++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7438771da..1fff59ea9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -37,6 +37,14 @@ Unreleased - SQLite data storage is now faster. There's no longer a reason to keep the JSON data file code, so it has been removed. +- Changes to the `CoverageData` interface: + + - The new :meth:`CoverageData.dumps` method serializes the data to a string, + and a corresponding :meth:`CoverageData.loads` method reconstitutes ths + data. The format of the data string is subject to change at any time, and + so should only be used between two installations of the same version of + coverage.py. + - Added the classmethod :meth:`Coverage.current` to get the latest started `Coverage` instance. diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 998abd64f..a9ba0d41d 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -188,12 +188,9 @@ def _reset(self): def _create_db(self): if self._debug.should('dataio'): self._debug.write("Creating data file {!r}".format(self._filename)) - self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug) - with self._dbs[get_thread_id()] as db: - for stmt in SCHEMA.split(';'): - stmt = stmt.strip() - if stmt: - db.execute(stmt) + self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) + with db: + db.executescript(SCHEMA) db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) db.execute( "insert into meta (has_lines, has_arcs, sys_argv) values (?, ?, ?)", @@ -203,8 +200,8 @@ def _create_db(self): def _open_db(self): if self._debug.should('dataio'): self._debug.write("Opening data file {!r}".format(self._filename)) - self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug) - with self._dbs[get_thread_id()] as db: + self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) + with db: try: schema_version, = db.execute("select version from coverage_schema").fetchone() except Exception as exc: @@ -254,6 +251,17 @@ def dump(self): # pragma: debugging with self._connect() as con: self._debug.write(con.dump()) + def dumps(self): + with self._connect() as con: + return con.dump() + + def loads(self, data): + if self._debug.should('dataio'): + self._debug.write("Loading data into data file {!r}".format(self._filename)) + self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) + with db: + db.executescript(data) + def _file_id(self, filename, add=False): """Get the file id for `filename`. @@ -800,6 +808,11 @@ def executemany(self, sql, data): self.debug.write("Executing many {!r} with {} rows".format(sql, len(data))) return self.con.executemany(sql, data) + def executescript(self, script): + if self.debug: + self.debug.write("Executing script with {} chars".format(len(script))) + self.con.executescript(script) + def dump(self): # pragma: debugging """Return a multi-line string, the dump of the database.""" return "\n".join(self.con.iterdump()) From 3abb819fb68671d9b3a167a06bdbeaa5569df0c7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 11 Jul 2019 15:44:15 -0400 Subject: [PATCH 577/952] :memory: support --- CHANGES.rst | 4 ++++ coverage/sqldata.py | 21 +++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1fff59ea9..541cb997b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -45,6 +45,10 @@ Unreleased so should only be used between two installations of the same version of coverage.py. + - The `CoverageData` constructor has a new argument, `no_disk` (default: + False). Setting it to True prevents writing any data to the disk. This is + useful for transient data objects. + - Added the classmethod :meth:`Coverage.current` to get the latest started `Coverage` instance. diff --git a/coverage/sqldata.py b/coverage/sqldata.py index a9ba0d41d..67bcf77aa 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -147,7 +147,8 @@ class CoverageData(SimpleReprMixin): """ - def __init__(self, basename=None, suffix=None, warn=None, debug=None): + def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=None): + self._no_disk = no_disk self._basename = os.path.abspath(basename or ".coverage") self._suffix = suffix self._warn = warn @@ -171,10 +172,13 @@ def __init__(self, basename=None, suffix=None, warn=None, debug=None): self._query_context_ids = None def _choose_filename(self): - self._filename = self._basename - suffix = filename_suffix(self._suffix) - if suffix: - self._filename += "." + suffix + if self._no_disk: + self._filename = ":memory:" + else: + self._filename = self._basename + suffix = filename_suffix(self._suffix) + if suffix: + self._filename += "." + suffix def _reset(self): if self._dbs: @@ -756,8 +760,11 @@ def __init__(self, filename, debug): self.debug = debug if debug.should('sql') else None self.filename = filename self.nest = 0 + self.con = None def connect(self): + if self.con is not None: + return # SQLite on Windows on py2 won't open a file if the filename argument # has non-ascii characters in it. Opening a relative file name avoids # a problem if the current directory has non-ascii. @@ -779,7 +786,9 @@ def connect(self): self.execute("pragma synchronous=off").close() def close(self): - self.con.close() + if self.con is not None and self.filename != ":memory:": + self.con.close() + self.con = None def __enter__(self): if self.nest == 0: From 496e8c1a0efdf24dfa0430751362511aedd736f6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 15 Jul 2019 08:11:01 -0400 Subject: [PATCH 578/952] Bitmaps for line numbers, 10% of the size. --- CHANGES.rst | 12 +++-- coverage/backward.py | 5 ++ coverage/sqldata.py | 126 ++++++++++++++++++++++++++++++++----------- tests/test_data.py | 30 +++++++++++ 4 files changed, 138 insertions(+), 35 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 541cb997b..9dc57518b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -32,10 +32,16 @@ Unreleased information to each covered line. Hovering over the "ctx" marker at the end of the line reveals a list of the contexts that covered the line. -- Dynamic contexts with no data are no longer written to the database. +- Database changes: -- SQLite data storage is now faster. There's no longer a reason to keep the - JSON data file code, so it has been removed. + - Line numbers are now stored in a much more compact way. For each file and + context, a single binary string is stored with a bit per line number. This + greatly improves memory use, but makes ad-hoc use difficult. + + - Dynamic contexts with no data are no longer written to the database. + + - SQLite data storage is now faster. There's no longer a reason to keep the + JSON data file code, so it has been removed. - Changes to the `CoverageData` interface: diff --git a/coverage/backward.py b/coverage/backward.py index 05fa261eb..720cd3e1a 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -44,6 +44,11 @@ except NameError: range = range +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest + # Where do we get the thread id from? try: from thread import get_ident as get_thread_id diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 67bcf77aa..ea4922ca4 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -17,13 +17,16 @@ import sys from coverage.backward import get_thread_id, iitems +from coverage.backward import bytes_to_ints, binary_bytes, zip_longest from coverage.debug import NoDebugging, SimpleReprMixin +from coverage import env from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone, filename_suffix, isolate_module +from coverage.misc import contract os = isolate_module(os) -SCHEMA_VERSION = 2 +SCHEMA_VERSION = 3 SCHEMA = """ CREATE TABLE coverage_schema ( @@ -31,7 +34,8 @@ version integer -- Schema versions: -- 1: Released in 5.0a2 - -- 2: Added contexts in 5.0a3. This is schema 2. + -- 2: Added contexts in 5.0a3. + -- 3: Replaced line table with line_map table. ); CREATE TABLE meta ( @@ -55,12 +59,12 @@ unique(context) ); -CREATE TABLE line ( +CREATE TABLE line_map ( -- If recording lines, a row per context per line executed. file_id integer, -- foreign key to `file`. context_id integer, -- foreign key to `context`. - lineno integer, -- the line number. - unique(file_id, context_id, lineno) + bitmap blob, -- Nth bit represents line N. + unique(file_id, context_id) ); CREATE TABLE arc ( @@ -79,6 +83,17 @@ ); """ +if env.PY2: + def to_blob(bytes): + return buffer(bytes) + def from_blob(blob): + return bytes(blob) +else: + def to_blob(bytes): + return bytes + def from_blob(blob): + return blob + class CoverageData(SimpleReprMixin): """Manages collected coverage data, including file storage. @@ -335,11 +350,16 @@ def add_lines(self, line_data): with self._connect() as con: self._set_context_id() for filename, linenos in iitems(line_data): + linemap = nums_to_bitmap(linenos) file_id = self._file_id(filename, add=True) - data = [(file_id, self._current_context_id, lineno) for lineno in linenos] - con.executemany( - "insert or ignore into line (file_id, context_id, lineno) values (?, ?, ?)", - data, + query = "select bitmap from line_map where file_id = ? and context_id = ?" + existing = list(con.execute(query, (file_id, self._current_context_id))) + if existing: + linemap = merge_bitmaps(linemap, from_blob(existing[0][0])) + + con.execute( + "insert or replace into line_map (file_id, context_id, bitmap) values (?, ?, ?)", + (file_id, self._current_context_id, to_blob(linemap)), ) def add_arcs(self, arc_data): @@ -472,12 +492,12 @@ def update(self, other_data, aliases=None): # Get line data. cur = conn.execute( - 'select file.path, context.context, line.lineno ' - 'from line ' - 'inner join file on file.id = line.file_id ' - 'inner join context on context.id = line.context_id' - ) - lines = [(files[path], context, lineno) for (path, context, lineno) in cur] + 'select file.path, context.context, line_map.bitmap ' + 'from line_map ' + 'inner join file on file.id = line_map.file_id ' + 'inner join context on context.id = line_map.context_id' + ) + lines = {(files[path], context): from_blob(bitmap) for (path, context, bitmap) in cur} cur.close() # Get tracer data. @@ -546,22 +566,35 @@ def update(self, other_data, aliases=None): (file_ids[file], context_ids[context], fromno, tono) for file, context, fromno, tono in arcs ) - line_rows = ( - (file_ids[file], context_ids[context], lineno) - for file, context, lineno in lines - ) + + # Get line data. + cur = conn.execute( + 'select file.path, context.context, line_map.bitmap ' + 'from line_map ' + 'inner join file on file.id = line_map.file_id ' + 'inner join context on context.id = line_map.context_id' + ) + for path, context, bitmap in cur: + key = (aliases.map(path), context) + bitmap = from_blob(bitmap) + if key in lines: + bitmap = merge_bitmaps(lines[key], bitmap) + lines[key] = bitmap + cur.close() self._choose_lines_or_arcs(arcs=bool(arcs), lines=bool(lines)) + # Write the combined data. conn.executemany( 'insert or ignore into arc ' '(file_id, context_id, fromno, tono) values (?, ?, ?, ?)', arc_rows ) + conn.execute("delete from line_map") conn.executemany( - 'insert or ignore into line ' - '(file_id, context_id, lineno) values (?, ?, ?)', - line_rows + "insert into line_map " + "(file_id, context_id, bitmap) values (?, ?, ?)", + [(file_ids[file], context_ids[context], to_blob(bitmap)) for (file, context), bitmap in lines.items()] ) conn.executemany( 'insert or ignore into tracer (file_id, tracer) values (?, ?)', @@ -677,15 +710,18 @@ def lines(self, filename, contexts=None): if file_id is None: return None else: - query = "select distinct lineno from line where file_id = ?" + query = "select bitmap from line_map where file_id = ?" data = [file_id] context_ids = self._get_query_context_ids(contexts) if context_ids is not None: ids_array = ', '.join('?'*len(context_ids)) query += " and context_id in (" + ids_array + ")" data += context_ids - linenos = con.execute(query, data) - return [lineno for lineno, in linenos] + bitmaps = list(con.execute(query, data)) + nums = set() + for row in bitmaps: + nums.update(bitmap_to_nums(from_blob(row[0]))) + return sorted(nums) def arcs(self, filename, contexts=None): self._start_using() @@ -730,18 +766,18 @@ def contexts_by_lineno(self, filename): lineno_contexts_map[tono].append(context) else: query = ( - "select line.lineno, context.context " - "from line, context " - "where line.file_id = ? and line.context_id = context.id" - ) + "select l.bitmap, c.context from line_map l, context c " + "where l.context_id = c.id " + "and file_id = ?" + ) data = [file_id] context_ids = self._get_query_context_ids() if context_ids is not None: ids_array = ', '.join('?'*len(context_ids)) - query += " and line.context_id in (" + ids_array + ")" + query += " and l.context_id in (" + ids_array + ")" data += context_ids - for lineno, context in con.execute(query, data): - if context not in lineno_contexts_map[lineno]: + for bitmap, context in con.execute(query, data): + for lineno in bitmap_to_nums(from_blob(bitmap)): lineno_contexts_map[lineno].append(context) return lineno_contexts_map @@ -825,3 +861,29 @@ def executescript(self, script): def dump(self): # pragma: debugging """Return a multi-line string, the dump of the database.""" return "\n".join(self.con.iterdump()) + + +@contract(nums='Iterable', returns='bytes') +def nums_to_bitmap(nums): + """Convert `nums` (an iterable of ints) into a bitmap.""" + nbytes = max(nums) // 8 + 1 + b = bytearray(nbytes) + for num in nums: + b[num//8] |= 1 << num % 8 + return bytes(b) + +@contract(bitmap='bytes', returns='list[int]') +def bitmap_to_nums(bitmap): + """Convert a bitmap into a list of numbers.""" + nums = [] + for byte_i, byte in enumerate(bytes_to_ints(bitmap)): + for bit_i in range(8): + if (byte & (1 << bit_i)): + nums.append(byte_i * 8 + bit_i) + return nums + +@contract(map1='bytes', map2='bytes', returns='bytes') +def merge_bitmaps(map1, map2): + """Merge two bitmaps""" + byte_pairs = zip_longest(bytes_to_ints(map1), bytes_to_ints(map2), fillvalue=0) + return binary_bytes(b1 | b2 for b1, b2 in byte_pairs) diff --git a/tests/test_data.py b/tests/test_data.py index bc988bcbc..5ac08bb63 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -6,6 +6,7 @@ import glob import os import os.path +import random import sqlite3 import threading @@ -16,6 +17,7 @@ from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException +from coverage.sqldata import nums_to_bitmap, bitmap_to_nums, merge_bitmaps from tests.coveragetest import CoverageTest @@ -805,3 +807,31 @@ def test_interleaved_erasing_bug716(self): # then this would try to use tables that no longer exist. # "no such table: meta" covdata2.add_lines(LINES_1) + + +class BitmapOpTest(CoverageTest): + """Tests of the bitmap operations in sqldata.py.""" + + run_in_temp_dir = False + + def numbers(self, r): + """Produce a list of numbers from a Random object.""" + return list(set(r.randint(1, 1000) for _ in range(r.randint(100, 200)))) + + def test_conversion(self): + r = random.Random(1792) + for _ in range(10): + nums = self.numbers(r) + bitmap = nums_to_bitmap(nums) + self.assertEqual(sorted(bitmap_to_nums(bitmap)), sorted(nums)) + + def test_merging(self): + r = random.Random(314159) + for _ in range(10): + nums1 = self.numbers(r) + nums2 = self.numbers(r) + merged = bitmap_to_nums(merge_bitmaps(nums_to_bitmap(nums1), nums_to_bitmap(nums2))) + all_nums = set() + all_nums.update(nums1) + all_nums.update(nums2) + self.assertEqual(sorted(all_nums), sorted(merged)) From 6d7f6f85ed580c67c4dd1d6885011815cff57148 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 15 Jul 2019 21:10:54 -0400 Subject: [PATCH 579/952] Prep for 5.0a6 --- CHANGES.rst | 7 +++++-- doc/conf.py | 2 +- doc/index.rst | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9dc57518b..988bf951e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,8 +17,11 @@ development at the same time, like 4.5.x and 5.0. .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- -Unreleased ----------- + +.. _changes_50a6: + +Version 5.0a6 --- 2019-07-16 +---------------------------- - Reporting on dynamic contexts. Big thanks to Stephan Richter and Albertas Agejevas for the contribution. diff --git a/doc/conf.py b/doc/conf.py index b402c5c01..28ec17f49 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -58,7 +58,7 @@ # The short X.Y version. version = '5.0' # CHANGEME # The full version, including alpha/beta/rc tags. -release = '5.0a5' # CHANGEME +release = '5.0a6' # CHANGEME # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/index.rst b/doc/index.rst index 80b3e343d..5ffad66e2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -28,10 +28,10 @@ not. .. ifconfig:: prerelease - The latest version is coverage.py 5.0a5, released May 7, 2019. + The latest version is coverage.py 5.0a6, released July 16, 2019. It is supported on: - * Python versions 2.7, 3.5, 3.6, 3.7, and alpha 3.8. + * Python versions 2.7, 3.5, 3.6, 3.7, and beta 3.8. * PyPy2 7.0 and PyPy3 7.0. From e07ae475a53cbde6d26a5b1428b880afe0181e96 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Jul 2019 06:05:20 -0400 Subject: [PATCH 580/952] Tidelift security contact, and non-transparent logo. --- README.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 217194e8e..511d08788 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. -.. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White_small.png +.. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logo_small.png :width: 75 :alt: Tidelift :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme @@ -69,6 +69,15 @@ See the `Contributing section`_ of the docs. .. _Contributing section: https://coverage.readthedocs.io/en/latest/contributing.html +Security +-------- + +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. + +.. _Tidelift security contact: https://tidelift.com/security + + License ------- From 644a2c48cf44e9bd0803ec736e702d63d054f0d0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Jul 2019 06:17:49 -0400 Subject: [PATCH 581/952] Get more information when uploading kits --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9c2a971c9..6f3edb9bc 100644 --- a/Makefile +++ b/Makefile @@ -85,10 +85,10 @@ manylinux: docker run -it --init --rm -v `pwd`:/io quay.io/pypa/manylinux1_i686 /io/ci/manylinux.sh build kit_upload: - twine upload dist/* + twine upload --verbose dist/* test_upload: - twine upload --repository testpypi dist/* + twine upload --verbose --repository testpypi dist/* kit_local: # pip.conf looks like this: From 3e6f394ff576f28fad3bf9cafde957016d468008 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Jul 2019 06:28:56 -0400 Subject: [PATCH 582/952] Bump version --- CHANGES.rst | 5 +++++ coverage/version.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 988bf951e..4b3ad3e35 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,11 @@ development at the same time, like 4.5.x and 5.0. .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- +Unreleased +---------- + +- Nothing yet. + .. _changes_50a6: diff --git a/coverage/version.py b/coverage/version.py index 74a78ad6c..122e02ef7 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 0, 0, 'alpha', 6) +version_info = (5, 0, 0, 'alpha', 7) def _make_version(major, minor, micro, releaselevel, serial): From 8c9bb0cb6c8531b502c40433406f0e467a905501 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 16 Jul 2019 06:52:01 -0400 Subject: [PATCH 583/952] Correct some references --- CHANGES.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4b3ad3e35..29740d59a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -51,7 +51,7 @@ Version 5.0a6 --- 2019-07-16 - SQLite data storage is now faster. There's no longer a reason to keep the JSON data file code, so it has been removed. -- Changes to the `CoverageData` interface: +- Changes to the :class:`CoverageData` interface: - The new :meth:`CoverageData.dumps` method serializes the data to a string, and a corresponding :meth:`CoverageData.loads` method reconstitutes ths @@ -59,12 +59,12 @@ Version 5.0a6 --- 2019-07-16 so should only be used between two installations of the same version of coverage.py. - - The `CoverageData` constructor has a new argument, `no_disk` (default: - False). Setting it to True prevents writing any data to the disk. This is - useful for transient data objects. + - The :meth:`CoverageData constructor` has a new + argument, `no_disk` (default: False). Setting it to True prevents writing + any data to the disk. This is useful for transient data objects. - Added the classmethod :meth:`Coverage.current` to get the latest started - `Coverage` instance. + Coverage instance. - Error handling during reporting has changed slightly. All reporting methods now behave the same. The ``--ignore-errors`` option keeps errors from @@ -119,11 +119,11 @@ Version 5.0a5 --- 2019-05-07 - Combining data stored in SQLite now goes about twice as fast, fixing `issue 761`_. Thanks, Stephan Richter. -- The ``filename`` attribute on `CoverageData` object has been made private. - You can use the ``data_filename`` method to get the actual file name being - used to store data, and the ``base_filename`` method to get the original - filename before parallelizing suffixes were added. This is part of fixing - `issue 708`_. +- The ``filename`` attribute on :class:`CoverageData` objects has been made + private. You can use the ``data_filename`` method to get the actual file + name being used to store data, and the ``base_filename`` method to get the + original filename before parallelizing suffixes were added. This is part of + fixing `issue 708`_. - Line numbers in the HTML report now align properly with source lines, even when Chrome's minimum font size is set, fixing `issue 748`_. Thanks Wen Ye. From e4054498c6eb2f0b97de80bcf9835f416a91207c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 17 Jul 2019 21:09:25 -0400 Subject: [PATCH 584/952] Minor correction --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 29740d59a..9eace5d23 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -28,8 +28,8 @@ Unreleased Version 5.0a6 --- 2019-07-16 ---------------------------- -- Reporting on dynamic contexts. Big thanks to Stephan Richter and Albertas - Agejevas for the contribution. +- Reporting on contexts. Big thanks to Stephan Richter and Albertas Agejevas + for the contribution. - The ``--contexts`` option is available on the ``report`` and ``html`` commands. It's a comma-separated list of shell-style wildcards, selecting From 0a93630dc92de59194794f5f0b9ae1987157f327 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 19 Jul 2019 21:41:58 -0400 Subject: [PATCH 585/952] z-compressed dumps and loads --- coverage/backward.py | 8 ++++++++ coverage/sqldata.py | 29 +++++++++++++++++------------ tests/test_data.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/coverage/backward.py b/coverage/backward.py index 720cd3e1a..0df2a41eb 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -106,6 +106,10 @@ def to_bytes(s): """Convert string `s` to bytes.""" return s.encode('utf8') + def to_string(b): + """Convert bytes `b` to string.""" + return b.decode('utf8') + def binary_bytes(byte_values): """Produce a byte string with the ints from `byte_values`.""" return bytes(byte_values) @@ -120,6 +124,10 @@ def to_bytes(s): """Convert string `s` to bytes (no-op in 2.x).""" return s + def to_string(b): + """Convert bytes `b` to string.""" + return b + def binary_bytes(byte_values): """Produce a byte string with the ints from `byte_values`.""" return "".join(chr(b) for b in byte_values) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index ea4922ca4..00a72b9fe 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -15,9 +15,10 @@ import os import sqlite3 import sys +import zlib from coverage.backward import get_thread_id, iitems -from coverage.backward import bytes_to_ints, binary_bytes, zip_longest +from coverage.backward import bytes_to_ints, binary_bytes, zip_longest, to_bytes, to_string from coverage.debug import NoDebugging, SimpleReprMixin from coverage import env from coverage.files import PathAliases @@ -219,8 +220,11 @@ def _create_db(self): def _open_db(self): if self._debug.should('dataio'): self._debug.write("Opening data file {!r}".format(self._filename)) - self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) - with db: + self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug) + self._read_db() + + def _read_db(self): + with self._dbs[get_thread_id()] as db: try: schema_version, = db.execute("select version from coverage_schema").fetchone() except Exception as exc: @@ -264,22 +268,23 @@ def __nonzero__(self): __bool__ = __nonzero__ - def dump(self): # pragma: debugging - """Write a dump of the database.""" - if self._debug: - with self._connect() as con: - self._debug.write(con.dump()) - + @contract(returns='bytes') def dumps(self): with self._connect() as con: - return con.dump() + return b'z' + zlib.compress(to_bytes(con.dump())) + @contract(data='bytes') def loads(self, data): if self._debug.should('dataio'): self._debug.write("Loading data into data file {!r}".format(self._filename)) + if data[:1] != b'z': + raise CoverageException("Unrecognized serialization: {!r} (head of {} bytes)".format(data[:40], len(data))) + script = to_string(zlib.decompress(data[1:])) self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) with db: - db.executescript(data) + db.executescript(script) + self._read_db() + self._have_used = True def _file_id(self, filename, add=False): """Get the file id for `filename`. @@ -858,7 +863,7 @@ def executescript(self, script): self.debug.write("Executing script with {} chars".format(len(script))) self.con.executescript(script) - def dump(self): # pragma: debugging + def dump(self): """Return a multi-line string, the dump of the database.""" return "\n".join(self.con.iterdump()) diff --git a/tests/test_data.py b/tests/test_data.py index 5ac08bb63..ad752d653 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -7,6 +7,7 @@ import os import os.path import random +import re import sqlite3 import threading @@ -809,6 +810,33 @@ def test_interleaved_erasing_bug716(self): covdata2.add_lines(LINES_1) +class DumpsLoadsTest(DataTestHelpers, CoverageTest): + """Tests of CoverageData.dumps and loads.""" + + run_in_temp_dir = False + + def test_serialization(self): + covdata1 = CoverageData(no_disk=True) + covdata1.add_lines(LINES_1) + covdata1.add_lines(LINES_2) + serial = covdata1.dumps() + + covdata2 = CoverageData(no_disk=True) + covdata2.loads(serial) + self.assert_line_counts(covdata2, SUMMARY_1_2) + self.assert_measured_files(covdata2, MEASURED_FILES_1_2) + + def test_misfed_serialization(self): + covdata = CoverageData(no_disk=True) + bad_data = b'Hello, world!\x07 ' + b'z' * 100 + msg = r"Unrecognized serialization: {} \(head of {} bytes\)".format( + re.escape(repr(bad_data[:40])), + len(bad_data), + ) + with self.assertRaisesRegex(CoverageException, msg): + covdata.loads(bad_data) + + class BitmapOpTest(CoverageTest): """Tests of the bitmap operations in sqldata.py.""" From 8466069620395e7d6811a747ff7e5468d08adcfd Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 20 Jul 2019 07:51:32 -0400 Subject: [PATCH 586/952] Don't try to delete a file called ':memory:' --- coverage/sqldata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 00a72b9fe..0404004dc 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -618,6 +618,8 @@ def erase(self, parallel=False): """ self._reset() + if self._no_disk: + return if self._debug.should('dataio'): self._debug.write("Erasing data file {!r}".format(self._filename)) file_be_gone(self._filename) From 4e0f1e1be25b38404b9f1481c1b2076949ed0613 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 26 Jul 2019 20:25:03 -0400 Subject: [PATCH 587/952] Simplify --- coverage/context.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/coverage/context.py b/coverage/context.py index d2f75db28..903fc51b4 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -46,18 +46,11 @@ def qualname_from_frame(frame): """Get a qualified name for the code running in `frame`.""" co = frame.f_code fname = co.co_name - if not co.co_varnames: - func = frame.f_globals[fname] - return func.__module__ + '.' + fname - - first_arg = co.co_varnames[0] - if co.co_argcount and first_arg == "self": + method = None + if co.co_argcount and co.co_varnames[0] == "self": self = frame.f_locals["self"] - else: - func = frame.f_globals[fname] - return func.__module__ + '.' + fname + method = getattr(self, fname, None) - method = getattr(self, fname, None) if method is None: func = frame.f_globals[fname] return func.__module__ + '.' + fname From 07c3c5dbf5bc3ebe22855fd77a388f12a0bec066 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 26 Jul 2019 21:22:44 -0400 Subject: [PATCH 588/952] Don't be fooled by a class named test_something. Fixes #829 --- CHANGES.rst | 5 ++++- coverage/context.py | 4 +++- tests/test_context.py | 5 +++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9eace5d23..8833c0d02 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,10 @@ development at the same time, like 4.5.x and 5.0. Unreleased ---------- -- Nothing yet. +- A class named "test_something" no longer confuses the `test_function` dynamic + context setting. Fixes `issue 829`_. + +.. _issue 829: https://github.com/nedbat/coveragepy/issues/829 .. _changes_50a6: diff --git a/coverage/context.py b/coverage/context.py index 903fc51b4..ea13da21e 100644 --- a/coverage/context.py +++ b/coverage/context.py @@ -52,7 +52,9 @@ def qualname_from_frame(frame): method = getattr(self, fname, None) if method is None: - func = frame.f_globals[fname] + func = frame.f_globals.get(fname) + if func is None: + return None return func.__module__ + '.' + fname func = getattr(method, '__func__', None) diff --git a/tests/test_context.py b/tests/test_context.py index 5d40e3393..21d29a0c2 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -285,3 +285,8 @@ def test_oldstyle(self): self.skipTest("Old-style classes are only in Python 2") self.assertEqual(OldStyle().meth(), "tests.test_context.OldStyle.meth") self.assertEqual(OldChild().meth(), "tests.test_context.OldStyle.meth") + + def test_bug_829(self): + # A class with a name like a function shouldn't confuse qualname_from_frame. + class test_something(object): + self.assertEqual(get_qualname(), None) From aef3f0a8fba2342017f39925f85e925a7900a80a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 29 Jul 2019 07:33:56 -0400 Subject: [PATCH 589/952] Add a note about the 3.8 multiprocessing fix --- CHANGES.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8833c0d02..3796c392a 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -69,6 +69,9 @@ Version 5.0a6 --- 2019-07-16 - Added the classmethod :meth:`Coverage.current` to get the latest started Coverage instance. +- Multiprocessing support in Python 3.8 was broken, but is now fixed. Closes + `issue 828`_. + - Error handling during reporting has changed slightly. All reporting methods now behave the same. The ``--ignore-errors`` option keeps errors from stopping the reporting, but files that couldn't parse as Python will always @@ -76,7 +79,7 @@ Version 5.0a6 --- 2019-07-16 the ``[run] disable_warnings`` configuration setting. - Coverage.py no longer fails if the user program deletes its current - directory, closing `issue 806`_. Thanks, Dan Hemberger. + directory. Fixes `issue 806`_. Thanks, Dan Hemberger. - The scrollbar markers in the HTML report now accurately show the highlighted lines, regardless of what categories of line are highlighted. @@ -88,6 +91,7 @@ Version 5.0a6 --- 2019-07-16 .. _ShiningPanda: https://wiki.jenkins.io/display/JENKINS/ShiningPanda+Plugin .. _issue 806: https://github.com/nedbat/coveragepy/pull/806 +.. _issue 828: https://github.com/nedbat/coveragepy/issues/828 .. _changes_50a5: From 0dd148015ebbb525d74e1e1ae37c36b9997267ed Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 29 Jul 2019 11:33:56 -0400 Subject: [PATCH 590/952] Changelog from 4.5.4 --- CHANGES.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 3796c392a..4bc6fad7e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -277,6 +277,17 @@ Version 5.0a1 --- 2018-06-05 .. _issue 700: https://github.com/nedbat/coveragepy/issues/700 +.. _changes_454: + +Version 4.5.4 -- 2019-07-29 +--------------------------- + +- Multiprocessing support in Python 3.8 was broken, but is now fixed. Closes + `issue 828`_. + +.. _issue 828: https://github.com/nedbat/coveragepy/issues/828 + + .. _changes_453: Version 4.5.3 -- 2019-03-09 From a3086f723228d4efd9148b7816320f2e49a15daf Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 29 Jul 2019 11:43:05 -0400 Subject: [PATCH 591/952] No more windows.exe installers (https://www.python.org/dev/peps/pep-0527/) --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f4a41b51f..b77cfb4e2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -115,8 +115,8 @@ install: - "python -c \"import os; open('python{}.{}.bat'.format(*os.environ['TOXENV'][2:]), 'w').write('@{}\\\\python \\x25*\\n'.format(os.environ['PYTHON']))\"" build_script: - # If not a metacov job, then build wheels and .exe installers. - - if NOT "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% %PYTHON%\python setup.py bdist_wheel bdist_wininst + # If not a metacov job, then build wheel installers. + - if NOT "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% %PYTHON%\python setup.py bdist_wheel # Push everything in dist\ as an artifact. - ps: if ( Test-Path 'dist' -PathType Container ) { Get-ChildItem dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName ('dist\' + $_.Name) } } From 6831811a12c97724d09d5affe46c7dfd444eb222 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 29 Jul 2019 11:46:49 -0400 Subject: [PATCH 592/952] RTFD moved a page --- howto.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/howto.txt b/howto.txt index 3c4d5dd8c..cead8d214 100644 --- a/howto.txt +++ b/howto.txt @@ -81,7 +81,7 @@ - IF NOT PRE-RELEASE: - visit https://readthedocs.org/projects/coverage/builds/ - wait for the new tag build to finish successfully. - - visit https://readthedocs.org/dashboard/coverage/versions/ + - visit https://readthedocs.org/dashboard/coverage/advanced/ - change the default version to the new version - Update Tidelift: - make upload_relnotes From ad620d081c8508f94846fc01be331f107cb14050 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 30 Jul 2019 11:13:06 -0400 Subject: [PATCH 593/952] I have to remember to run pylint before checking things in. --- tests/test_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_context.py b/tests/test_context.py index 21d29a0c2..9c8d46057 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -288,5 +288,5 @@ def test_oldstyle(self): def test_bug_829(self): # A class with a name like a function shouldn't confuse qualname_from_frame. - class test_something(object): + class test_something(object): # pylint: disable=unused-variable self.assertEqual(get_qualname(), None) From e94f523162dd39acddfa17b20f4234f1ee5dec7f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 30 Jul 2019 11:06:36 -0400 Subject: [PATCH 594/952] Refactor numbits into their own files --- coverage/numbits.py | 42 ++++++++++++++++++++++++++++++++++++++++++ coverage/sqldata.py | 40 +++++++--------------------------------- tests/test_data.py | 30 ------------------------------ tests/test_numbits.py | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 63 deletions(-) create mode 100644 coverage/numbits.py create mode 100644 tests/test_numbits.py diff --git a/coverage/numbits.py b/coverage/numbits.py new file mode 100644 index 000000000..bc29ed94c --- /dev/null +++ b/coverage/numbits.py @@ -0,0 +1,42 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +""" +Functions to manipulate packed binary representations of number sets. + +To save space, coverage stores sets of line numbers in SQLite using a packed +binary representation called a numbits. A numbits is stored as a blob in the +database. The exact meaning of the bytes in the blobs should be considered an +implementation detail that might change in the future. Use these functions to +work with those binary blobs of data. + +""" + +from coverage.backward import bytes_to_ints, binary_bytes, zip_longest +from coverage.misc import contract + + +@contract(nums='Iterable', returns='bytes') +def nums_to_numbits(nums): + """Convert `nums` (an iterable of ints) into a numbits.""" + nbytes = max(nums) // 8 + 1 + b = bytearray(nbytes) + for num in nums: + b[num//8] |= 1 << num % 8 + return bytes(b) + +@contract(numbits='bytes', returns='list[int]') +def numbits_to_nums(numbits): + """Convert a numbits into a list of numbers.""" + nums = [] + for byte_i, byte in enumerate(bytes_to_ints(numbits)): + for bit_i in range(8): + if (byte & (1 << bit_i)): + nums.append(byte_i * 8 + bit_i) + return nums + +@contract(numbits1='bytes', numbits2='bytes', returns='bytes') +def merge_numbits(numbits1, numbits2): + """Merge two numbits""" + byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0) + return binary_bytes(b1 | b2 for b1, b2 in byte_pairs) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 0404004dc..1856ee321 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -17,13 +17,13 @@ import sys import zlib -from coverage.backward import get_thread_id, iitems -from coverage.backward import bytes_to_ints, binary_bytes, zip_longest, to_bytes, to_string +from coverage.backward import get_thread_id, iitems, to_bytes, to_string from coverage.debug import NoDebugging, SimpleReprMixin from coverage import env from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone, filename_suffix, isolate_module from coverage.misc import contract +from coverage.numbits import nums_to_numbits, numbits_to_nums, merge_numbits os = isolate_module(os) @@ -355,12 +355,12 @@ def add_lines(self, line_data): with self._connect() as con: self._set_context_id() for filename, linenos in iitems(line_data): - linemap = nums_to_bitmap(linenos) + linemap = nums_to_numbits(linenos) file_id = self._file_id(filename, add=True) query = "select bitmap from line_map where file_id = ? and context_id = ?" existing = list(con.execute(query, (file_id, self._current_context_id))) if existing: - linemap = merge_bitmaps(linemap, from_blob(existing[0][0])) + linemap = merge_numbits(linemap, from_blob(existing[0][0])) con.execute( "insert or replace into line_map (file_id, context_id, bitmap) values (?, ?, ?)", @@ -583,7 +583,7 @@ def update(self, other_data, aliases=None): key = (aliases.map(path), context) bitmap = from_blob(bitmap) if key in lines: - bitmap = merge_bitmaps(lines[key], bitmap) + bitmap = merge_numbits(lines[key], bitmap) lines[key] = bitmap cur.close() @@ -727,7 +727,7 @@ def lines(self, filename, contexts=None): bitmaps = list(con.execute(query, data)) nums = set() for row in bitmaps: - nums.update(bitmap_to_nums(from_blob(row[0]))) + nums.update(numbits_to_nums(from_blob(row[0]))) return sorted(nums) def arcs(self, filename, contexts=None): @@ -784,7 +784,7 @@ def contexts_by_lineno(self, filename): query += " and l.context_id in (" + ids_array + ")" data += context_ids for bitmap, context in con.execute(query, data): - for lineno in bitmap_to_nums(from_blob(bitmap)): + for lineno in numbits_to_nums(from_blob(bitmap)): lineno_contexts_map[lineno].append(context) return lineno_contexts_map @@ -868,29 +868,3 @@ def executescript(self, script): def dump(self): """Return a multi-line string, the dump of the database.""" return "\n".join(self.con.iterdump()) - - -@contract(nums='Iterable', returns='bytes') -def nums_to_bitmap(nums): - """Convert `nums` (an iterable of ints) into a bitmap.""" - nbytes = max(nums) // 8 + 1 - b = bytearray(nbytes) - for num in nums: - b[num//8] |= 1 << num % 8 - return bytes(b) - -@contract(bitmap='bytes', returns='list[int]') -def bitmap_to_nums(bitmap): - """Convert a bitmap into a list of numbers.""" - nums = [] - for byte_i, byte in enumerate(bytes_to_ints(bitmap)): - for bit_i in range(8): - if (byte & (1 << bit_i)): - nums.append(byte_i * 8 + bit_i) - return nums - -@contract(map1='bytes', map2='bytes', returns='bytes') -def merge_bitmaps(map1, map2): - """Merge two bitmaps""" - byte_pairs = zip_longest(bytes_to_ints(map1), bytes_to_ints(map2), fillvalue=0) - return binary_bytes(b1 | b2 for b1, b2 in byte_pairs) diff --git a/tests/test_data.py b/tests/test_data.py index ad752d653..ff97b3309 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -6,7 +6,6 @@ import glob import os import os.path -import random import re import sqlite3 import threading @@ -18,7 +17,6 @@ from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException -from coverage.sqldata import nums_to_bitmap, bitmap_to_nums, merge_bitmaps from tests.coveragetest import CoverageTest @@ -835,31 +833,3 @@ def test_misfed_serialization(self): ) with self.assertRaisesRegex(CoverageException, msg): covdata.loads(bad_data) - - -class BitmapOpTest(CoverageTest): - """Tests of the bitmap operations in sqldata.py.""" - - run_in_temp_dir = False - - def numbers(self, r): - """Produce a list of numbers from a Random object.""" - return list(set(r.randint(1, 1000) for _ in range(r.randint(100, 200)))) - - def test_conversion(self): - r = random.Random(1792) - for _ in range(10): - nums = self.numbers(r) - bitmap = nums_to_bitmap(nums) - self.assertEqual(sorted(bitmap_to_nums(bitmap)), sorted(nums)) - - def test_merging(self): - r = random.Random(314159) - for _ in range(10): - nums1 = self.numbers(r) - nums2 = self.numbers(r) - merged = bitmap_to_nums(merge_bitmaps(nums_to_bitmap(nums1), nums_to_bitmap(nums2))) - all_nums = set() - all_nums.update(nums1) - all_nums.update(nums2) - self.assertEqual(sorted(all_nums), sorted(merged)) diff --git a/tests/test_numbits.py b/tests/test_numbits.py new file mode 100644 index 000000000..b54f65dfb --- /dev/null +++ b/tests/test_numbits.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Tests for coverage.numbits""" + +import random + +from coverage.numbits import nums_to_numbits, numbits_to_nums, merge_numbits + +from tests.coveragetest import CoverageTest + +class NumbitsOpTest(CoverageTest): + """Tests of the numbits operations in numbits.py.""" + + run_in_temp_dir = False + + def numbers(self, r): + """Produce a list of numbers from a Random object.""" + return list(set(r.randint(1, 1000) for _ in range(r.randint(100, 200)))) + + def test_conversion(self): + r = random.Random(1792) + for _ in range(10): + nums = self.numbers(r) + numbits = nums_to_numbits(nums) + self.assertEqual(sorted(numbits_to_nums(numbits)), sorted(nums)) + + def test_merging(self): + r = random.Random(314159) + for _ in range(10): + nums1 = self.numbers(r) + nums2 = self.numbers(r) + merged = numbits_to_nums(merge_numbits(nums_to_numbits(nums1), nums_to_numbits(nums2))) + all_nums = set() + all_nums.update(nums1) + all_nums.update(nums2) + self.assertEqual(sorted(all_nums), sorted(merged)) From 13ceea280e0f0b77a655dc7d344925c0ccf84b41 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 30 Jul 2019 12:05:19 -0400 Subject: [PATCH 595/952] Use Hypothesis instead of my own janky randomization --- Makefile | 2 +- requirements/pytest.pip | 1 + tests/test_numbits.py | 36 ++++++++++++++---------------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 6f3edb9bc..6dc0cce01 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ clean: -rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz -rm -rf doc/_build doc/_spell doc/sample_html_beta -rm -rf .tox_kits - -rm -rf .cache .pytest_cache + -rm -rf .cache .pytest_cache .hypothesis -rm -rf $$TMPDIR/coverage_test sterile: clean diff --git a/requirements/pytest.pip b/requirements/pytest.pip index b5a4276c9..bd3255b15 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -9,6 +9,7 @@ pytest-xdist==1.28.0 flaky==3.5.3 mock==3.0.5 PyContracts==1.8.12 +hypothesis==4.32.1 # Our testing mixins unittest-mixins==1.6 diff --git a/tests/test_numbits.py b/tests/test_numbits.py index b54f65dfb..2b8e2e4bf 100644 --- a/tests/test_numbits.py +++ b/tests/test_numbits.py @@ -3,35 +3,27 @@ """Tests for coverage.numbits""" -import random +from hypothesis import given +from hypothesis.strategies import sets, integers from coverage.numbits import nums_to_numbits, numbits_to_nums, merge_numbits from tests.coveragetest import CoverageTest +# Hypothesis-generated line number data +line_numbers = sets(integers(min_value=1, max_value=9999), min_size=1) + class NumbitsOpTest(CoverageTest): """Tests of the numbits operations in numbits.py.""" run_in_temp_dir = False - def numbers(self, r): - """Produce a list of numbers from a Random object.""" - return list(set(r.randint(1, 1000) for _ in range(r.randint(100, 200)))) - - def test_conversion(self): - r = random.Random(1792) - for _ in range(10): - nums = self.numbers(r) - numbits = nums_to_numbits(nums) - self.assertEqual(sorted(numbits_to_nums(numbits)), sorted(nums)) - - def test_merging(self): - r = random.Random(314159) - for _ in range(10): - nums1 = self.numbers(r) - nums2 = self.numbers(r) - merged = numbits_to_nums(merge_numbits(nums_to_numbits(nums1), nums_to_numbits(nums2))) - all_nums = set() - all_nums.update(nums1) - all_nums.update(nums2) - self.assertEqual(sorted(all_nums), sorted(merged)) + @given(line_numbers) + def test_conversion(self, nums): + nums2 = numbits_to_nums(nums_to_numbits(nums)) + self.assertEqual(nums, set(nums2)) + + @given(line_numbers, line_numbers) + def test_merging(self, nums1, nums2): + merged = numbits_to_nums(merge_numbits(nums_to_numbits(nums1), nums_to_numbits(nums2))) + self.assertEqual(nums1 | nums2, set(merged)) From 41cddfc4a8399292a6f1ebee10ce7f9ade2193f6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 30 Jul 2019 15:24:43 -0400 Subject: [PATCH 596/952] numbits is the new name for the binary line numbers --- coverage/sqldata.py | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 1856ee321..a37c9ae5c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -27,7 +27,7 @@ os = isolate_module(os) -SCHEMA_VERSION = 3 +SCHEMA_VERSION = 4 SCHEMA = """ CREATE TABLE coverage_schema ( @@ -37,6 +37,7 @@ -- 1: Released in 5.0a2 -- 2: Added contexts in 5.0a3. -- 3: Replaced line table with line_map table. + -- 4: Changed line_map.bitmap to line_map.numbits. ); CREATE TABLE meta ( @@ -64,7 +65,7 @@ -- If recording lines, a row per context per line executed. file_id integer, -- foreign key to `file`. context_id integer, -- foreign key to `context`. - bitmap blob, -- Nth bit represents line N. + numbits blob, -- see the numbits functions in coverage.numbits unique(file_id, context_id) ); @@ -278,7 +279,9 @@ def loads(self, data): if self._debug.should('dataio'): self._debug.write("Loading data into data file {!r}".format(self._filename)) if data[:1] != b'z': - raise CoverageException("Unrecognized serialization: {!r} (head of {} bytes)".format(data[:40], len(data))) + raise CoverageException( + "Unrecognized serialization: {!r} (head of {} bytes)".format(data[:40], len(data)) + ) script = to_string(zlib.decompress(data[1:])) self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) with db: @@ -357,13 +360,14 @@ def add_lines(self, line_data): for filename, linenos in iitems(line_data): linemap = nums_to_numbits(linenos) file_id = self._file_id(filename, add=True) - query = "select bitmap from line_map where file_id = ? and context_id = ?" + query = "select numbits from line_map where file_id = ? and context_id = ?" existing = list(con.execute(query, (file_id, self._current_context_id))) if existing: linemap = merge_numbits(linemap, from_blob(existing[0][0])) con.execute( - "insert or replace into line_map (file_id, context_id, bitmap) values (?, ?, ?)", + "insert or replace into line_map " + " (file_id, context_id, numbits) values (?, ?, ?)", (file_id, self._current_context_id, to_blob(linemap)), ) @@ -497,12 +501,14 @@ def update(self, other_data, aliases=None): # Get line data. cur = conn.execute( - 'select file.path, context.context, line_map.bitmap ' + 'select file.path, context.context, line_map.numbits ' 'from line_map ' 'inner join file on file.id = line_map.file_id ' 'inner join context on context.id = line_map.context_id' ) - lines = {(files[path], context): from_blob(bitmap) for (path, context, bitmap) in cur} + lines = { + (files[path], context): from_blob(numbits) for (path, context, numbits) in cur + } cur.close() # Get tracer data. @@ -574,17 +580,17 @@ def update(self, other_data, aliases=None): # Get line data. cur = conn.execute( - 'select file.path, context.context, line_map.bitmap ' + 'select file.path, context.context, line_map.numbits ' 'from line_map ' 'inner join file on file.id = line_map.file_id ' 'inner join context on context.id = line_map.context_id' ) - for path, context, bitmap in cur: + for path, context, numbits in cur: key = (aliases.map(path), context) - bitmap = from_blob(bitmap) + numbits = from_blob(numbits) if key in lines: - bitmap = merge_numbits(lines[key], bitmap) - lines[key] = bitmap + numbits = merge_numbits(lines[key], numbits) + lines[key] = numbits cur.close() self._choose_lines_or_arcs(arcs=bool(arcs), lines=bool(lines)) @@ -598,8 +604,11 @@ def update(self, other_data, aliases=None): conn.execute("delete from line_map") conn.executemany( "insert into line_map " - "(file_id, context_id, bitmap) values (?, ?, ?)", - [(file_ids[file], context_ids[context], to_blob(bitmap)) for (file, context), bitmap in lines.items()] + "(file_id, context_id, numbits) values (?, ?, ?)", + [ + (file_ids[file], context_ids[context], to_blob(numbits)) + for (file, context), numbits in lines.items() + ] ) conn.executemany( 'insert or ignore into tracer (file_id, tracer) values (?, ?)', @@ -717,7 +726,7 @@ def lines(self, filename, contexts=None): if file_id is None: return None else: - query = "select bitmap from line_map where file_id = ?" + query = "select numbits from line_map where file_id = ?" data = [file_id] context_ids = self._get_query_context_ids(contexts) if context_ids is not None: @@ -773,7 +782,7 @@ def contexts_by_lineno(self, filename): lineno_contexts_map[tono].append(context) else: query = ( - "select l.bitmap, c.context from line_map l, context c " + "select l.numbits, c.context from line_map l, context c " "where l.context_id = c.id " "and file_id = ?" ) @@ -783,8 +792,8 @@ def contexts_by_lineno(self, filename): ids_array = ', '.join('?'*len(context_ids)) query += " and l.context_id in (" + ids_array + ")" data += context_ids - for bitmap, context in con.execute(query, data): - for lineno in numbits_to_nums(from_blob(bitmap)): + for numbits, context in con.execute(query, data): + for lineno in numbits_to_nums(from_blob(numbits)): lineno_contexts_map[lineno].append(context) return lineno_contexts_map From cb4080d565ddb1232674affb051944522ca37aee Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 30 Jul 2019 15:35:00 -0400 Subject: [PATCH 597/952] A little more discipline for blob converters --- coverage/sqldata.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index a37c9ae5c..4a00414db 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -86,14 +86,20 @@ """ if env.PY2: - def to_blob(bytes): - return buffer(bytes) + def to_blob(b): + """Convert a bytestring into a type SQLite will accept for a blob.""" + return buffer(b) # pylint: disable=undefined-variable + def from_blob(blob): + """Convert a blob read from SQLite into a bytestring.""" return bytes(blob) else: - def to_blob(bytes): - return bytes + def to_blob(b): + """Convert a bytestring into a type SQLite will accept for a blob.""" + return b + def from_blob(blob): + """Convert a blob read from SQLite into a bytestring.""" return blob @@ -507,7 +513,8 @@ def update(self, other_data, aliases=None): 'inner join context on context.id = line_map.context_id' ) lines = { - (files[path], context): from_blob(numbits) for (path, context, numbits) in cur + (files[path], context): from_blob(numbits) + for (path, context, numbits) in cur } cur.close() From 66fb5e7bda44ac9f7b2d86687c96a34a0d632010 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 31 Jul 2019 10:18:43 -0400 Subject: [PATCH 598/952] Add numbits_any_intersection --- coverage/numbits.py | 6 ++++++ tests/test_numbits.py | 10 +++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/coverage/numbits.py b/coverage/numbits.py index bc29ed94c..19c328c4c 100644 --- a/coverage/numbits.py +++ b/coverage/numbits.py @@ -40,3 +40,9 @@ def merge_numbits(numbits1, numbits2): """Merge two numbits""" byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0) return binary_bytes(b1 | b2 for b1, b2 in byte_pairs) + +@contract(numbits1='bytes', numbits2='bytes', returns='bool') +def numbits_any_intersection(numbits1, numbits2): + """Is there any number that appears in both numbits?""" + byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0) + return any(b1 & b2 for b1, b2 in byte_pairs) diff --git a/tests/test_numbits.py b/tests/test_numbits.py index 2b8e2e4bf..ff574d406 100644 --- a/tests/test_numbits.py +++ b/tests/test_numbits.py @@ -6,7 +6,9 @@ from hypothesis import given from hypothesis.strategies import sets, integers -from coverage.numbits import nums_to_numbits, numbits_to_nums, merge_numbits +from coverage.numbits import ( + nums_to_numbits, numbits_to_nums, merge_numbits, numbits_any_intersection, + ) from tests.coveragetest import CoverageTest @@ -27,3 +29,9 @@ def test_conversion(self, nums): def test_merging(self, nums1, nums2): merged = numbits_to_nums(merge_numbits(nums_to_numbits(nums1), nums_to_numbits(nums2))) self.assertEqual(nums1 | nums2, set(merged)) + + @given(line_numbers, line_numbers) + def test_any_intersection(self, nums1, nums2): + inter = numbits_any_intersection(nums_to_numbits(nums1), nums_to_numbits(nums2)) + expect = bool(nums1 & nums2) + self.assertEqual(expect, bool(inter)) From 79111b14cd70bbfd75555bb819c6dbdc25a543aa Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 1 Aug 2019 09:15:25 -0400 Subject: [PATCH 599/952] Keep hypothesis from freaking out about variable timings during metacov --- tests/test_numbits.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_numbits.py b/tests/test_numbits.py index ff574d406..4a835c4f4 100644 --- a/tests/test_numbits.py +++ b/tests/test_numbits.py @@ -3,9 +3,10 @@ """Tests for coverage.numbits""" -from hypothesis import given +from hypothesis import given, settings from hypothesis.strategies import sets, integers +from coverage import env from coverage.numbits import ( nums_to_numbits, numbits_to_nums, merge_numbits, numbits_any_intersection, ) @@ -15,22 +16,33 @@ # Hypothesis-generated line number data line_numbers = sets(integers(min_value=1, max_value=9999), min_size=1) +# When coverage-testing ourselves, hypothesis complains about a test being +# flaky because the first run exceeds the deadline (and fails), and the second +# run succeeds. Disable the deadline if we are coverage-testing. +default_settings = settings() +if env.METACOV: + default_settings = settings(default_settings, deadline=None) + + class NumbitsOpTest(CoverageTest): """Tests of the numbits operations in numbits.py.""" run_in_temp_dir = False @given(line_numbers) + @settings(default_settings) def test_conversion(self, nums): nums2 = numbits_to_nums(nums_to_numbits(nums)) self.assertEqual(nums, set(nums2)) @given(line_numbers, line_numbers) + @settings(default_settings) def test_merging(self, nums1, nums2): merged = numbits_to_nums(merge_numbits(nums_to_numbits(nums1), nums_to_numbits(nums2))) self.assertEqual(nums1 | nums2, set(merged)) @given(line_numbers, line_numbers) + @settings(default_settings) def test_any_intersection(self, nums1, nums2): inter = numbits_any_intersection(nums_to_numbits(nums1), nums_to_numbits(nums2)) expect = bool(nums1 & nums2) From a361ff3897736917544da1a4e28d8db6457f0fcc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 1 Aug 2019 09:42:54 -0400 Subject: [PATCH 600/952] Add num_in_numbits --- coverage/backward.py | 8 ++++++++ coverage/numbits.py | 12 ++++++++++-- tests/test_numbits.py | 11 ++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/coverage/backward.py b/coverage/backward.py index 0df2a41eb..34ab2f1aa 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -114,6 +114,10 @@ def binary_bytes(byte_values): """Produce a byte string with the ints from `byte_values`.""" return bytes(byte_values) + def byte_to_int(byte): + """Turn a byte indexed from a bytes object into an int.""" + return byte + def bytes_to_ints(bytes_value): """Turn a bytes object into a sequence of ints.""" # In Python 3, iterating bytes gives ints. @@ -132,6 +136,10 @@ def binary_bytes(byte_values): """Produce a byte string with the ints from `byte_values`.""" return "".join(chr(b) for b in byte_values) + def byte_to_int(byte): + """Turn a byte indexed from a bytes object into an int.""" + return ord(byte) + def bytes_to_ints(bytes_value): """Turn a bytes object into a sequence of ints.""" for byte in bytes_value: diff --git a/coverage/numbits.py b/coverage/numbits.py index 19c328c4c..c044073c0 100644 --- a/coverage/numbits.py +++ b/coverage/numbits.py @@ -12,13 +12,13 @@ """ -from coverage.backward import bytes_to_ints, binary_bytes, zip_longest +from coverage.backward import byte_to_int, bytes_to_ints, binary_bytes, zip_longest from coverage.misc import contract @contract(nums='Iterable', returns='bytes') def nums_to_numbits(nums): - """Convert `nums` (an iterable of ints) into a numbits.""" + """Convert `nums` (a non-empty iterable of ints) into a numbits.""" nbytes = max(nums) // 8 + 1 b = bytearray(nbytes) for num in nums: @@ -46,3 +46,11 @@ def numbits_any_intersection(numbits1, numbits2): """Is there any number that appears in both numbits?""" byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0) return any(b1 & b2 for b1, b2 in byte_pairs) + +@contract(num='int', numbits='bytes', returns='bool') +def num_in_numbits(num, numbits): + """Does the integer `num` appear in `numbits`?""" + nbyte, nbit = divmod(num, 8) + if nbyte > len(numbits): + return False + return bool(byte_to_int(numbits[nbyte]) & (1 << nbit)) diff --git a/tests/test_numbits.py b/tests/test_numbits.py index 4a835c4f4..f4e529977 100644 --- a/tests/test_numbits.py +++ b/tests/test_numbits.py @@ -9,12 +9,14 @@ from coverage import env from coverage.numbits import ( nums_to_numbits, numbits_to_nums, merge_numbits, numbits_any_intersection, + num_in_numbits, ) from tests.coveragetest import CoverageTest # Hypothesis-generated line number data -line_numbers = sets(integers(min_value=1, max_value=9999), min_size=1) +line_number = integers(min_value=1, max_value=9999) +line_numbers = sets(line_number, min_size=1) # When coverage-testing ourselves, hypothesis complains about a test being # flaky because the first run exceeds the deadline (and fails), and the second @@ -47,3 +49,10 @@ def test_any_intersection(self, nums1, nums2): inter = numbits_any_intersection(nums_to_numbits(nums1), nums_to_numbits(nums2)) expect = bool(nums1 & nums2) self.assertEqual(expect, bool(inter)) + + @given(line_number, line_numbers) + @settings(default_settings) + def test_num_in_numbits(self, num, nums): + numbits = nums_to_numbits(nums) + is_in = num_in_numbits(num, numbits) + self.assertEqual(num in nums, is_in) From 666d5ca196f59bcdc85fc8ab4193200e8c61fb4a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 1 Aug 2019 09:45:22 -0400 Subject: [PATCH 601/952] Use more conventional strategy naming --- tests/test_numbits.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_numbits.py b/tests/test_numbits.py index f4e529977..27b79d02b 100644 --- a/tests/test_numbits.py +++ b/tests/test_numbits.py @@ -15,8 +15,8 @@ from tests.coveragetest import CoverageTest # Hypothesis-generated line number data -line_number = integers(min_value=1, max_value=9999) -line_numbers = sets(line_number, min_size=1) +line_numbers = integers(min_value=1, max_value=9999) +line_number_sets = sets(line_numbers, min_size=1) # When coverage-testing ourselves, hypothesis complains about a test being # flaky because the first run exceeds the deadline (and fails), and the second @@ -31,26 +31,26 @@ class NumbitsOpTest(CoverageTest): run_in_temp_dir = False - @given(line_numbers) + @given(line_number_sets) @settings(default_settings) def test_conversion(self, nums): nums2 = numbits_to_nums(nums_to_numbits(nums)) self.assertEqual(nums, set(nums2)) - @given(line_numbers, line_numbers) + @given(line_number_sets, line_number_sets) @settings(default_settings) def test_merging(self, nums1, nums2): merged = numbits_to_nums(merge_numbits(nums_to_numbits(nums1), nums_to_numbits(nums2))) self.assertEqual(nums1 | nums2, set(merged)) - @given(line_numbers, line_numbers) + @given(line_number_sets, line_number_sets) @settings(default_settings) def test_any_intersection(self, nums1, nums2): inter = numbits_any_intersection(nums_to_numbits(nums1), nums_to_numbits(nums2)) expect = bool(nums1 & nums2) self.assertEqual(expect, bool(inter)) - @given(line_number, line_numbers) + @given(line_numbers, line_number_sets) @settings(default_settings) def test_num_in_numbits(self, num, nums): numbits = nums_to_numbits(nums) From 2d7ad503f60f982d6431870663238a76e5a28b56 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 1 Aug 2019 10:56:21 -0400 Subject: [PATCH 602/952] Fix a bug in num_in_numbits. --- coverage/numbits.py | 2 +- tests/test_numbits.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/coverage/numbits.py b/coverage/numbits.py index c044073c0..abe40f41a 100644 --- a/coverage/numbits.py +++ b/coverage/numbits.py @@ -51,6 +51,6 @@ def numbits_any_intersection(numbits1, numbits2): def num_in_numbits(num, numbits): """Does the integer `num` appear in `numbits`?""" nbyte, nbit = divmod(num, 8) - if nbyte > len(numbits): + if nbyte >= len(numbits): return False return bool(byte_to_int(numbits[nbyte]) & (1 << nbit)) diff --git a/tests/test_numbits.py b/tests/test_numbits.py index 27b79d02b..a556869be 100644 --- a/tests/test_numbits.py +++ b/tests/test_numbits.py @@ -3,7 +3,7 @@ """Tests for coverage.numbits""" -from hypothesis import given, settings +from hypothesis import example, given, settings from hypothesis.strategies import sets, integers from coverage import env @@ -52,6 +52,7 @@ def test_any_intersection(self, nums1, nums2): @given(line_numbers, line_number_sets) @settings(default_settings) + @example(152, {144}) def test_num_in_numbits(self, num, nums): numbits = nums_to_numbits(nums) is_in = num_in_numbits(num, numbits) From 57a637f22b14eb50dfda53093f6674edc7c7f4f6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 6 Aug 2019 07:00:35 -0400 Subject: [PATCH 603/952] Foreign keys --- coverage/sqldata.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 4a00414db..7e3a0390d 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -27,7 +27,7 @@ os = isolate_module(os) -SCHEMA_VERSION = 4 +SCHEMA_VERSION = 5 SCHEMA = """ CREATE TABLE coverage_schema ( @@ -38,6 +38,7 @@ -- 2: Added contexts in 5.0a3. -- 3: Replaced line table with line_map table. -- 4: Changed line_map.bitmap to line_map.numbits. + -- 5: Added foreign key declarations. ); CREATE TABLE meta ( @@ -66,6 +67,8 @@ file_id integer, -- foreign key to `file`. context_id integer, -- foreign key to `context`. numbits blob, -- see the numbits functions in coverage.numbits + foreign key (file_id) references file (id), + foreign key (context_id) references context (id), unique(file_id, context_id) ); @@ -75,13 +78,16 @@ context_id integer, -- foreign key to `context`. fromno integer, -- line number jumped from. tono integer, -- line number jumped to. + foreign key (file_id) references file (id), + foreign key (context_id) references context (id), unique(file_id, context_id, fromno, tono) ); CREATE TABLE tracer ( -- A row per file indicating the tracer used for that file. file_id integer primary key, - tracer text + tracer text, + foreign key (file_id) references file (id) ); """ From 784e647e6c4615c3e2d3bf1756642e1c678529c6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 6 Aug 2019 07:38:54 -0400 Subject: [PATCH 604/952] Pudb is a useful thing to have available when needed --- coverage/debug.py | 2 +- requirements/pytest.pip | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/coverage/debug.py b/coverage/debug.py index c11c927ac..9de9886b7 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -335,7 +335,7 @@ def break_in_pudb(func): # pragma: debugging """A function decorator to stop in the debugger for each call.""" @functools.wraps(func) def _wrapper(*args, **kwargs): - import pudb # pylint: disable=import-error + import pudb sys.stdout = sys.__stdout__ pudb.set_trace() return func(*args, **kwargs) diff --git a/requirements/pytest.pip b/requirements/pytest.pip index bd3255b15..3c5823106 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -14,3 +14,6 @@ hypothesis==4.32.1 # Our testing mixins unittest-mixins==1.6 #-e/Users/ned/unittest_mixins + +# Just so I have a debugger if I want it +pudb==2019.1 From 772fef4cd64a898fac7a8dd60a6f97a70ece28fb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 6 Aug 2019 07:42:11 -0400 Subject: [PATCH 605/952] Meta data stored as key/value --- coverage/sqldata.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 7e3a0390d..9b08cd762 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -27,7 +27,7 @@ os = isolate_module(os) -SCHEMA_VERSION = 5 +SCHEMA_VERSION = 6 SCHEMA = """ CREATE TABLE coverage_schema ( @@ -39,27 +39,31 @@ -- 3: Replaced line table with line_map table. -- 4: Changed line_map.bitmap to line_map.numbits. -- 5: Added foreign key declarations. + -- 6: Key-value in meta. ); CREATE TABLE meta ( - -- One row, to record some metadata about the data - has_lines boolean, -- Is this data recording lines? - has_arcs boolean, -- .. or branches? - sys_argv text -- The coverage command line that recorded the data. + -- Key-value pairs, to record metadata about the data + key text, + value text, + unique (key) + -- Keys: + -- 'has_arcs' boolean -- Is this data recording branches? + -- 'sys_argv' text -- The coverage command line that recorded the data. ); CREATE TABLE file ( -- A row per file measured. id integer primary key, path text, - unique(path) + unique (path) ); CREATE TABLE context ( -- A row per context measured. id integer primary key, context text, - unique(context) + unique (context) ); CREATE TABLE line_map ( @@ -69,7 +73,7 @@ numbits blob, -- see the numbits functions in coverage.numbits foreign key (file_id) references file (id), foreign key (context_id) references context (id), - unique(file_id, context_id) + unique (file_id, context_id) ); CREATE TABLE arc ( @@ -80,7 +84,7 @@ tono integer, -- line number jumped to. foreign key (file_id) references file (id), foreign key (context_id) references context (id), - unique(file_id, context_id, fromno, tono) + unique (file_id, context_id, fromno, tono) ); CREATE TABLE tracer ( @@ -226,8 +230,8 @@ def _create_db(self): db.executescript(SCHEMA) db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) db.execute( - "insert into meta (has_lines, has_arcs, sys_argv) values (?, ?, ?)", - (self._has_lines, self._has_arcs, str(getattr(sys, 'argv', None))) + "insert into meta (key, value) values (?, ?)", + ('sys_argv', str(getattr(sys, 'argv', None))) ) def _open_db(self): @@ -254,8 +258,9 @@ def _read_db(self): ) ) - for row in db.execute("select has_lines, has_arcs from meta"): - self._has_lines, self._has_arcs = row + for row in db.execute("select value from meta where key = 'has_arcs'"): + self._has_arcs = bool(int(row[0])) + self._has_lines = not self._has_arcs for path, id in db.execute("select path, id from file"): self._file_map[path] = id @@ -419,7 +424,10 @@ def _choose_lines_or_arcs(self, lines=False, arcs=False): self._has_lines = lines self._has_arcs = arcs with self._connect() as con: - con.execute("update meta set has_lines = ?, has_arcs = ?", (lines, arcs)) + con.execute( + "insert into meta (key, value) values (?, ?)", + ('has_arcs', str(int(arcs))) + ) def add_file_tracers(self, file_tracers): """Add per-file plugin information. From ce247f659bd9f4cd0e3292c996dc2b9531fb3318 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 6 Aug 2019 07:49:17 -0400 Subject: [PATCH 606/952] Version and timestamp in the meta data --- coverage/sqldata.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 9b08cd762..5b3f87667 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -10,6 +10,7 @@ # TODO: run_info import collections +import datetime import glob import itertools import os @@ -24,6 +25,7 @@ from coverage.misc import CoverageException, file_be_gone, filename_suffix, isolate_module from coverage.misc import contract from coverage.numbits import nums_to_numbits, numbits_to_nums, merge_numbits +from coverage.version import __version__ os = isolate_module(os) @@ -50,6 +52,8 @@ -- Keys: -- 'has_arcs' boolean -- Is this data recording branches? -- 'sys_argv' text -- The coverage command line that recorded the data. + -- 'version' text -- The version of coverage.py that made the file. + -- 'when' text -- Datetime when the file was created. ); CREATE TABLE file ( @@ -229,9 +233,13 @@ def _create_db(self): with db: db.executescript(SCHEMA) db.execute("insert into coverage_schema (version) values (?)", (SCHEMA_VERSION,)) - db.execute( + db.executemany( "insert into meta (key, value) values (?, ?)", - ('sys_argv', str(getattr(sys, 'argv', None))) + [ + ('sys_argv', str(getattr(sys, 'argv', None))), + ('version', __version__), + ('when', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')), + ] ) def _open_db(self): From fa9e42064b8544d1e63e7be30f831e4197cc21b3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 6 Aug 2019 17:47:55 -0400 Subject: [PATCH 607/952] Don't need the schema history in the schema --- coverage/sqldata.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 5b3f87667..3ee34f0fb 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -31,17 +31,18 @@ SCHEMA_VERSION = 6 +# Schema versions: +# 1: Released in 5.0a2 +# 2: Added contexts in 5.0a3. +# 3: Replaced line table with line_map table. +# 4: Changed line_map.bitmap to line_map.numbits. +# 5: Added foreign key declarations. +# 6: Key-value in meta. + SCHEMA = """ CREATE TABLE coverage_schema ( - -- One row, to record the version of the schema store in this db. + -- One row, to record the version of the schema in this db. version integer - -- Schema versions: - -- 1: Released in 5.0a2 - -- 2: Added contexts in 5.0a3. - -- 3: Replaced line table with line_map table. - -- 4: Changed line_map.bitmap to line_map.numbits. - -- 5: Added foreign key declarations. - -- 6: Key-value in meta. ); CREATE TABLE meta ( From 6b3017454af6662dde107755f1ee92153e69a4d8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 7 Aug 2019 08:54:19 -0400 Subject: [PATCH 608/952] One more detail to keep in sync --- doc/index.rst | 2 +- howto.txt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/index.rst b/doc/index.rst index 5ffad66e2..f4d20a92d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -40,7 +40,7 @@ not. * IronPython 2.7.7, though only for running code, not reporting. **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 4.5.3, `described here`_. + apply.** The latest stable version is coverage.py 4.5.4, `described here`_. .. _described here: http://coverage.readthedocs.io/ diff --git a/howto.txt b/howto.txt index cead8d214..9b19b72bd 100644 --- a/howto.txt +++ b/howto.txt @@ -13,7 +13,8 @@ - "New in x.y:" - Python versions supported - Update docs - - Version, date and python versions in doc/index.rst + - Version, date and Python versions in doc/index.rst + - Version of latest stable release in doc/index.rst - Version and copyright date in doc/conf.py - Look for CHANGEME comments - Don't forget the man page: doc/python-coverage.1.txt From 1a27df4c768e5a183ddd0f890d139996ffc52778 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 12 Jul 2019 10:24:35 -0400 Subject: [PATCH 609/952] Fix unusual backslash token issue. #822 --- CHANGES.rst | 4 ++++ coverage/phystokens.py | 9 ++++----- tests/test_phystokens.py | 10 ++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 4bc6fad7e..d51cfe301 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,6 +23,10 @@ Unreleased - A class named "test_something" no longer confuses the `test_function` dynamic context setting. Fixes `issue 829`_. +- Fixed an unusual tokenizing issue with backslashes in comments. Fixes + `issue 822`_. + +.. _issue 822: https://github.com/nedbat/coveragepy/issues/822 .. _issue 829: https://github.com/nedbat/coveragepy/issues/829 diff --git a/coverage/phystokens.py b/coverage/phystokens.py index ccfe63b3c..b6866e7dd 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -27,7 +27,7 @@ def phys_tokens(toks): """ last_line = None last_lineno = -1 - last_ttype = None + last_ttext = None for ttype, ttext, (slineno, scol), (elineno, ecol), ltext in toks: if last_lineno != elineno: if last_line and last_line.endswith("\\\n"): @@ -47,9 +47,7 @@ def phys_tokens(toks): # so we need to figure out if the backslash is already in the # string token or not. inject_backslash = True - if last_ttype == tokenize.COMMENT: - # Comments like this \ - # should never result in a new token. + if last_ttext.endswith("\\"): inject_backslash = False elif ttype == token.STRING: if "\n" in ttext and ttext.split('\n', 1)[0][-1] == '\\': @@ -66,7 +64,8 @@ def phys_tokens(toks): last_line ) last_line = ltext - last_ttype = ttype + if ttype not in (tokenize.NEWLINE, tokenize.NL): + last_ttext = ttext yield ttype, ttext, (slineno, scol), (elineno, ecol), ltext last_lineno = elineno diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py index 1045225e4..48f8ebb74 100644 --- a/tests/test_phystokens.py +++ b/tests/test_phystokens.py @@ -43,6 +43,13 @@ def hello(): [('ws', ' '), ('nam', 'b'), ('op', '='), ('str', '"indented"')], ] +# https://github.com/nedbat/coveragepy/issues/822 +BUG_822 = u"""\ +print( "Message 1" ) +array = [ 1,2,3,4, # 4 numbers \\ + 5,6,7 ] # 3 numbers +print( "Message 2" ) +""" class PhysTokensTest(CoverageTest): """Tests for coverage.py's improved tokenizer.""" @@ -78,6 +85,9 @@ def test_tab_indentation(self): # Mixed tabs and spaces... self.assertEqual(list(source_token_lines(MIXED_WS)), MIXED_WS_TOKENS) + def test_bug_822(self): + self.check_tokenization(BUG_822) + def test_tokenize_real_file(self): # Check the tokenization of a real file (large, btw). real_file = os.path.join(TESTS_DIR, "test_coverage.py") From f2a778b3aa18de0bd43b1ba1a144d16faa53b8a2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Aug 2019 16:06:00 -0400 Subject: [PATCH 610/952] Improve doc tooling --- MANIFEST.in | 2 +- Makefile | 10 +++++----- doc/conf.py | 13 ++++++++++--- doc/requirements.pip | 4 ++-- requirements/dev.pip | 4 +++- requirements/pytest.pip | 1 - requirements/tox.pip | 2 +- tox.ini | 3 ++- 8 files changed, 24 insertions(+), 15 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 32cc18097..b30f88512 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -34,7 +34,7 @@ exclude ci/*.token recursive-include coverage/fullcoverage *.py recursive-include coverage/ctracer *.c *.h -recursive-include doc conf.py *.pip *.rst *.txt *.png +recursive-include doc *.py *.pip *.rst *.txt *.png recursive-include doc/_static *.* prune doc/_build diff --git a/Makefile b/Makefile index 6dc0cce01..6810fdeb3 100644 --- a/Makefile +++ b/Makefile @@ -114,21 +114,21 @@ uninstall: # Documentation -SPHINXBUILD = sphinx-build +SPHINXBUILD = .tox/doc/bin/sphinx-build SPHINXOPTS = -a -E doc WEBHOME = ~/web/stellated/ WEBSAMPLE = $(WEBHOME)/files/sample_coverage_html WEBSAMPLEBETA = $(WEBHOME)/files/sample_coverage_html_beta docreqs: - pip install -r doc/requirements.pip + tox -q -e doc --notest -dochtml: - PYTHONPATH=$(CURDIR) $(SPHINXBUILD) -b html $(SPHINXOPTS) doc/_build/html +dochtml: docreqs + $(SPHINXBUILD) -b html $(SPHINXOPTS) doc/_build/html @echo @echo "Build finished. The HTML pages are in doc/_build/html." -docspell: +docspell: docreqs $(SPHINXBUILD) -b spelling $(SPHINXOPTS) doc/_spell publish: diff --git a/doc/conf.py b/doc/conf.py index 28ec17f49..836f77c98 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -2,7 +2,8 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -# +"""Sphinx configuration.""" + # coverage.py documentation build configuration file, created by # sphinx-quickstart on Wed May 13 22:18:33 2009. # @@ -14,7 +15,7 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -33,6 +34,7 @@ 'sphinx.ext.todo', 'sphinx.ext.ifconfig', 'sphinxcontrib.spelling', + 'sphinx.ext.intersphinx', ] # Add any paths that contain templates here, relative to this directory. @@ -49,7 +51,7 @@ # General information about the project. project = u'Coverage.py' -copyright = u'2009\N{EN DASH}2019, Ned Batchelder.' # CHANGEME +copyright = u'2009\N{EN DASH}2019, Ned Batchelder.' # CHANGEME # pylint: disable=redefined-builtin # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -97,6 +99,10 @@ # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + } + # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with @@ -189,6 +195,7 @@ prerelease = bool(max(release).isalpha()) def setup(app): + """Configure Sphinx""" app.add_stylesheet('coverage.css') app.add_config_value('prerelease', False, 'env') print("** Prerelease = %r" % prerelease) diff --git a/doc/requirements.pip b/doc/requirements.pip index 7c9f90676..3f8647379 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -4,6 +4,6 @@ doc8==0.8.0 pyenchant==2.0.0 -sphinx==2.0.1 -sphinxcontrib-spelling==4.2.1 +sphinx==2.1.2 +sphinxcontrib-spelling==4.3.0 sphinx_rtd_theme==0.4.3 diff --git a/requirements/dev.pip b/requirements/dev.pip index 9b92d20a6..2010dcc07 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -4,9 +4,11 @@ # Requirements for doing local development work on coverage.py. # https://requires.io/github/nedbat/coveragepy/requirements/ -pip==19.1.1 +pip==19.2.1 virtualenv==16.5.0 +pluggy==0.12.0 + # PyPI requirements for running tests. -r tox.pip -r pytest.pip diff --git a/requirements/pytest.pip b/requirements/pytest.pip index 3c5823106..30799efaa 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -4,7 +4,6 @@ # The pytest specifics used by coverage.py pytest==4.6.2 -pluggy>=0.7 # pytest needs this, but pip doesn't understand pytest-xdist==1.28.0 flaky==3.5.3 mock==3.0.5 diff --git a/requirements/tox.pip b/requirements/tox.pip index e4ff4031d..6eb326121 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -2,6 +2,6 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # The version of tox used by coverage.py -tox==3.9.0 +tox==3.13.2 # Adds env recreation on requirements file changes. tox-battery==0.5.1 diff --git a/tox.ini b/tox.ini index 6f539a105..33e3309a0 100644 --- a/tox.ini +++ b/tox.ini @@ -61,9 +61,10 @@ commands = [testenv:lint] deps = -r requirements/dev.pip + -r doc/requirements.pip setenv = - LINTABLE = coverage tests igor.py setup.py __main__.py + LINTABLE = coverage tests doc igor.py setup.py __main__.py commands = python -m tabnanny {env:LINTABLE} From af9830596d53b43efea837a67efa2c683f69793f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Aug 2019 16:07:37 -0400 Subject: [PATCH 611/952] Utility to check .rst for accurate copies from source files. --- doc/check_copied_from.py | 113 +++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 2 files changed, 114 insertions(+) create mode 100644 doc/check_copied_from.py diff --git a/doc/check_copied_from.py b/doc/check_copied_from.py new file mode 100644 index 000000000..79ec005bc --- /dev/null +++ b/doc/check_copied_from.py @@ -0,0 +1,113 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Find lines in files that should be faithful copies, and check that they are. + +Inside a comment-marked section, any chunk of indented lines should be +faithfully copied from FILENAME. The indented lines are dedented before +comparing. + +The section is between these comments: + + .. copied_from + + .. end_copied_from + +This tool will print any mismatches, and then exit with a count of mismatches. + +""" + +import glob +from itertools import groupby +from operator import itemgetter +import re +import sys +import textwrap + + +def check_copied_from(rst_name): + """Check copies in a .rst file. + + Prints problems. Returns count of bad copies. + """ + bad_copies = 0 + file_read = None + file_text = None + with open(rst_name) as frst: + for filename, first_line, text in find_copied_chunks(frst): + if filename != file_read: + with open(filename) as f: + file_text = f.read() + file_read = filename + if text not in file_text: + print("{}:{}: Bad copy from {}, starting with {!r}".format( + rst_name, first_line, filename, text.splitlines()[0] + )) + bad_copies += 1 + + return bad_copies + + +def find_copied_chunks(frst): + """Find chunks of text that are meant to be faithful copies. + + `frst` is an iterable of strings, the .rst text. + + Yields (source_filename, first_line, text) tuples. + """ + for (_, filename), chunks in groupby(find_copied_lines(frst), itemgetter(0)): + chunks = list(chunks) + first_line = chunks[0][1] + text = textwrap.dedent("\n".join(map(itemgetter(2), chunks))) + yield filename, first_line, text + + +def find_copied_lines(frst): + """Find lines of text that are meant to be faithful copies. + + `frst` is an iterable of strings, the .rst text. + + Yields tuples ((chunk_num, file_name), line_num, line). + + `chunk_num` is an integer that is different for each distinct (blank + line separated) chunk of text, but has no meaning other than that. + + `file_name` is the file the chunk should be copied from. `line_num` + is the line number in the .rst file, and `line` is the text of the line. + + """ + in_section = False + source_file = None + chunk_num = 0 + + for line_num, line in enumerate(frst, start=1): + line = line.rstrip() + if in_section: + m = re.search(r"^.. end_copied_from", line) + if m: + in_section = False + else: + if re.search(r"^\s+\S", line): + # Indented line + yield (chunk_num, source_file), line_num, line + elif not line.strip(): + # Blank line + chunk_num += 1 + else: + m = re.search(r"^.. copied_from: (.*)", line) + if m: + in_section = True + source_file = m.group(1) + + +def main(args): + """Check all the files in `args`, return count of bad copies.""" + bad_copies = 0 + for arg in args: + for fname in glob.glob(arg): + bad_copies += check_copied_from(fname) + return bad_copies + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/tox.ini b/tox.ini index 33e3309a0..c9deb506c 100644 --- a/tox.ini +++ b/tox.ini @@ -52,6 +52,7 @@ commands = deps = -r doc/requirements.pip commands = + python doc/check_copied_from.py doc/*.rst doc8 -q --ignore-path doc/_build doc CHANGES.rst README.rst sphinx-build -b html -aqE doc doc/_build/html rst2html.py --strict README.rst doc/_build/trash From fedaa98daab82a2a8ce0a76fe5cb9dc1c1c92eac Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Aug 2019 16:13:57 -0400 Subject: [PATCH 612/952] Improved numbits operations * Better names (merge -> union) * More ops (intersection) * Can be registered for SQLite use * Numbits can be empty Also, line_map is a dumb table name. line_bits is marginally better. --- coverage/numbits.py | 144 ++++++++++++++++++++++++++++++++++++------ coverage/sqldata.py | 75 +++++++++------------- tests/test_numbits.py | 115 ++++++++++++++++++++++++++++++--- 3 files changed, 262 insertions(+), 72 deletions(-) diff --git a/coverage/numbits.py b/coverage/numbits.py index abe40f41a..4b340c8e0 100644 --- a/coverage/numbits.py +++ b/coverage/numbits.py @@ -5,29 +5,62 @@ Functions to manipulate packed binary representations of number sets. To save space, coverage stores sets of line numbers in SQLite using a packed -binary representation called a numbits. A numbits is stored as a blob in the -database. The exact meaning of the bytes in the blobs should be considered an -implementation detail that might change in the future. Use these functions to -work with those binary blobs of data. +binary representation called a numbits. A numbits is a set of positive +integers. + +A numbits is stored as a blob in the database. The exact meaning of the bytes +in the blobs should be considered an implementation detail that might change in +the future. Use these functions to work with those binary blobs of data. """ +from coverage import env from coverage.backward import byte_to_int, bytes_to_ints, binary_bytes, zip_longest -from coverage.misc import contract +from coverage.misc import contract, new_contract + +if env.PY3: + def _to_blob(b): + """Convert a bytestring into a type SQLite will accept for a blob.""" + return b + new_contract('blob', lambda v: isinstance(v, bytes)) +else: + def _to_blob(b): + """Convert a bytestring into a type SQLite will accept for a blob.""" + return buffer(b) # pylint: disable=undefined-variable -@contract(nums='Iterable', returns='bytes') + new_contract('blob', lambda v: isinstance(v, buffer)) # pylint: disable=undefined-variable + +@contract(nums='Iterable', returns='blob') def nums_to_numbits(nums): - """Convert `nums` (a non-empty iterable of ints) into a numbits.""" - nbytes = max(nums) // 8 + 1 + """Convert `nums` into a numbits. + + Arguments: + nums (a reusable iterable of integers): the line numbers to store. + + Returns: + A binary blob. + """ + try: + nbytes = max(nums) // 8 + 1 + except ValueError: + # nums was empty. + return _to_blob(b'') b = bytearray(nbytes) for num in nums: b[num//8] |= 1 << num % 8 - return bytes(b) + return _to_blob(bytes(b)) -@contract(numbits='bytes', returns='list[int]') +@contract(numbits='blob', returns='list[int]') def numbits_to_nums(numbits): - """Convert a numbits into a list of numbers.""" + """Convert a numbits into a list of numbers. + + Arguments: + numbits (a binary blob): the packed number set. + + Returns: + A list of integers. + """ nums = [] for byte_i, byte in enumerate(bytes_to_ints(numbits)): for bit_i in range(8): @@ -35,22 +68,95 @@ def numbits_to_nums(numbits): nums.append(byte_i * 8 + bit_i) return nums -@contract(numbits1='bytes', numbits2='bytes', returns='bytes') -def merge_numbits(numbits1, numbits2): - """Merge two numbits""" +@contract(numbits1='blob', numbits2='blob', returns='blob') +def numbits_union(numbits1, numbits2): + """Compute the union of two numbits. + + Arguments: + numbits1, numbits2: packed number sets. + + Returns: + A new numbits, the union of the two number sets. + """ + byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0) + return _to_blob(binary_bytes(b1 | b2 for b1, b2 in byte_pairs)) + +@contract(numbits1='blob', numbits2='blob', returns='blob') +def numbits_intersection(numbits1, numbits2): + """Compute the intersection of two numbits. + + Arguments: + numbits1, numbits2: packed number sets. + + Returns: + A new numbits, the intersection of the two number sets. + """ byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0) - return binary_bytes(b1 | b2 for b1, b2 in byte_pairs) + intersection_bytes = binary_bytes(b1 & b2 for b1, b2 in byte_pairs) + return _to_blob(intersection_bytes.rstrip(b'\0')) -@contract(numbits1='bytes', numbits2='bytes', returns='bool') +@contract(numbits1='blob', numbits2='blob', returns='bool') def numbits_any_intersection(numbits1, numbits2): - """Is there any number that appears in both numbits?""" + """Is there any number that appears in both numbits? + + Determine whether two number sets have a non-empty intersection. This is + faster than computing the intersection. + + Arguments: + numbits1, numbits2: packed number sets. + + Returns: + A boolean, true if there is any number in both of the number sets. + """ byte_pairs = zip_longest(bytes_to_ints(numbits1), bytes_to_ints(numbits2), fillvalue=0) return any(b1 & b2 for b1, b2 in byte_pairs) -@contract(num='int', numbits='bytes', returns='bool') +@contract(num='int', numbits='blob', returns='bool') def num_in_numbits(num, numbits): - """Does the integer `num` appear in `numbits`?""" + """Does the integer `num` appear in `numbits`? + + Arguments: + num (integer) + + numbits (binary blob) + + Returns: + A boolean, true if `num` is a member of `numbits`. + """ nbyte, nbit = divmod(num, 8) if nbyte >= len(numbits): return False return bool(byte_to_int(numbits[nbyte]) & (1 << nbit)) + +def register_sqlite_functions(connection): + """ + Define numbits functions in a SQLite connection. + + This defines these functions for use in SQLite statements: + + * :func:`numbits_union` + * :func:`numbits_intersection` + * :func:`numbits_any_intersection` + * :func:`num_in_numbits` + + `connection` is a :class:`sqlite3.Connection ` + object. After creating the connection, pass it to this function to + register the numbits functions. Then you can use numbits functions in your + queries:: + + import sqlite3 + from coverage.numbits import register_sqlite_functions + + conn = sqlite3.connect('example.db') + register_sqlite_functions(conn) + c = conn.cursor() + c.execute( + "select lb.file_id, lb.context_id from line_bits lb" + "where num_in_numbits(?, lb.numbits)", + (interesting_line_number,) + ) + """ + connection.create_function("numbits_union", 2, numbits_union) + connection.create_function("numbits_intersection", 2, numbits_intersection) + connection.create_function("numbits_any_intersection", 2, numbits_any_intersection) + connection.create_function("num_in_numbits", 2, num_in_numbits) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 3ee34f0fb..5e7edd728 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -20,16 +20,18 @@ from coverage.backward import get_thread_id, iitems, to_bytes, to_string from coverage.debug import NoDebugging, SimpleReprMixin -from coverage import env from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone, filename_suffix, isolate_module from coverage.misc import contract -from coverage.numbits import nums_to_numbits, numbits_to_nums, merge_numbits +from coverage.numbits import nums_to_numbits, numbits_to_nums, numbits_union from coverage.version import __version__ os = isolate_module(os) -SCHEMA_VERSION = 6 +# If you change the schema, increment the SCHEMA_VERSION, and update the +# docs in docs/dbschema.rst also. + +SCHEMA_VERSION = 7 # Schema versions: # 1: Released in 5.0a2 @@ -38,6 +40,7 @@ # 4: Changed line_map.bitmap to line_map.numbits. # 5: Added foreign key declarations. # 6: Key-value in meta. +# 7: line_map -> line_bits SCHEMA = """ CREATE TABLE coverage_schema ( @@ -71,8 +74,9 @@ unique (context) ); -CREATE TABLE line_map ( - -- If recording lines, a row per context per line executed. +CREATE TABLE line_bits ( + -- If recording lines, a row per context per file executed. + -- All of the line numbers for that file/context are in one numbits. file_id integer, -- foreign key to `file`. context_id integer, -- foreign key to `context`. numbits blob, -- see the numbits functions in coverage.numbits @@ -100,24 +104,6 @@ ); """ -if env.PY2: - def to_blob(b): - """Convert a bytestring into a type SQLite will accept for a blob.""" - return buffer(b) # pylint: disable=undefined-variable - - def from_blob(blob): - """Convert a blob read from SQLite into a bytestring.""" - return bytes(blob) -else: - def to_blob(b): - """Convert a bytestring into a type SQLite will accept for a blob.""" - return b - - def from_blob(blob): - """Convert a blob read from SQLite into a bytestring.""" - return blob - - class CoverageData(SimpleReprMixin): """Manages collected coverage data, including file storage. @@ -386,15 +372,15 @@ def add_lines(self, line_data): for filename, linenos in iitems(line_data): linemap = nums_to_numbits(linenos) file_id = self._file_id(filename, add=True) - query = "select numbits from line_map where file_id = ? and context_id = ?" + query = "select numbits from line_bits where file_id = ? and context_id = ?" existing = list(con.execute(query, (file_id, self._current_context_id))) if existing: - linemap = merge_numbits(linemap, from_blob(existing[0][0])) + linemap = numbits_union(linemap, existing[0][0]) con.execute( - "insert or replace into line_map " + "insert or replace into line_bits " " (file_id, context_id, numbits) values (?, ?, ?)", - (file_id, self._current_context_id, to_blob(linemap)), + (file_id, self._current_context_id, linemap), ) def add_arcs(self, arc_data): @@ -530,13 +516,13 @@ def update(self, other_data, aliases=None): # Get line data. cur = conn.execute( - 'select file.path, context.context, line_map.numbits ' - 'from line_map ' - 'inner join file on file.id = line_map.file_id ' - 'inner join context on context.id = line_map.context_id' + 'select file.path, context.context, line_bits.numbits ' + 'from line_bits ' + 'inner join file on file.id = line_bits.file_id ' + 'inner join context on context.id = line_bits.context_id' ) lines = { - (files[path], context): from_blob(numbits) + (files[path], context): numbits for (path, context, numbits) in cur } cur.close() @@ -610,16 +596,15 @@ def update(self, other_data, aliases=None): # Get line data. cur = conn.execute( - 'select file.path, context.context, line_map.numbits ' - 'from line_map ' - 'inner join file on file.id = line_map.file_id ' - 'inner join context on context.id = line_map.context_id' + 'select file.path, context.context, line_bits.numbits ' + 'from line_bits ' + 'inner join file on file.id = line_bits.file_id ' + 'inner join context on context.id = line_bits.context_id' ) for path, context, numbits in cur: key = (aliases.map(path), context) - numbits = from_blob(numbits) if key in lines: - numbits = merge_numbits(lines[key], numbits) + numbits = numbits_union(lines[key], numbits) lines[key] = numbits cur.close() @@ -631,12 +616,12 @@ def update(self, other_data, aliases=None): '(file_id, context_id, fromno, tono) values (?, ?, ?, ?)', arc_rows ) - conn.execute("delete from line_map") + conn.execute("delete from line_bits") conn.executemany( - "insert into line_map " + "insert into line_bits " "(file_id, context_id, numbits) values (?, ?, ?)", [ - (file_ids[file], context_ids[context], to_blob(numbits)) + (file_ids[file], context_ids[context], numbits) for (file, context), numbits in lines.items() ] ) @@ -756,7 +741,7 @@ def lines(self, filename, contexts=None): if file_id is None: return None else: - query = "select numbits from line_map where file_id = ?" + query = "select numbits from line_bits where file_id = ?" data = [file_id] context_ids = self._get_query_context_ids(contexts) if context_ids is not None: @@ -766,7 +751,7 @@ def lines(self, filename, contexts=None): bitmaps = list(con.execute(query, data)) nums = set() for row in bitmaps: - nums.update(numbits_to_nums(from_blob(row[0]))) + nums.update(numbits_to_nums(row[0])) return sorted(nums) def arcs(self, filename, contexts=None): @@ -812,7 +797,7 @@ def contexts_by_lineno(self, filename): lineno_contexts_map[tono].append(context) else: query = ( - "select l.numbits, c.context from line_map l, context c " + "select l.numbits, c.context from line_bits l, context c " "where l.context_id = c.id " "and file_id = ?" ) @@ -823,7 +808,7 @@ def contexts_by_lineno(self, filename): query += " and l.context_id in (" + ids_array + ")" data += context_ids for numbits, context in con.execute(query, data): - for lineno in numbits_to_nums(from_blob(numbits)): + for lineno in numbits_to_nums(numbits): lineno_contexts_map[lineno].append(context) return lineno_contexts_map diff --git a/tests/test_numbits.py b/tests/test_numbits.py index a556869be..eb094d2a0 100644 --- a/tests/test_numbits.py +++ b/tests/test_numbits.py @@ -3,20 +3,23 @@ """Tests for coverage.numbits""" +import sqlite3 + from hypothesis import example, given, settings from hypothesis.strategies import sets, integers from coverage import env +from coverage.backward import byte_to_int from coverage.numbits import ( - nums_to_numbits, numbits_to_nums, merge_numbits, numbits_any_intersection, - num_in_numbits, + nums_to_numbits, numbits_to_nums, numbits_union, numbits_intersection, + numbits_any_intersection, num_in_numbits, register_sqlite_functions, ) from tests.coveragetest import CoverageTest # Hypothesis-generated line number data line_numbers = integers(min_value=1, max_value=9999) -line_number_sets = sets(line_numbers, min_size=1) +line_number_sets = sets(line_numbers) # When coverage-testing ourselves, hypothesis complains about a test being # flaky because the first run exceeds the deadline (and fails), and the second @@ -26,6 +29,12 @@ default_settings = settings(default_settings, deadline=None) +def good_numbits(numbits): + """Assert that numbits is good.""" + # It shouldn't end with a zero byte, that should have been trimmed off. + assert (not numbits) or (byte_to_int(numbits[-1]) != 0) + + class NumbitsOpTest(CoverageTest): """Tests of the numbits operations in numbits.py.""" @@ -34,19 +43,43 @@ class NumbitsOpTest(CoverageTest): @given(line_number_sets) @settings(default_settings) def test_conversion(self, nums): - nums2 = numbits_to_nums(nums_to_numbits(nums)) + numbits = nums_to_numbits(nums) + good_numbits(numbits) + nums2 = numbits_to_nums(numbits) self.assertEqual(nums, set(nums2)) @given(line_number_sets, line_number_sets) @settings(default_settings) - def test_merging(self, nums1, nums2): - merged = numbits_to_nums(merge_numbits(nums_to_numbits(nums1), nums_to_numbits(nums2))) - self.assertEqual(nums1 | nums2, set(merged)) + def test_union(self, nums1, nums2): + nb1 = nums_to_numbits(nums1) + good_numbits(nb1) + nb2 = nums_to_numbits(nums2) + good_numbits(nb2) + nbu = numbits_union(nb1, nb2) + good_numbits(nbu) + union = numbits_to_nums(nbu) + self.assertEqual(nums1 | nums2, set(union)) + + @given(line_number_sets, line_number_sets) + @settings(default_settings) + def test_intersection(self, nums1, nums2): + nb1 = nums_to_numbits(nums1) + good_numbits(nb1) + nb2 = nums_to_numbits(nums2) + good_numbits(nb2) + nbi = numbits_intersection(nb1, nb2) + good_numbits(nbi) + intersection = numbits_to_nums(nbi) + self.assertEqual(nums1 & nums2, set(intersection)) @given(line_number_sets, line_number_sets) @settings(default_settings) def test_any_intersection(self, nums1, nums2): - inter = numbits_any_intersection(nums_to_numbits(nums1), nums_to_numbits(nums2)) + nb1 = nums_to_numbits(nums1) + good_numbits(nb1) + nb2 = nums_to_numbits(nums2) + good_numbits(nb2) + inter = numbits_any_intersection(nb1, nb2) expect = bool(nums1 & nums2) self.assertEqual(expect, bool(inter)) @@ -55,5 +88,71 @@ def test_any_intersection(self, nums1, nums2): @example(152, {144}) def test_num_in_numbits(self, num, nums): numbits = nums_to_numbits(nums) + good_numbits(numbits) is_in = num_in_numbits(num, numbits) self.assertEqual(num in nums, is_in) + + +class NumbitsSqliteFunctionTest(CoverageTest): + """Tests of the SQLite integration for numbits functions.""" + + run_in_temp_dir = False + + def setUp(self): + super(NumbitsSqliteFunctionTest, self).setUp() + conn = sqlite3.connect(":memory:") + register_sqlite_functions(conn) + self.cursor = conn.cursor() + self.cursor.execute("create table data (id int, numbits blob)") + self.cursor.executemany( + "insert into data (id, numbits) values (?, ?)", + [ + (i, nums_to_numbits(range(i, 100, i))) + for i in range(1, 11) + ] + ) + self.addCleanup(self.cursor.close) + + def test_numbits_union(self): + res = self.cursor.execute( + "select numbits_union(" + "(select numbits from data where id = 7)," + "(select numbits from data where id = 9)" + ")" + ) + answer = numbits_to_nums(list(res)[0][0]) + self.assertEqual( + [7, 9, 14, 18, 21, 27, 28, 35, 36, 42, 45, 49, + 54, 56, 63, 70, 72, 77, 81, 84, 90, 91, 98, 99], + answer + ) + + def test_numbits_intersection(self): + res = self.cursor.execute( + "select numbits_intersection(" + "(select numbits from data where id = 7)," + "(select numbits from data where id = 9)" + ")" + ) + answer = numbits_to_nums(list(res)[0][0]) + self.assertEqual([63], answer) + + def test_numbits_any_intersection(self): + res = self.cursor.execute( + "select numbits_any_intersection(?, ?)", + (nums_to_numbits([1, 2, 3]), nums_to_numbits([3, 4, 5])) + ) + answer = [any_inter for (any_inter,) in res] + self.assertEqual([1], answer) + + res = self.cursor.execute( + "select numbits_any_intersection(?, ?)", + (nums_to_numbits([1, 2, 3]), nums_to_numbits([7, 8, 9])) + ) + answer = [any_inter for (any_inter,) in res] + self.assertEqual([0], answer) + + def test_num_in_numbits(self): + res = self.cursor.execute("select id, num_in_numbits(12, numbits) from data order by id") + answer = [is_in for (id, is_in) in res] + self.assertEqual([1, 1, 1, 1, 0, 1, 0, 0, 0, 0], answer) From df1dc16fbd3d7b3e34768178d8b2e320ab584412 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Aug 2019 16:14:28 -0400 Subject: [PATCH 613/952] Document SQLite access to data. --- doc/api.rst | 19 +++++++-- doc/dbschema.rst | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 4 deletions(-) create mode 100644 doc/dbschema.rst diff --git a/doc/api.rst b/doc/api.rst index d485d90a6..eb63719ce 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -14,8 +14,9 @@ Coverage.py API .. :history: 20121111T235800, added a bit of clarification. .. :history: 20140819T132600, change class name to Coverage +There are a few different ways to use coverage.py programmatically. -The API to coverage.py is very simple, contained in a module called `coverage`. +The API to coverage.py is in a module called `coverage`. Most of the interface is in the :class:`coverage.Coverage` class. Methods on the Coverage object correspond roughly to operations available in the command line interface. For example, a simple use would be:: @@ -32,12 +33,22 @@ line interface. For example, a simple use would be:: cov.html_report() -The :class:`coverage.CoverageData` class provides access to coverage data -stored in coverage.py data files. +Coverage.py supports plugins that can change its behavior, to collect +information from non-Python files, or to perform complex configuration. See +:ref:`api_plugin` for details. + +If you want to access the data that coverage.py has collected, the +:class:`coverage.CoverageData` class provides an API to read coverage.py data +files. + +For more intensive data use, you might want to access the coverage.py database +file directly. The schema is subject to change, so this is for advanced uses +only. :ref:`dbschema` explains more. .. toctree:: :maxdepth: 1 api_coverage - api_coveragedata api_plugin + api_coveragedata + dbschema diff --git a/doc/dbschema.rst b/doc/dbschema.rst new file mode 100644 index 000000000..aa838fdf2 --- /dev/null +++ b/doc/dbschema.rst @@ -0,0 +1,105 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +.. _dbschema: + +=========================== +Coverage.py database schema +=========================== + +.. versionadded:: 5.0 + +.. module:: coverage + +Coverage.py stores data in a SQLite database, by default called ``.coverage``. +For most needs, the :class:`CoverageData` API will be sufficient, and should be +preferred to accessing the database directly. Only advanced uses will need to +use the database. + +You can use SQLite tools such as the :mod:`sqlite3 ` module in +the Python standard library to access the data. Some data is stored in a +packed format that will need custom functions to access. See +:func:`register_sqlite_functions`. + + +Database schema +--------------- + +This is the database schema. + +TODO: explain more. Readers: what needs explaining? + +.. copied_from: coverage/sqldata.py + +.. code:: + + CREATE TABLE coverage_schema ( + -- One row, to record the version of the schema in this db. + version integer + ); + + CREATE TABLE meta ( + -- Key-value pairs, to record metadata about the data + key text, + value text, + unique (key) + -- Keys: + -- 'has_arcs' boolean -- Is this data recording branches? + -- 'sys_argv' text -- The coverage command line that recorded the data. + -- 'version' text -- The version of coverage.py that made the file. + -- 'when' text -- Datetime when the file was created. + ); + + CREATE TABLE file ( + -- A row per file measured. + id integer primary key, + path text, + unique (path) + ); + + CREATE TABLE context ( + -- A row per context measured. + id integer primary key, + context text, + unique (context) + ); + + CREATE TABLE line_bits ( + -- If recording lines, a row per context per file executed. + -- All of the line numbers for that file/context are in one numbits. + file_id integer, -- foreign key to `file`. + context_id integer, -- foreign key to `context`. + numbits blob, -- see the numbits functions in coverage.numbits + foreign key (file_id) references file (id), + foreign key (context_id) references context (id), + unique (file_id, context_id) + ); + + CREATE TABLE arc ( + -- If recording branches, a row per context per from/to line transition executed. + file_id integer, -- foreign key to `file`. + context_id integer, -- foreign key to `context`. + fromno integer, -- line number jumped from. + tono integer, -- line number jumped to. + foreign key (file_id) references file (id), + foreign key (context_id) references context (id), + unique (file_id, context_id, fromno, tono) + ); + + CREATE TABLE tracer ( + -- A row per file indicating the tracer used for that file. + file_id integer primary key, + tracer text, + foreign key (file_id) references file (id) + ); + +.. end_copied_from + + +.. _numbits: + +Numbits +------- + +.. automodule:: coverage.numbits + :members: From f3810de6bff28fe6089d7ed27d14d2769f5e77d7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Aug 2019 16:20:30 -0400 Subject: [PATCH 614/952] Mention the new coverage.numbits module --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index d51cfe301..1e3279929 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,6 +20,10 @@ development at the same time, like 4.5.x and 5.0. Unreleased ---------- +- The compact line number representation introduced in 5.0a6 is called a + "numbits." The coverage.numbits module provides functions for working with + them. + - A class named "test_something" no longer confuses the `test_function` dynamic context setting. Fixes `issue 829`_. From 08a54d5d17a878241b6f7ac1e08f44aa55c782aa Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Aug 2019 17:06:25 -0400 Subject: [PATCH 615/952] Fix some sphinx mis-references --- CHANGES.rst | 18 +++++++++--------- doc/api_coveragedata.rst | 1 + doc/api_plugin.rst | 1 + doc/changes.rst | 2 -- doc/config.rst | 3 +-- doc/contexts.rst | 2 -- doc/dbschema.rst | 10 ++++------ 7 files changed, 16 insertions(+), 21 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1e3279929..f2c017f2d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,8 +21,8 @@ Unreleased ---------- - The compact line number representation introduced in 5.0a6 is called a - "numbits." The coverage.numbits module provides functions for working with - them. + "numbits." The :mod:`coverage.numbits` module provides functions for working + with them. - A class named "test_something" no longer confuses the `test_function` dynamic context setting. Fixes `issue 829`_. @@ -62,19 +62,19 @@ Version 5.0a6 --- 2019-07-16 - SQLite data storage is now faster. There's no longer a reason to keep the JSON data file code, so it has been removed. -- Changes to the :class:`CoverageData` interface: +- Changes to the :class:`.CoverageData` interface: - - The new :meth:`CoverageData.dumps` method serializes the data to a string, - and a corresponding :meth:`CoverageData.loads` method reconstitutes ths + - The new :meth:`.CoverageData.dumps` method serializes the data to a string, + and a corresponding :meth:`.CoverageData.loads` method reconstitutes ths data. The format of the data string is subject to change at any time, and so should only be used between two installations of the same version of coverage.py. - - The :meth:`CoverageData constructor` has a new + - The :meth:`CoverageData constructor<.CoverageData.__init__>` has a new argument, `no_disk` (default: False). Setting it to True prevents writing any data to the disk. This is useful for transient data objects. -- Added the classmethod :meth:`Coverage.current` to get the latest started +- Added the classmethod :meth:`.Coverage.current` to get the latest started Coverage instance. - Multiprocessing support in Python 3.8 was broken, but is now fixed. Closes @@ -115,7 +115,7 @@ Version 5.0a5 --- 2019-05-07 :ref:`dynamic_context_plugins` for more details. - Another tool (such as a test runner) can use the new - :meth:`Coverage.switch_context` method to explicitly change the context. + :meth:`.Coverage.switch_context` method to explicitly change the context. - The ``dynamic_context = test_function`` setting now works with Python 2 old-style classes, though it only reports the method name, not the class it @@ -134,7 +134,7 @@ Version 5.0a5 --- 2019-05-07 - Combining data stored in SQLite now goes about twice as fast, fixing `issue 761`_. Thanks, Stephan Richter. -- The ``filename`` attribute on :class:`CoverageData` objects has been made +- The ``filename`` attribute on :class:`.CoverageData` objects has been made private. You can use the ``data_filename`` method to get the actual file name being used to store data, and the ``base_filename`` method to get the original filename before parallelizing suffixes were added. This is part of diff --git a/doc/api_coveragedata.rst b/doc/api_coveragedata.rst index b3b643283..3ab741eca 100644 --- a/doc/api_coveragedata.rst +++ b/doc/api_coveragedata.rst @@ -11,6 +11,7 @@ The CoverageData class .. versionadded:: 4.0 .. module:: coverage + :noindex: .. autoclass:: CoverageData :members: diff --git a/doc/api_plugin.rst b/doc/api_plugin.rst index d070e2bf1..912192477 100644 --- a/doc/api_plugin.rst +++ b/doc/api_plugin.rst @@ -13,6 +13,7 @@ Plug-in classes .. module:: coverage + :noindex: The CoveragePlugin class ------------------------ diff --git a/doc/changes.rst b/doc/changes.rst index 1dd7b911b..73d1532ba 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -3,6 +3,4 @@ .. _changes: -.. module:: coverage - .. include:: ../CHANGES.rst diff --git a/doc/config.rst b/doc/config.rst index 04daa01d4..35584f010 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -17,7 +17,6 @@ Configuration reference .. :history: 20150124T173400, updated for 4.0a4 .. :history: 20150802T174600, updated for 4.0b1 -.. module:: coverage Coverage.py options can be specified in a configuration file. This makes it easier to re-run coverage.py with consistent settings, and also allows for @@ -162,7 +161,7 @@ in measurement or reporting. Ignored if ``source`` is set. See :ref:`source` for details. ``note`` (string): an arbitrary string that will be written to the data file. -You can use the :meth:`CoverageData.run_infos` method to retrieve this string +You can use the :meth:`.CoverageData.run_infos` method to retrieve this string from a data file. ``omit`` (multi-string): a list of file name patterns, the files to leave out diff --git a/doc/contexts.rst b/doc/contexts.rst index 6a1293fd4..8f7854f9f 100644 --- a/doc/contexts.rst +++ b/doc/contexts.rst @@ -11,8 +11,6 @@ Measurement contexts .. versionadded:: 5.0 -.. module:: coverage - Coverage.py measures whether code was run, but it can also record the context in which it was run. This can provide more information to help you understand the behavior of your tests. diff --git a/doc/dbschema.rst b/doc/dbschema.rst index aa838fdf2..c57de92ec 100644 --- a/doc/dbschema.rst +++ b/doc/dbschema.rst @@ -9,17 +9,15 @@ Coverage.py database schema .. versionadded:: 5.0 -.. module:: coverage - Coverage.py stores data in a SQLite database, by default called ``.coverage``. -For most needs, the :class:`CoverageData` API will be sufficient, and should be -preferred to accessing the database directly. Only advanced uses will need to -use the database. +For most needs, the :class:`.CoverageData` API will be sufficient, and should +be preferred to accessing the database directly. Only advanced uses will need +to use the database. You can use SQLite tools such as the :mod:`sqlite3 ` module in the Python standard library to access the data. Some data is stored in a packed format that will need custom functions to access. See -:func:`register_sqlite_functions`. +:func:`.register_sqlite_functions`. Database schema From a005ce15e11ad66e384b5eed095812db04bc9420 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Aug 2019 18:05:04 -0400 Subject: [PATCH 616/952] Fix autosphinx links in uploaded release notes The :meth:`.CoverageData.foobar` syntax isn't understood by pandoc when converting to markdown. A Sphinx rst builder converts them into more basic Sphinx links first. --- Makefile | 13 +++++++------ doc/conf.py | 1 + doc/requirements.pip | 1 + 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 6810fdeb3..580f1f239 100644 --- a/Makefile +++ b/Makefile @@ -114,8 +114,8 @@ uninstall: # Documentation -SPHINXBUILD = .tox/doc/bin/sphinx-build -SPHINXOPTS = -a -E doc +SPHINXOPTS = -aE +SPHINXBUILD = .tox/doc/bin/sphinx-build $(SPHINXOPTS) WEBHOME = ~/web/stellated/ WEBSAMPLE = $(WEBHOME)/files/sample_coverage_html WEBSAMPLEBETA = $(WEBHOME)/files/sample_coverage_html_beta @@ -124,12 +124,12 @@ docreqs: tox -q -e doc --notest dochtml: docreqs - $(SPHINXBUILD) -b html $(SPHINXOPTS) doc/_build/html + $(SPHINXBUILD) -b html doc doc/_build/html @echo @echo "Build finished. The HTML pages are in doc/_build/html." docspell: docreqs - $(SPHINXBUILD) -b spelling $(SPHINXOPTS) doc/_spell + $(SPHINXBUILD) -b spelling doc doc/_spell publish: rm -f $(WEBSAMPLE)/*.* @@ -141,5 +141,6 @@ publishbeta: mkdir -p $(WEBSAMPLEBETA) cp doc/sample_html_beta/*.* $(WEBSAMPLEBETA) -upload_relnotes: - python ci/upload_relnotes.py CHANGES.rst pypi/coverage +upload_relnotes: docreqs + $(SPHINXBUILD) -b rst doc /tmp/rst_rst + python ci/upload_relnotes.py /tmp/rst_rst/changes.rst pypi/coverage diff --git a/doc/conf.py b/doc/conf.py index 836f77c98..4baf66acc 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -35,6 +35,7 @@ 'sphinx.ext.ifconfig', 'sphinxcontrib.spelling', 'sphinx.ext.intersphinx', + 'sphinx_rst_builder', ] # Add any paths that contain templates here, relative to this directory. diff --git a/doc/requirements.pip b/doc/requirements.pip index 3f8647379..28777cb56 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -5,5 +5,6 @@ doc8==0.8.0 pyenchant==2.0.0 sphinx==2.1.2 +sphinx-rst-builder==0.0.1 sphinxcontrib-spelling==4.3.0 sphinx_rtd_theme==0.4.3 From 48660ffdcad828ba21da711a07af9ab19e106276 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 11 Aug 2019 18:52:27 -0400 Subject: [PATCH 617/952] debug=plugin didn't handle all plugin methods. #834 --- CHANGES.rst | 4 ++++ coverage/plugin_support.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f2c017f2d..34a756345 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -30,7 +30,11 @@ Unreleased - Fixed an unusual tokenizing issue with backslashes in comments. Fixes `issue 822`_. +- `debug=plugin` didn't properly support configuration or dynamic context + plugins, but now it does, closing `issue 834`_. + .. _issue 822: https://github.com/nedbat/coveragepy/issues/822 +.. _issue 834: https://github.com/nedbat/coveragepy/issues/834 .. _issue 829: https://github.com/nedbat/coveragepy/issues/829 diff --git a/coverage/plugin_support.py b/coverage/plugin_support.py index 7c25a5f11..89c1c7658 100644 --- a/coverage/plugin_support.py +++ b/coverage/plugin_support.py @@ -167,6 +167,20 @@ def file_reporter(self, filename): reporter = DebugFileReporterWrapper(filename, reporter, debug) return reporter + def dynamic_context(self, frame): + context = self.plugin.dynamic_context(frame) + self.debug.write("dynamic_context(%r) --> %r" % (frame, context)) + return context + + def find_executable_files(self, src_dir): + executable_files = self.plugin.find_executable_files(src_dir) + self.debug.write("find_executable_files(%r) --> %r" % (src_dir, executable_files)) + return executable_files + + def configure(self, config): + self.debug.write("configure(%r)" % (config,)) + self.plugin.configure(config) + def sys_info(self): return self.plugin.sys_info() From c8643d84d6f2a89287ddce0e37cfca1eaf337d80 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 12 Aug 2019 06:28:15 -0400 Subject: [PATCH 618/952] Cleaner release note conversion and upload --- Makefile | 3 ++- ci/upload_relnotes.py | 28 +++++++++++++--------------- tox.ini | 4 ++-- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index 580f1f239..996b7d4f4 100644 --- a/Makefile +++ b/Makefile @@ -143,4 +143,5 @@ publishbeta: upload_relnotes: docreqs $(SPHINXBUILD) -b rst doc /tmp/rst_rst - python ci/upload_relnotes.py /tmp/rst_rst/changes.rst pypi/coverage + pandoc -frst -tmarkdown_strict --atx-headers /tmp/rst_rst/changes.rst > /tmp/rst_rst/changes.md + python ci/upload_relnotes.py /tmp/rst_rst/changes.md pypi/coverage diff --git a/ci/upload_relnotes.py b/ci/upload_relnotes.py index a5dd7fe53..630f4d0a3 100644 --- a/ci/upload_relnotes.py +++ b/ci/upload_relnotes.py @@ -1,17 +1,15 @@ #!/usr/bin/env python3 """ -Upload CHANGES.rst to Tidelift as Markdown chunks - -Requires pandoc installed. +Upload CHANGES.md to Tidelift as Markdown chunks Put your Tidelift API token in a file called tidelift.token alongside this program, for example: user/n3IwOpxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxc2ZwE4 -Run with two arguments: the .rst file to parse, and the Tidelift package name: +Run with two arguments: the .md file to parse, and the Tidelift package name: - python upload_relnotes.py CHANGES.rst pypi/coverage + python upload_relnotes.py CHANGES.md pypi/coverage Every section that has something that looks like a version number in it will be uploaded as the release notes for that version. @@ -20,7 +18,6 @@ import os.path import re -import subprocess import sys import requests @@ -31,12 +28,15 @@ def __init__(self): self.buffer = [] def append(self, text): + """Add `text` to the buffer.""" self.buffer.append(text) def clear(self): + """Clear the buffer.""" self.buffer = [] def flush(self): + """Produce a ("text", text) tuple if there's anything here.""" buffered = "".join(self.buffer).strip() if buffered: yield ("text", buffered) @@ -74,7 +74,6 @@ def sections(parsed_data): if header: yield (*header, "\n".join(text)) text = [] - notes = [] header = (ttype, ttext) elif ttype == "text": text.append(ttext) @@ -84,7 +83,7 @@ def sections(parsed_data): def relnotes(mdlines): - """Yield (version, text) pairs from markdown lines. + r"""Yield (version, text) pairs from markdown lines. Each tuple is a separate version mentioned in the release notes. @@ -97,11 +96,8 @@ def relnotes(mdlines): version = m_version.group() yield version, text -def convert_rst_file_to_markdown(rst_filename): - markdown = subprocess.check_output(["pandoc", "-frst", "-tmarkdown_strict", "--atx-headers", rst_filename]) - return markdown.decode("utf8") - def update_release_note(package, version, text): + """Update the release notes for one version of a package.""" url = f"https://api.tidelift.com/external-api/lifting/{package}/release-notes/{version}" token_file = os.path.join(os.path.dirname(__file__), "tidelift.token") with open(token_file) as ftoken: @@ -115,10 +111,12 @@ def update_release_note(package, version, text): result = requests.put(**req_args) print(f"{version}: {result.status_code}") -def convert_and_upload(rst_filename, package): - markdown = convert_rst_file_to_markdown(rst_filename) +def parse_and_upload(md_filename, package): + """Main function: parse markdown and upload to Tidelift.""" + with open(md_filename) as f: + markdown = f.read() for version, text in relnotes(markdown.splitlines(True)): update_release_note(package, version, text) if __name__ == "__main__": - convert_and_upload(*sys.argv[1:]) + parse_and_upload(*sys.argv[1:]) # pylint: disable=no-value-for-parameter diff --git a/tox.ini b/tox.ini index c9deb506c..bf636f27f 100644 --- a/tox.ini +++ b/tox.ini @@ -65,7 +65,7 @@ deps = -r doc/requirements.pip setenv = - LINTABLE = coverage tests doc igor.py setup.py __main__.py + LINTABLE = coverage tests doc ci igor.py setup.py __main__.py commands = python -m tabnanny {env:LINTABLE} @@ -73,7 +73,7 @@ commands = check-manifest --ignore 'lab*,perf*,doc/sample_html*,.treerc,.github*' python setup.py -q sdist bdist_wheel twine check dist/* - python -m pylint --notes= {env:LINTABLE} + python -m pylint --notes= -j 4 {env:LINTABLE} [travis] #2.7: py27, lint From d399bada2bf21c938cc51815ac52acd51e3b9c3e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 25 Aug 2019 14:21:42 -0400 Subject: [PATCH 619/952] Tidelift enterprise --- README.rst | 37 ++++++++++++++++++++----------------- doc/index.rst | 18 +++++++++++++----- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/README.rst b/README.rst index 511d08788..65d40917d 100644 --- a/README.rst +++ b/README.rst @@ -17,23 +17,6 @@ Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. -.. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logo_small.png - :width: 75 - :alt: Tidelift - :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme - -.. list-table:: - :widths: 10 100 - - * - |tideliftlogo| - - Professional support for coverage.py is available as part of the `Tidelift - Subscription`_. Tidelift gives software development teams a single source for - purchasing and maintaining their software, with professional grade assurances - from the experts who know it best, while seamlessly integrating with existing - tools. - -.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme - Coverage.py runs on many versions of Python: * CPython 2.7. @@ -53,6 +36,26 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on 3.3 and 3.4. +For Enterprise +-------------- + +.. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logo_small.png + :width: 75 + :alt: Tidelift + :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme + +.. list-table:: + :widths: 10 100 + + * - |tideliftlogo| + - `Available as part of the Tidelift Subscription. `_ + Coverage and the maintainers of thousands of other packages are working with + Tidelift to deliver one enterprise subscription that covers all of the open + source you use. If you want the flexibility of open source and the confidence + of commercial-grade software, this is for you. + `Learn more. `_ + + Getting Started --------------- diff --git a/doc/index.rst b/doc/index.rst index f4d20a92d..173ec7e59 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -44,6 +44,9 @@ not. .. _described here: http://coverage.readthedocs.io/ +For Enterprise +-------------- + .. image:: media/Tidelift_Logos_RGB_Tidelift_Shorthand_On-White.png :width: 75 :alt: Tidelift @@ -51,11 +54,16 @@ not. :class: tideliftlogo :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme -Professional support for coverage.py is available as part of the `Tidelift -Subscription`_. Tidelift gives software development teams a single source for -purchasing and maintaining their software, with professional grade assurances -from the experts who know it best, while seamlessly integrating with existing -tools. +.. |br| raw:: html + +
+ +`Available as part of the Tidelift Subscription. `_ |br| +Coverage and the maintainers of thousands of other packages are working with +Tidelift to deliver one enterprise subscription that covers all of the open +source you use. If you want the flexibility of open source and the confidence +of commercial-grade software, this is for you. `Learn more. `_ .. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=docs From c152eac7d046d412f985565a35aadc2a01c5fd8c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 25 Aug 2019 14:43:17 -0400 Subject: [PATCH 620/952] Enterprise in toc (cherry picked from commit 70ede36c8f1146ad389332d944735407d1000c86) --- doc/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/index.rst b/doc/index.rst index 173ec7e59..52f4da01a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -177,6 +177,7 @@ More information :maxdepth: 1 install + For Enterprise cmd config source From 790f0b30010a3a1f68f4fa7c172ce3b31c7c4b24 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 26 Aug 2019 20:29:29 -0400 Subject: [PATCH 621/952] Better wording from Tidelift --- README.rst | 2 +- doc/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 65d40917d..7c38ad0f0 100644 --- a/README.rst +++ b/README.rst @@ -49,7 +49,7 @@ For Enterprise * - |tideliftlogo| - `Available as part of the Tidelift Subscription. `_ - Coverage and the maintainers of thousands of other packages are working with + Coverage and thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. If you want the flexibility of open source and the confidence of commercial-grade software, this is for you. diff --git a/doc/index.rst b/doc/index.rst index 52f4da01a..d1eace30a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -59,7 +59,7 @@ For Enterprise
`Available as part of the Tidelift Subscription. `_ |br| -Coverage and the maintainers of thousands of other packages are working with +Coverage and thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. If you want the flexibility of open source and the confidence of commercial-grade software, this is for you. `Learn more. Date: Mon, 8 Jul 2019 23:09:38 -0400 Subject: [PATCH 622/952] Create a JSON report --- CHANGES.rst | 3 + CONTRIBUTORS.txt | 1 + coverage/cmdline.py | 34 ++++++++++ coverage/config.py | 10 +++ coverage/control.py | 54 ++++++++-------- coverage/jsonreport.py | 100 ++++++++++++++++++++++++++++ coverage/report.py | 38 ++++++++++- coverage/results.py | 3 +- coverage/xmlreport.py | 17 +++-- doc/branch.rst | 5 +- doc/cmd.rst | 17 ++++- doc/config.rst | 19 ++++++ doc/howitworks.rst | 4 +- doc/source.rst | 10 +-- tests/test_cmdline.py | 94 +++++++++++++++++++++++---- tests/test_config.py | 6 ++ tests/test_json.py | 144 +++++++++++++++++++++++++++++++++++++++++ 17 files changed, 496 insertions(+), 63 deletions(-) create mode 100644 coverage/jsonreport.py create mode 100644 tests/test_json.py diff --git a/CHANGES.rst b/CHANGES.rst index 34a756345..539f8a094 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -33,6 +33,9 @@ Unreleased - `debug=plugin` didn't properly support configuration or dynamic context plugins, but now it does, closing `issue 834`_. +- Added a JSON report `issue 720`_. + +.. _issue 720: https://github.com/nedbat/coveragepy/issues/720 .. _issue 822: https://github.com/nedbat/coveragepy/issues/822 .. _issue 834: https://github.com/nedbat/coveragepy/issues/834 .. _issue 829: https://github.com/nedbat/coveragepy/issues/829 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 06eef4f79..859edcf6d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -79,6 +79,7 @@ Marc Abramowitz Marcus Cobden Mark van der Wal Martin Fuzzey +Matt Bachmann Matthew Boehm Matthew Desmarais Max Linke diff --git a/coverage/cmdline.py b/coverage/cmdline.py index fdab7d931..78e90d45e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -118,6 +118,15 @@ class Opts(object): metavar="OUTFILE", help="Write the XML report to this file. Defaults to 'coverage.xml'", ) + output_json = optparse.make_option( + '-o', '', action='store', dest="outfile", + metavar="OUTFILE", + help="Write the JSON report to this file. Defaults to 'coverage.json'", + ) + json_pretty_print = optparse.make_option( + '', '--pretty-print', action='store_true', + help="Print the json formatted for human readers", + ) parallel_mode = optparse.make_option( '-p', '--parallel-mode', action='store_true', help=( @@ -402,6 +411,22 @@ def get_prog_name(self): usage="[options] [modules]", description="Generate an XML report of coverage results." ), + + 'json': CmdOptionParser( + "json", + [ + Opts.fail_under, + Opts.ignore_errors, + Opts.include, + Opts.omit, + Opts.output_json, + Opts.json_pretty_print, + Opts.show_contexts, + Opts.contexts, + ] + GLOBAL_ARGS, + usage="[options] [modules]", + description="Generate a JSON report of coverage results." + ), } @@ -565,6 +590,14 @@ def command_line(self, argv): elif options.action == "xml": outfile = options.outfile total = self.coverage.xml_report(outfile=outfile, **report_args) + elif options.action == "json": + outfile = options.outfile + total = self.coverage.json_report( + outfile=outfile, + pretty_print=options.pretty_print, + show_contexts=options.show_contexts, + **report_args + ) if total is not None: # Apply the command line fail-under options, and then use the config @@ -752,6 +785,7 @@ def unglob_args(args): erase Erase previously collected coverage data. help Get help on using coverage.py. html Create an HTML report. + json Create a JSON report of coverage results. report Report coverage stats on modules. run Run a Python program and measure code execution. xml Create an XML report of coverage results. diff --git a/coverage/config.py b/coverage/config.py index 4bb60eb31..516fe1b9f 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -215,6 +215,11 @@ def __init__(self): self.xml_output = "coverage.xml" self.xml_package_depth = 99 + # Defaults for [JSON] + self.json_output = "coverage.json" + self.json_pretty_print = False + self.json_show_contexts = False + # Defaults for [paths] self.paths = {} @@ -363,6 +368,11 @@ def from_file(self, filename, our_file): # [xml] ('xml_output', 'xml:output'), ('xml_package_depth', 'xml:package_depth', 'int'), + + # [json] + ('json_output', 'json:output'), + ('json_pretty_print', 'json:pretty_print', 'boolean'), + ('json_show_contexts', 'json:show_contexts', 'boolean'), ] def _set_attr_from_config_option(self, cp, attr, where, type_=''): diff --git a/coverage/control.py b/coverage/control.py index 7d9d9e911..8cba6c38e 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -22,11 +22,13 @@ from coverage.files import PathAliases, set_relative_directory, abs_file from coverage.html import HtmlReporter from coverage.inorout import InOrOut +from coverage.jsonreport import JsonReporter from coverage.misc import CoverageException, bool_or_none, join_regex -from coverage.misc import ensure_dir_for_file, file_be_gone, isolate_module +from coverage.misc import ensure_dir_for_file, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins from coverage.python import PythonFileReporter +from coverage.report import render_report from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -862,33 +864,29 @@ def xml_report( ignore_errors=ignore_errors, report_omit=omit, report_include=include, xml_output=outfile, report_contexts=contexts, ) - file_to_close = None - delete_file = False - if self.config.xml_output: - if self.config.xml_output == '-': - outfile = sys.stdout - else: - # Ensure that the output directory is created; done here - # because this report pre-opens the output file. - # HTMLReport does this using the Report plumbing because - # its task is more complex, being multiple files. - ensure_dir_for_file(self.config.xml_output) - open_kwargs = {} - if env.PY3: - open_kwargs['encoding'] = 'utf8' - outfile = open(self.config.xml_output, "w", **open_kwargs) - file_to_close = outfile - try: - reporter = XmlReporter(self) - return reporter.report(morfs, outfile=outfile) - except CoverageException: - delete_file = True - raise - finally: - if file_to_close: - file_to_close.close() - if delete_file: - file_be_gone(self.config.xml_output) + return render_report(self.config.xml_output, XmlReporter(self), morfs) + + def json_report( + self, morfs=None, outfile=None, ignore_errors=None, + omit=None, include=None, contexts=None, pretty_print=None, + show_contexts=None + ): + """Generate a JSON report of coverage results. + + Each module in `morfs` is included in the report. `outfile` is the + path to write the file to, "-" will write to stdout. + + See :meth:`report` for other arguments. + + Returns a float, the total percentage covered. + + """ + self.config.from_args( + ignore_errors=ignore_errors, report_omit=omit, report_include=include, + json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, + json_show_contexts=show_contexts + ) + return render_report(self.config.json_output, JsonReporter(self), morfs) def sys_info(self): """Return a list of (key, value) pairs showing internal information.""" diff --git a/coverage/jsonreport.py b/coverage/jsonreport.py new file mode 100644 index 000000000..e44cbf089 --- /dev/null +++ b/coverage/jsonreport.py @@ -0,0 +1,100 @@ +# coding: utf-8 +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Json reporting for coverage.py""" +import datetime +import json +import sys + +from coverage import __version__ +from coverage.report import get_analysis_to_report +from coverage.results import Numbers + + +class JsonReporter(object): + """A reporter for writing JSON coverage results.""" + + def __init__(self, coverage): + self.coverage = coverage + self.config = self.coverage.config + self.total = Numbers() + self.report_data = {} + + def report(self, morfs, outfile=None): + """Generate a json report for `morfs`. + + `morfs` is a list of modules or file names. + + `outfile` is a file object to write the json to + + """ + outfile = outfile or sys.stdout + coverage_data = self.coverage.get_data() + coverage_data.set_query_contexts(self.config.report_contexts) + self.report_data["meta"] = { + "version": __version__, + "timestamp": datetime.datetime.now().isoformat(), + "branch_coverage": coverage_data.has_arcs(), + "show_contexts": self.config.json_show_contexts, + } + + measured_files = {} + for file_reporter, analysis in get_analysis_to_report(self.coverage, morfs): + measured_files[file_reporter.relative_filename()] = self.report_one_file( + coverage_data, + file_reporter, + analysis + ) + + self.report_data["files"] = measured_files + + self.report_data["totals"] = { + 'covered_lines': self.total.n_executed, + 'num_statements': self.total.n_statements, + 'percent_covered': self.total.pc_covered, + 'missing_lines': self.total.n_missing, + 'excluded_lines': self.total.n_excluded, + } + + if coverage_data.has_arcs(): + self.report_data["totals"].update({ + 'num_branches': self.total.n_branches, + 'num_partial_branches': self.total.n_partial_branches, + }) + + json.dump( + self.report_data, + outfile, + indent=4 if self.config.json_pretty_print else None + ) + + return self.total.n_statements and self.total.pc_covered + + def report_one_file(self, coverage_data, file_reporter, analysis): + """Extract the relevant report data for a single file""" + nums = analysis.numbers + self.total += nums + summary = { + 'covered_lines': nums.n_executed, + 'num_statements': nums.n_statements, + 'percent_covered': nums.pc_covered, + 'missing_lines': nums.n_missing, + 'excluded_lines': nums.n_excluded, + } + reported_file = { + 'executed_lines': sorted(analysis.executed), + 'summary': summary, + 'missing_lines': sorted(analysis.missing), + 'excluded_lines': sorted(analysis.excluded) + } + if self.config.json_show_contexts: + reported_file['contexts'] = analysis.data.contexts_by_lineno( + file_reporter.filename + ) + if coverage_data.has_arcs(): + reported_file['summary'].update({ + 'num_branches': nums.n_branches, + 'num_partial_branches': nums.n_partial_branches, + }) + return reported_file diff --git a/coverage/report.py b/coverage/report.py index 9a7402909..d24dddc88 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -2,9 +2,45 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Reporter foundation for coverage.py.""" +import sys +from coverage import env from coverage.files import prep_patterns, FnmatchMatcher -from coverage.misc import CoverageException, NoSource, NotPython +from coverage.misc import CoverageException, NoSource, NotPython, ensure_dir_for_file, file_be_gone + + +def render_report(output_path, reporter, morfs): + """Run the provided reporter ensuring any required setup and cleanup is done + + At a high level this method ensures the output file is ready to be written to. Then writes the + report to it. Then closes the file and deletes any garbage created if necessary. + """ + file_to_close = None + delete_file = False + if output_path: + if output_path == '-': + outfile = sys.stdout + else: + # Ensure that the output directory is created; done here + # because this report pre-opens the output file. + # HTMLReport does this using the Report plumbing because + # its task is more complex, being multiple files. + ensure_dir_for_file(output_path) + open_kwargs = {} + if env.PY3: + open_kwargs['encoding'] = 'utf8' + outfile = open(output_path, "w", **open_kwargs) + file_to_close = outfile + try: + return reporter.report(morfs, outfile=outfile) + except CoverageException: + delete_file = True + raise + finally: + if file_to_close: + file_to_close.close() + if delete_file: + file_be_gone(output_path) def get_analysis_to_report(coverage, morfs): diff --git a/coverage/results.py b/coverage/results.py index ba335209b..c88da9195 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -23,7 +23,8 @@ def __init__(self, data, file_reporter): # Identify missing statements. executed = self.data.lines(self.filename) or [] executed = self.file_reporter.translate_lines(executed) - self.missing = self.statements - executed + self.executed = executed + self.missing = self.statements - self.executed if self.data.has_arcs(): self._arc_possibilities = sorted(self.file_reporter.arcs()) diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 079677194..265bf02cf 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -44,8 +44,6 @@ def __init__(self, coverage): self.source_paths.add(files.canonical_filename(src)) self.packages = {} self.xml_out = None - self.data = coverage.get_data() - self.has_arcs = self.data.has_arcs() def report(self, morfs, outfile=None): """Generate a Cobertura-compatible XML report for `morfs`. @@ -57,6 +55,7 @@ def report(self, morfs, outfile=None): """ # Initial setup. outfile = outfile or sys.stdout + has_arcs = self.coverage.get_data().has_arcs() # Create the DOM that will store the data. impl = xml.dom.minidom.getDOMImplementation() @@ -73,7 +72,7 @@ def report(self, morfs, outfile=None): # Call xml_file for each file in the data. for fr, analysis in get_analysis_to_report(self.coverage, morfs): - self.xml_file(fr, analysis) + self.xml_file(fr, analysis, has_arcs) xsources = self.xml_out.createElement("sources") xcoverage.appendChild(xsources) @@ -102,7 +101,7 @@ def report(self, morfs, outfile=None): xclasses.appendChild(class_elt) xpackage.setAttribute("name", pkg_name.replace(os.sep, '.')) xpackage.setAttribute("line-rate", rate(lhits, lnum)) - if self.has_arcs: + if has_arcs: branch_rate = rate(bhits, bnum) else: branch_rate = "0" @@ -117,7 +116,7 @@ def report(self, morfs, outfile=None): xcoverage.setAttribute("lines-valid", str(lnum_tot)) xcoverage.setAttribute("lines-covered", str(lhits_tot)) xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot)) - if self.has_arcs: + if has_arcs: xcoverage.setAttribute("branches-valid", str(bnum_tot)) xcoverage.setAttribute("branches-covered", str(bhits_tot)) xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot)) @@ -138,7 +137,7 @@ def report(self, morfs, outfile=None): pct = 100.0 * (lhits_tot + bhits_tot) / denom return pct - def xml_file(self, fr, analysis): + def xml_file(self, fr, analysis, has_arcs): """Add to the XML report for a single file.""" # Create the 'lines' and 'package' XML elements, which @@ -182,7 +181,7 @@ def xml_file(self, fr, analysis): # executed? If so, that should be recorded here. xline.setAttribute("hits", str(int(line not in analysis.missing))) - if self.has_arcs: + if has_arcs: if line in branch_stats: total, taken = branch_stats[line] xline.setAttribute("branch", "true") @@ -198,7 +197,7 @@ def xml_file(self, fr, analysis): class_lines = len(analysis.statements) class_hits = class_lines - len(analysis.missing) - if self.has_arcs: + if has_arcs: class_branches = sum(t for t, k in branch_stats.values()) missing_branches = sum(t - k for t, k in branch_stats.values()) class_br_hits = class_branches - missing_branches @@ -208,7 +207,7 @@ def xml_file(self, fr, analysis): # Finalize the statistics that are collected in the XML DOM. xclass.setAttribute("line-rate", rate(class_hits, class_lines)) - if self.has_arcs: + if has_arcs: branch_rate = rate(class_br_hits, class_branches) else: branch_rate = "0" diff --git a/doc/branch.rst b/doc/branch.rst index 92cab27b1..9af8f050e 100644 --- a/doc/branch.rst +++ b/doc/branch.rst @@ -55,8 +55,9 @@ The HTML report gives information about which lines had missing branches. Lines that were missing some branches are shown in yellow, with an annotation at the far right showing branch destination line numbers that were not exercised. -The XML report produced by ``coverage xml`` also includes branch information, -including separate statement and branch coverage percentages. +The XML and JSON reports produced by ``coverage xml`` and ``coverage json`` +respectively also include branch information, including separate statement and +branch coverage percentages. How it works diff --git a/doc/cmd.rst b/doc/cmd.rst index 427384932..ad84a6ae8 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -22,6 +22,7 @@ Coverage.py command line usage .. :history: 20121117T091000, Added command aliases. .. :history: 20140924T193000, Added --concurrency .. :history: 20150802T174700, Updated for 4.0b1 +.. :history: 20190828T212200, added json report .. highlight:: console @@ -41,6 +42,8 @@ Coverage.py has a number of commands which determine the action performed: * **html** -- Produce annotated HTML listings with coverage results. +* **json** -- Produce a JSON report with coverage results. + * **xml** -- Produce an XML report with coverage results. * **annotate** -- Annotate source files with coverage results. @@ -292,7 +295,8 @@ Reporting --------- Coverage.py provides a few styles of reporting, with the **report**, **html**, -**annotate**, and **xml** commands. They share a number of common options. +**annotate**, **json**, and **xml** commands. They share a number of common +options. The command-line arguments are module or file names to report on, if you'd like to report on a subset of the data collected. @@ -473,6 +477,17 @@ You can specify the name of the output file with the ``-o`` switch. Other common reporting options are described above in :ref:`cmd_reporting`. +.. _cmd_json: + +JSON reporting +------------- + +The **json** command writes coverage data to a "coverage.json" file. + +You can specify the name of the output file with the ``-o`` switch. + +Other common reporting options are described above in :ref:`cmd_reporting`. + .. _cmd_debug: diff --git a/doc/config.rst b/doc/config.rst index 35584f010..84c77131a 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -301,3 +301,22 @@ also apply to XML output, where appropriate. identified as packages in the report. Directories deeper than this depth are not reported as packages. The default is that all directories are reported as packages. + +.. _config_json: + +[json] +----- + +Values particular to json reporting. The values in the ``[report]`` section +also apply to JSON output, where appropriate. + +``json_output`` (string, default "coverage.json"): where to write the json +report. + +``json_pretty_print`` (boolean, default false): controls if fields in the json +are outputted with whitespace formatted for human consumption (True) or for +minimum file size (False) + +``json_show_contexts`` (boolean, default false): should the json report include +an indication on each line of which contexts executed the line. +See :ref:`dynamic_contexts` for details. diff --git a/doc/howitworks.rst b/doc/howitworks.rst index 62af42e35..0e11c29e8 100644 --- a/doc/howitworks.rst +++ b/doc/howitworks.rst @@ -83,8 +83,8 @@ Reporting Once we have the set of executed lines and missing lines, reporting is just a matter of formatting that information in a useful way. Each reporting method -(text, html, annotated source, xml) has a different output format, but the -process is the same: write out the information in the particular format, +(text, html, json, annotated source, xml) has a different output format, but +the process is the same: write out the information in the particular format, possibly including the source code itself. diff --git a/doc/source.rst b/doc/source.rst index e1bc80385..21a5f612a 100644 --- a/doc/source.rst +++ b/doc/source.rst @@ -80,11 +80,11 @@ reported. Usually you want to see all the code that was measured, but if you are measuring a large project, you may want to get reports for just certain parts. -The report commands (``report``, ``html``, ``annotate``, and ``xml``) all take -optional ``modules`` arguments, and ``--include`` and ``--omit`` switches. The -``modules`` arguments specify particular modules to report on. The ``include`` -and ``omit`` values are lists of file name patterns, just as with the ``run`` -command. +The report commands (``report``, ``html``, ``json``, ``annotate``, and ``xml``) +all take optional ``modules`` arguments, and ``--include`` and ``--omit`` +switches. The ``modules`` arguments specify particular modules to report on. +The ``include`` and ``omit`` values are lists of file name patterns, just as +with the ``run`` command. Remember that the reporting commands can only report on the data that has been collected, so the data you're looking for may not be in the data available for diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 387bf61fb..e15c5fcba 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -46,6 +46,10 @@ class BaseCmdLineTest(CoverageTest): ignore_errors=None, include=None, omit=None, morfs=[], outfile=None, contexts=None, ) + _defaults.Coverage().json_report( + ignore_errors=None, include=None, omit=None, morfs=[], outfile=None, + contexts=None, pretty_print=None, show_contexts=None + ) _defaults.Coverage( cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None, debug=None, @@ -69,6 +73,7 @@ def model_object(self): cov.report.return_value = 50.0 cov.html_report.return_value = 50.0 cov.xml_report.return_value = 50.0 + cov.json_report.return_value = 50.0 return mk @@ -667,6 +672,59 @@ def test_xml(self): cov.xml_report(morfs=["mod1", "mod2", "mod3"]) """) + def test_json(self): + # coverage json [-i] [--omit DIR,...] [FILE1 FILE2 ...] + self.cmd_executes("json", """\ + cov = Coverage() + cov.load() + cov.json_report() + """) + self.cmd_executes("json --pretty-print", """\ + cov = Coverage() + cov.load() + cov.json_report(pretty_print=True) + """) + self.cmd_executes("json --pretty-print --show-contexts", """\ + cov = Coverage() + cov.load() + cov.json_report(pretty_print=True, show_contexts=True) + """) + self.cmd_executes("json -i", """\ + cov = Coverage() + cov.load() + cov.json_report(ignore_errors=True) + """) + self.cmd_executes("json -o myjson.foo", """\ + cov = Coverage() + cov.load() + cov.json_report(outfile="myjson.foo") + """) + self.cmd_executes("json -o -", """\ + cov = Coverage() + cov.load() + cov.json_report(outfile="-") + """) + self.cmd_executes("json --omit fooey", """\ + cov = Coverage(omit=["fooey"]) + cov.load() + cov.json_report(omit=["fooey"]) + """) + self.cmd_executes("json --omit fooey,booey", """\ + cov = Coverage(omit=["fooey", "booey"]) + cov.load() + cov.json_report(omit=["fooey", "booey"]) + """) + self.cmd_executes("json mod1", """\ + cov = Coverage() + cov.load() + cov.json_report(morfs=["mod1"]) + """) + self.cmd_executes("json mod1 mod2 mod3", """\ + cov = Coverage() + cov.load() + cov.json_report(morfs=["mod1", "mod2", "mod3"]) + """) + def test_no_arguments_at_all(self): self.cmd_help("", topic="minimum_help", ret=OK) @@ -847,11 +905,12 @@ def test_exit(self): class CoverageReportingFake(object): """A fake Coverage.coverage test double.""" # pylint: disable=missing-docstring - def __init__(self, report_result, html_result, xml_result): + def __init__(self, report_result, html_result, xml_result, json_report): self.config = CoverageConfig() self.report_result = report_result self.html_result = html_result self.xml_result = xml_result + self.json_result = json_report def set_option(self, optname, optvalue): self.config.set_option(optname, optvalue) @@ -871,24 +930,31 @@ def html_report(self, *args_unused, **kwargs_unused): def xml_report(self, *args_unused, **kwargs_unused): return self.xml_result + def json_report(self, *args_unused, **kwargs_unused): + return self.json_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), + ((20, 30, 40, 50), None, "report --fail-under=19", 0), + ((20, 30, 40, 50), None, "report --fail-under=21", 2), + ((20, 30, 40, 50), None, "html --fail-under=29", 0), + ((20, 30, 40, 50), None, "html --fail-under=31", 2), + ((20, 30, 40, 50), None, "xml --fail-under=39", 0), + ((20, 30, 40, 50), None, "xml --fail-under=41", 2), + ((20, 30, 40, 50), None, "json --fail-under=49", 0), + ((20, 30, 40, 50), None, "json --fail-under=51", 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), + ((20, 30, 40, 50), 19, "report", 0), + ((20, 30, 40, 50), 21, "report", 2), + ((20, 30, 40, 50), 29, "html", 0), + ((20, 30, 40, 50), 31, "html", 2), + ((20, 30, 40, 50), 39, "xml", 0), + ((20, 30, 40, 50), 41, "xml", 2), + ((20, 30, 40, 50), 49, "json", 0), + ((20, 30, 40, 50), 51, "json", 2), # Command-line overrides configuration. - ((20, 30, 40), 19, "report --fail-under=21", 2), + ((20, 30, 40, 50), 19, "report --fail-under=21", 2), ]) def test_fail_under(results, fail_under, cmd, ret): cov = CoverageReportingFake(*results) diff --git a/tests/test_config.py b/tests/test_config.py index 7b019f949..ebea18a70 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -332,6 +332,10 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): hello = world ; comments still work. names = Jane/John/Jenny + + [{section}json] + pretty_print = True + show_contexts = True """ # Just some sample setup.cfg text from the docs. @@ -399,6 +403,8 @@ def assert_config_settings_are_correct(self, cov): 'names': 'Jane/John/Jenny', }) self.assertEqual(cov.config.get_plugin_options("plugins.another"), {}) + self.assertEqual(cov.config.json_show_contexts, True) + self.assertEqual(cov.config.json_pretty_print, True) def test_config_file_settings(self): self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section="")) diff --git a/tests/test_json.py b/tests/test_json.py new file mode 100644 index 000000000..1ae5764ee --- /dev/null +++ b/tests/test_json.py @@ -0,0 +1,144 @@ +# coding: utf-8 +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Test json-based summary reporting for coverage.py""" +from datetime import datetime +import json +import os + +import coverage +from tests.coveragetest import UsingModulesMixin, CoverageTest + + +class JsonReportTest(UsingModulesMixin, CoverageTest): + """Tests of the JSON reports from coverage.py.""" + def _assert_expected_json_report(self, cov, expected_result): + """ + Helper for tests that handles the common ceremony so the tests can be clearly show the + consequences of setting various arguments. + """ + self.make_file("a.py", """\ + a = {'b': 1} + if a.get('a'): + b = 1 + """) + a = self.start_import_stop(cov, "a") + output_path = os.path.join(self.temp_dir, "a.json") + cov.json_report(a, outfile=output_path) + with open(output_path) as result_file: + parsed_result = json.load(result_file) + self.assert_recent_datetime( + datetime.strptime(parsed_result['meta']['timestamp'], "%Y-%m-%dT%H:%M:%S.%f") + ) + del (parsed_result['meta']['timestamp']) + assert parsed_result == expected_result + + def test_branch_coverage(self): + cov = coverage.Coverage(branch=True) + expected_result = { + 'meta': { + "version": coverage.__version__, + "branch_coverage": True, + "show_contexts": False, + }, + 'files': { + 'a.py': { + 'executed_lines': [1, 2], + 'missing_lines': [3], + 'excluded_lines': [], + 'summary': { + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'num_branches': 2, + 'excluded_lines': 0, + 'num_partial_branches': 1, + 'percent_covered': 60.0 + } + } + }, + 'totals': { + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'num_branches': 2, + 'excluded_lines': 0, + 'num_partial_branches': 1, + 'percent_covered': 60.0 + } + } + self._assert_expected_json_report(cov, expected_result) + + def test_simple_line_coverage(self): + cov = coverage.Coverage() + expected_result = { + 'meta': { + "version": coverage.__version__, + "branch_coverage": False, + "show_contexts": False, + }, + 'files': { + 'a.py': { + 'executed_lines': [1, 2], + 'missing_lines': [3], + 'excluded_lines': [], + 'summary': { + 'excluded_lines': 0, + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'percent_covered': 66.66666666666667 + } + } + }, + 'totals': { + 'excluded_lines': 0, + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'percent_covered': 66.66666666666667 + } + } + self._assert_expected_json_report(cov, expected_result) + + def test_context(self): + cov = coverage.Coverage(context="cool_test") + cov.config.json_show_contexts = True + expected_result = { + 'meta': { + "version": coverage.__version__, + "branch_coverage": False, + "show_contexts": True, + }, + 'files': { + 'a.py': { + 'executed_lines': [1, 2], + 'missing_lines': [3], + 'excluded_lines': [], + "contexts": { + "1": [ + "cool_test" + ], + "2": [ + "cool_test" + ] + }, + 'summary': { + 'excluded_lines': 0, + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'percent_covered': 66.66666666666667 + } + } + }, + 'totals': { + 'excluded_lines': 0, + 'missing_lines': 1, + 'covered_lines': 2, + 'num_statements': 3, + 'percent_covered': 66.66666666666667 + } + } + self._assert_expected_json_report(cov, expected_result) From 6e5418d9d7d03e0149ea5050f27ba28cdefc1104 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 31 Aug 2019 07:50:04 -0400 Subject: [PATCH 623/952] Tweaks to JSON report docs --- CHANGES.rst | 11 +++++++---- coverage/cmdline.py | 2 +- doc/branch.rst | 4 ++-- doc/cmd.rst | 6 ++++-- doc/config.rst | 20 ++++++++++---------- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 539f8a094..5d0ff1266 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,20 +20,23 @@ development at the same time, like 4.5.x and 5.0. Unreleased ---------- +- Data can now be "reported" in JSON format, for programmatic use, as requested + in `issue 720`_. The new ``coverage json`` command writes raw and summarized + data to a JSON file. Thanks, Matt Bachmann. + - The compact line number representation introduced in 5.0a6 is called a "numbits." The :mod:`coverage.numbits` module provides functions for working with them. -- A class named "test_something" no longer confuses the `test_function` dynamic - context setting. Fixes `issue 829`_. +- A class named "test_something" no longer confuses the ``test_function`` + dynamic context setting. Fixes `issue 829`_. - Fixed an unusual tokenizing issue with backslashes in comments. Fixes `issue 822`_. -- `debug=plugin` didn't properly support configuration or dynamic context +- ``debug=plugin`` didn't properly support configuration or dynamic context plugins, but now it does, closing `issue 834`_. -- Added a JSON report `issue 720`_. .. _issue 720: https://github.com/nedbat/coveragepy/issues/720 .. _issue 822: https://github.com/nedbat/coveragepy/issues/822 diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 78e90d45e..2bec4ea8c 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -125,7 +125,7 @@ class Opts(object): ) json_pretty_print = optparse.make_option( '', '--pretty-print', action='store_true', - help="Print the json formatted for human readers", + help="Format the JSON for human readers", ) parallel_mode = optparse.make_option( '-p', '--parallel-mode', action='store_true', diff --git a/doc/branch.rst b/doc/branch.rst index 9af8f050e..312b3a3a3 100644 --- a/doc/branch.rst +++ b/doc/branch.rst @@ -56,8 +56,8 @@ that were missing some branches are shown in yellow, with an annotation at the far right showing branch destination line numbers that were not exercised. The XML and JSON reports produced by ``coverage xml`` and ``coverage json`` -respectively also include branch information, including separate statement and -branch coverage percentages. +also include branch information, including separate statement and branch +coverage percentages. How it works diff --git a/doc/cmd.rst b/doc/cmd.rst index ad84a6ae8..1aca89a88 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -477,14 +477,16 @@ You can specify the name of the output file with the ``-o`` switch. Other common reporting options are described above in :ref:`cmd_reporting`. + .. _cmd_json: JSON reporting -------------- +-------------- The **json** command writes coverage data to a "coverage.json" file. -You can specify the name of the output file with the ``-o`` switch. +You can specify the name of the output file with the ``-o`` switch. The JSON +can be nicely formatted by specifying the ``--pretty-print`` switch. Other common reporting options are described above in :ref:`cmd_reporting`. diff --git a/doc/config.rst b/doc/config.rst index 84c77131a..43d5a27e4 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -302,21 +302,21 @@ identified as packages in the report. Directories deeper than this depth are not reported as packages. The default is that all directories are reported as packages. + .. _config_json: [json] ------ +------ -Values particular to json reporting. The values in the ``[report]`` section +Values particular to JSON reporting. The values in the ``[report]`` section also apply to JSON output, where appropriate. -``json_output`` (string, default "coverage.json"): where to write the json -report. +``output`` (string, default "coverage.json"): where to write the JSON file. -``json_pretty_print`` (boolean, default false): controls if fields in the json -are outputted with whitespace formatted for human consumption (True) or for -minimum file size (False) +``pretty_print`` (boolean, default false): controls if the JSON is outputted +with whitespace formatted for human consumption (True) or for minimum file size +(False). -``json_show_contexts`` (boolean, default false): should the json report include -an indication on each line of which contexts executed the line. -See :ref:`dynamic_contexts` for details. +``show_contexts`` (boolean, default false): should the JSON report include an +indication of which contexts executed each line. See :ref:`dynamic_contexts` +for details. From fae17db5a2e9706d8e27acd76a0867c29e721d0d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 1 Sep 2019 17:28:51 -0400 Subject: [PATCH 624/952] Make callers debug output easier to use as editor launches --- coverage/debug.py | 2 +- tests/test_debug.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index 9de9886b7..29e4977f1 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -153,7 +153,7 @@ def short_stack(limit=None, skip=0): """ stack = inspect.stack()[limit:skip:-1] - return "\n".join("%30s : %s @%d" % (t[3], t[1], t[2]) for t in stack) + return "\n".join("%30s : %s:%d" % (t[3], t[1], t[2]) for t in stack) def dump_stack_frames(limit=None, out=None, skip=0): diff --git a/tests/test_debug.py b/tests/test_debug.py index 7d4c0a165..e54a54e75 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -131,8 +131,8 @@ def test_debug_callers(self): 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 = re_lines(out_lines, r" @\d+", match=False).splitlines() - frame_pattern = r"\s+f1_debug_output : .*tests[/\\]test_debug.py @\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 = re_lines(out_lines, frame_pattern).splitlines() self.assertEqual(len(real_messages), len(frames)) @@ -142,7 +142,7 @@ def test_debug_callers(self): # as the code changes. This test is here to ensure that the debug code # continues working. It's ok to adjust these details over time. self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Adding file tracers: 0 files") - self.assertRegex(last_line, r"\s+add_file_tracers : .*coverage[/\\]sqldata.py @\d+$") + self.assertRegex(last_line, r"\s+add_file_tracers : .*coverage[/\\]sqldata.py:\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) From 6ac3ca707457c62c16470c805fc5fa4e38fd59eb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 1 Sep 2019 17:30:11 -0400 Subject: [PATCH 625/952] Log an operation before starting it --- coverage/sqldata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 5e7edd728..10d4e577c 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -463,9 +463,9 @@ def touch_file(self, filename, plugin_name=""): `plugin_name` is the name of the plugin responsible for this file. It is used to associate the right filereporter, etc. """ - self._start_using() if self._debug.should('dataop'): self._debug.write("Touching %r" % (filename,)) + self._start_using() if not self._has_arcs and not self._has_lines: raise CoverageException("Can't touch files in an empty CoverageData") From ada27a855934588cf753f8712bd72d41eadb1058 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 1 Sep 2019 18:20:32 -0400 Subject: [PATCH 626/952] No need for format indexes (mostly) --- ci/download_appveyor.py | 6 +++--- coverage/control.py | 2 +- coverage/debug.py | 8 ++++---- coverage/files.py | 2 +- coverage/inorout.py | 2 +- coverage/parser.py | 38 +++++++++++++++++++------------------- coverage/python.py | 4 ++-- coverage/pytracer.py | 2 +- coverage/report.py | 2 +- coverage/summary.py | 2 +- igor.py | 2 +- lab/parser.py | 4 ++-- lab/platform_info.py | 4 ++-- perf/perf_measure.py | 2 +- setup.py | 2 +- tests/test_concurrency.py | 6 +++--- tests/test_data.py | 4 ++-- tests/test_html.py | 2 +- tests/test_oddball.py | 2 +- tests/test_plugins.py | 2 +- tests/test_process.py | 6 +++--- tests/test_xml.py | 6 +++--- 22 files changed, 55 insertions(+), 55 deletions(-) diff --git a/ci/download_appveyor.py b/ci/download_appveyor.py index 7cec413ca..a3d814962 100644 --- a/ci/download_appveyor.py +++ b/ci/download_appveyor.py @@ -17,7 +17,7 @@ def make_auth_headers(): token = f.read().strip() headers = { - 'Authorization': 'Bearer {0}'.format(token), + 'Authorization': 'Bearer {}'.format(token), } return headers @@ -50,7 +50,7 @@ def download_latest_artifacts(account_project): for artifact in artifacts: is_zip = artifact['type'] == "Zip" filename = artifact['fileName'] - print(" {0}, {1} bytes".format(filename, artifact['size'])) + print(" {}, {} bytes".format(filename, artifact['size'])) url = make_url( "/buildjobs/{jobid}/artifacts/{filename}", @@ -86,7 +86,7 @@ def unpack_zipfile(filename): with open(filename, 'rb') as fzip: z = zipfile.ZipFile(fzip) for name in z.namelist(): - print(" extracting {0}".format(name)) + print(" extracting {}".format(name)) ensure_dirs(name) z.extract(name) diff --git a/coverage/control.py b/coverage/control.py index 8cba6c38e..d59884566 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -486,7 +486,7 @@ def stop(self): def _atexit(self): """Clean up on process shutdown.""" if self._debug.should("process"): - self._debug.write("atexit: {0!r}".format(self)) + self._debug.write("atexit: {!r}".format(self)) if self._started: self.stop() if self._auto_save: diff --git a/coverage/debug.py b/coverage/debug.py index 29e4977f1..6e6b1df1a 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -101,7 +101,7 @@ def should(self, option): # pylint: disable=unused-argument def info_header(label): """Make a nice header string.""" - return "--{0:-<60s}".format(" "+label+" ") + return "--{:-<60s}".format(" "+label+" ") def info_formatter(info): @@ -174,8 +174,8 @@ def short_id(id64): def add_pid_and_tid(text): """A filter to add pid and tid to debug messages.""" # Thread ids are useful, but too long. Make a shorter one. - tid = "{0:04x}".format(short_id(_thread.get_ident())) - text = "{0:5d}.{1}: {2}".format(os.getpid(), tid, text) + tid = "{:04x}".format(short_id(_thread.get_ident())) + text = "{:5d}.{}: {}".format(os.getpid(), tid, text) return text @@ -241,7 +241,7 @@ def filter(self, text): """Add a cwd message for each new cwd.""" cwd = os.getcwd() if cwd != self.cwd: - text = "cwd is now {0!r}\n".format(cwd) + text + text = "cwd is now {!r}\n".format(cwd) + text self.cwd = cwd return text diff --git a/coverage/files.py b/coverage/files.py index d94959126..dc8c248fe 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -338,7 +338,7 @@ def __init__(self): def pprint(self): # pragma: debugging """Dump the important parts of the PathAliases, for debugging.""" for regex, result in self.aliases: - print("{0!r} --> {1!r}".format(regex.pattern, result)) + print("{!r} --> {!r}".format(regex.pattern, result)) def add(self, pattern, result): """Add the `pattern`/`result` pair to the list of aliases. diff --git a/coverage/inorout.py b/coverage/inorout.py index 3e0613a3d..c31e9206b 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -358,7 +358,7 @@ def warn_already_imported_files(self): disp = self.should_trace(filename) if disp.trace: - msg = "Already imported a file that will be measured: {0}".format(filename) + msg = "Already imported a file that will be measured: {}".format(filename) self.warn(msg, slug="already-imported") warned.add(filename) diff --git a/coverage/parser.py b/coverage/parser.py index c2c58a8c1..12c2d0a5f 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -521,8 +521,8 @@ def __init__(self, text, statements, multiline): if AST_DUMP: # pragma: debugging # Dump the AST so that failing tests have helpful output. - print("Statements: {0}".format(self.statements)) - print("Multiline map: {0}".format(self.multiline)) + print("Statements: {}".format(self.statements)) + print("Multiline map: {}".format(self.multiline)) ast_dump(self.root_node) self.arcs = set() @@ -653,7 +653,7 @@ def add_arcs(self, node): # to see if it's overlooked. if 0: if node_name not in self.OK_TO_DEFAULT: - print("*** Unhandled: {0}".format(node)) + print("*** Unhandled: {}".format(node)) # Default for simple statements: one exit from this node. return set([ArcStart(self.line_for_node(node))]) @@ -823,7 +823,7 @@ def process_raise_exits(self, exits): for xit in exits: self.add_arc( xit.lineno, -block.start, xit.cause, - "didn't except from function '{0}'".format(block.name), + "didn't except from function {!r}".format(block.name), ) break @@ -838,7 +838,7 @@ def process_return_exits(self, exits): for xit in exits: self.add_arc( xit.lineno, -block.start, xit.cause, - "didn't return from function '{0}'".format(block.name), + "didn't return from function {!r}".format(block.name), ) break @@ -1161,17 +1161,17 @@ def _code_object__ClassDef(self, node): for xit in exits: self.add_arc( xit.lineno, -start, xit.cause, - "didn't exit the body of class '{0}'".format(node.name), + "didn't exit the body of class {!r}".format(node.name), ) def _make_oneline_code_method(noun): # pylint: disable=no-self-argument """A function to make methods for online callable _code_object__ methods.""" def _code_object__oneline_callable(self, node): start = self.line_for_node(node) - self.add_arc(-start, start, None, "didn't run the {0} on line {1}".format(noun, start)) + self.add_arc(-start, start, None, "didn't run the {} on line {}".format(noun, start)) self.add_arc( start, -start, None, - "didn't finish the {0} on line {1}".format(noun, start), + "didn't finish the {} on line {}".format(noun, start), ) return _code_object__oneline_callable @@ -1203,15 +1203,15 @@ def ast_dump(node, depth=0): """ indent = " " * depth if not isinstance(node, ast.AST): - print("{0}<{1} {2!r}>".format(indent, node.__class__.__name__, node)) + print("{}<{} {!r}>".format(indent, node.__class__.__name__, node)) return lineno = getattr(node, "lineno", None) if lineno is not None: - linemark = " @ {0}".format(node.lineno) + linemark = " @ {}".format(node.lineno) else: linemark = "" - head = "{0}<{1}{2}".format(indent, node.__class__.__name__, linemark) + head = "{}<{}{}".format(indent, node.__class__.__name__, linemark) named_fields = [ (name, value) @@ -1219,28 +1219,28 @@ def ast_dump(node, depth=0): if name not in SKIP_DUMP_FIELDS ] if not named_fields: - print("{0}>".format(head)) + print("{}>".format(head)) elif len(named_fields) == 1 and _is_simple_value(named_fields[0][1]): field_name, value = named_fields[0] - print("{0} {1}: {2!r}>".format(head, field_name, value)) + print("{} {}: {!r}>".format(head, field_name, value)) else: print(head) if 0: - print("{0}# mro: {1}".format( + print("{}# mro: {}".format( indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]), )) next_indent = indent + " " for field_name, value in named_fields: - prefix = "{0}{1}:".format(next_indent, field_name) + prefix = "{}{}:".format(next_indent, field_name) if _is_simple_value(value): - print("{0} {1!r}".format(prefix, value)) + print("{} {!r}".format(prefix, value)) elif isinstance(value, list): - print("{0} [".format(prefix)) + print("{} [".format(prefix)) for n in value: ast_dump(n, depth + 8) - print("{0}]".format(next_indent)) + print("{}]".format(next_indent)) else: print(prefix) ast_dump(value, depth + 8) - print("{0}>".format(indent)) + print("{}>".format(indent)) diff --git a/coverage/python.py b/coverage/python.py index 31db1a272..ed467e61b 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -136,7 +136,7 @@ def source_for_morf(morf): elif isinstance(morf, types.ModuleType): # A module should have had .__file__, otherwise we can't use it. # This could be a PEP-420 namespace package. - raise CoverageException("Module {0} has no file".format(morf)) + raise CoverageException("Module {} has no file".format(morf)) else: filename = morf @@ -170,7 +170,7 @@ def __init__(self, morf, coverage=None): self._excluded = None def __repr__(self): - return "".format(self.filename) + return "".format(self.filename) @contract(returns='unicode') def relative_filename(self): diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 4ffe41e3c..e64d7f55a 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -60,7 +60,7 @@ def __init__(self): atexit.register(setattr, self, 'in_atexit', True) def __repr__(self): - return "".format( + return "".format( id(self), sum(len(v) for v in self.data.values()), len(self.data), diff --git a/coverage/report.py b/coverage/report.py index d24dddc88..f58de8d23 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -78,7 +78,7 @@ def get_analysis_to_report(coverage, morfs): # should_be_python() method. if fr.should_be_python(): if config.ignore_errors: - msg = "Could not parse Python file {0}".format(fr.filename) + msg = "Could not parse Python file {!r}".format(fr.filename) coverage._warn(msg, slug="couldnt-parse") else: raise diff --git a/coverage/summary.py b/coverage/summary.py index 72d210335..08c8a947a 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -104,7 +104,7 @@ def report(self, morfs, outfile=None): if getattr(self.config, 'sort', None): position = column_order.get(self.config.sort.lower()) if position is None: - raise CoverageException("Invalid sorting option: {0!r}".format(self.config.sort)) + raise CoverageException("Invalid sorting option: {!r}".format(self.config.sort)) lines.sort(key=lambda l: (l[1][position], l[0])) for line in lines: diff --git a/igor.py b/igor.py index 1bb7d19ff..5d7828c32 100644 --- a/igor.py +++ b/igor.py @@ -218,7 +218,7 @@ def do_zip_mods(): (u'cp1252', u'“hi”'), ] for encoding, text in details: - filename = 'encoded_{0}.py'.format(encoding) + filename = 'encoded_{}.py'.format(encoding) ords = [ord(c) for c in text] source_text = source.format(encoding=encoding, text=text, ords=ords) zf.writestr(filename, source_text.encode(encoding)) diff --git a/lab/parser.py b/lab/parser.py index b3560506f..bf203189b 100644 --- a/lab/parser.py +++ b/lab/parser.py @@ -65,9 +65,9 @@ def main(self, args): if options.histogram: total = sum(opcode_counts.values()) - print("{0} total opcodes".format(total)) + print("{} total opcodes".format(total)) for opcode, number in opcode_counts.most_common(): - print("{0:20s} {1:6d} {2:.1%}".format(opcode, number, number/total)) + print("{:20s} {:6d} {:.1%}".format(opcode, number, number/total)) def one_file(self, options, filename): """Process just one file.""" diff --git a/lab/platform_info.py b/lab/platform_info.py index 61e02dd26..7ddde47a5 100644 --- a/lab/platform_info.py +++ b/lab/platform_info.py @@ -15,11 +15,11 @@ def whatever(f): def dump_module(mod): - print("\n### {0} ---------------------------".format(mod.__name__)) + print("\n### {} ---------------------------".format(mod.__name__)) for name in dir(mod): if name.startswith("_"): continue - print("{0:30s}: {1!r:.100}".format(name, whatever(getattr(mod, name)))) + print("{:30s}: {!r:.100}".format(name, whatever(getattr(mod, name)))) for mod in [platform, sys]: diff --git a/perf/perf_measure.py b/perf/perf_measure.py index a8f2ffaa0..b903567cc 100644 --- a/perf/perf_measure.py +++ b/perf/perf_measure.py @@ -137,7 +137,7 @@ def operations(thing): yield kwargs['file_count'] * kwargs['call_count'] * kwargs['line_count'] ops = sum(sum(operations(thing)) for thing in ["file", "call", "line"]) - print("{0:.1f}M operations".format(ops/1e6)) + print("{:.1f}M operations".format(ops/1e6)) def check_coefficients(self): # For checking the calculation of actual stats: diff --git a/setup.py b/setup.py index a964412c1..b1d086be5 100644 --- a/setup.py +++ b/setup.py @@ -98,7 +98,7 @@ # We need to get HTML assets from our htmlfiles directory. zip_safe=False, - author='Ned Batchelder and {0} others'.format(num_others), + author='Ned Batchelder and {} others'.format(num_others), author_email='ned@nedbatchelder.com', description=doc, long_description=long_description, diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index bcf619422..9d777afe2 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -69,7 +69,7 @@ def line_count(s): def print_simple_annotation(code, linenos): """Print the lines in `code` with X for each line number in `linenos`.""" for lineno, line in enumerate(code.splitlines(), start=1): - print(" {0} {1}".format("X" if lineno in linenos else " ", line)) + print(" {} {}".format("X" if lineno in linenos else " ", line)) class LineCountTest(CoverageTest): @@ -242,7 +242,7 @@ def try_some_code(self, code, concurrency, the_module, expected_out=None): # If the test fails, it's helpful to see this info: fname = abs_file("try_it.py") linenos = data.lines(fname) - print("{0}: {1}".format(len(linenos), linenos)) + print("{}: {}".format(len(linenos), linenos)) print_simple_annotation(code, linenos) lines = line_count(code) @@ -500,7 +500,7 @@ def test_thread_safe_save_data(tmpdir): # Create some Python modules and put them in the path modules_dir = tmpdir.mkdir('test_modules') - module_names = ["m{0:03d}".format(i) for i in range(1000)] + module_names = ["m{:03d}".format(i) for i in range(1000)] for module_name in module_names: modules_dir.join(module_name + ".py").write("def f(): pass\n") diff --git a/tests/test_data.py b/tests/test_data.py index ff97b3309..e09aaf447 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -556,7 +556,7 @@ def test_read_sql_errors(self): with sqlite3.connect("wrong_schema.db") as con: con.execute("create table coverage_schema (version integer)") con.execute("insert into coverage_schema (version) values (99)") - msg = r"Couldn't .* '.*[/\\]{0}': wrong schema: 99 instead of \d+".format("wrong_schema.db") + msg = r"Couldn't .* '.*[/\\]{}': wrong schema: 99 instead of \d+".format("wrong_schema.db") with self.assertRaisesRegex(CoverageException, msg): covdata = CoverageData("wrong_schema.db") covdata.read() @@ -564,7 +564,7 @@ def test_read_sql_errors(self): with sqlite3.connect("no_schema.db") as con: con.execute("create table foobar (baz text)") - msg = r"Couldn't .* '.*[/\\]{0}': \S+".format("no_schema.db") + msg = r"Couldn't .* '.*[/\\]{}': \S+".format("no_schema.db") with self.assertRaisesRegex(CoverageException, msg): covdata = CoverageData("no_schema.db") covdata.read() diff --git a/tests/test_html.py b/tests/test_html.py index 3e5671133..d30279a5f 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -87,7 +87,7 @@ def assert_correct_timestamp(self, html): self.assert_recent_datetime( timestamp, seconds=120, - msg="Timestamp is wrong: {0}".format(timestamp), + msg="Timestamp is wrong: {}".format(timestamp), ) diff --git a/tests/test_oddball.py b/tests/test_oddball.py index 1087ea57d..3574806c3 100644 --- a/tests/test_oddball.py +++ b/tests/test_oddball.py @@ -541,7 +541,7 @@ def test_correct_filename(self): # but now it's fixed. :) self.make_file("to_exec.py", """\ \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n - print("var is {0}".format(var)) # line 31 + print("var is {}".format(var)) # line 31 """) self.make_file("main.py", """\ namespace = {'var': 17} diff --git a/tests/test_plugins.py b/tests/test_plugins.py index a4c7a5ee8..416f05a2e 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -321,7 +321,7 @@ def render(filename, linenum): # will examine the `filename` and `linenum` locals to # determine the source file and line number. fiddle_around = 1 # not used, just chaff. - return "[{0} @ {1}]".format(filename, linenum) + return "[{} @ {}]".format(filename, linenum) def helper(x): # This function is here just to show that not all code in diff --git a/tests/test_process.py b/tests/test_process.py index 000330ad7..be7ae446e 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -164,7 +164,7 @@ def test_combine_no_usable_files(self): self.assertEqual(status, 1) for n in "12": - self.assert_exists(".coverage.bad{0}".format(n)) + self.assert_exists(".coverage.bad{}".format(n)) warning_regex = ( r"(" # JSON message: r"Coverage.py warning: Couldn't read data from '.*\.coverage\.bad{0}': " @@ -1345,7 +1345,7 @@ def possible_pth_dirs(): 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)) + try_it = os.path.join(pth_dir, "touch_{}.it".format(WORKER)) with open(try_it, "w") as f: try: f.write("foo") @@ -1370,7 +1370,7 @@ def setUp(self): # Create the .pth file. self.assertTrue(PTH_DIR) pth_contents = "import coverage; coverage.process_startup()\n" - pth_path = os.path.join(PTH_DIR, "subcover_{0}.pth".format(WORKER)) + pth_path = os.path.join(PTH_DIR, "subcover_{}.pth".format(WORKER)) with open(pth_path, "w") as pth: pth.write(pth_contents) self.pth_path = pth_path diff --git a/tests/test_xml.py b/tests/test_xml.py index 09ab2f850..658a6ab62 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -51,13 +51,13 @@ def here(p): return os.path.join(curdir, p) for i in range(width): - next_dir = here("d{0}".format(i)) + next_dir = here("d{}".format(i)) self.make_tree(width, depth-1, next_dir) if curdir != ".": self.make_file(here("__init__.py"), "") for i in range(width): - filename = here("f{0}.py".format(i)) - self.make_file(filename, "# {0}\n".format(filename)) + filename = here("f{}.py".format(i)) + self.make_file(filename, "# {}\n".format(filename)) def assert_source(self, xmldom, src): """Assert that the XML has a element with `src`.""" From 4042ce713e373f19ca81948eefcfab83d8dd0bbc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 1 Sep 2019 18:52:41 -0400 Subject: [PATCH 627/952] Updated Tidelift badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7c38ad0f0..9382bf90e 100644 --- a/README.rst +++ b/README.rst @@ -126,7 +126,7 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. .. |repos| image:: https://repology.org/badge/tiny-repos/python:coverage.svg :target: https://repology.org/metapackage/python:coverage/versions :alt: Packaging status -.. |tidelift| image:: https://tidelift.com/badges/github/nedbat/coveragepy +.. |tidelift| image:: https://tidelift.com/badges/package/pypi/coverage :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme :alt: Tidelift .. |stars| image:: https://img.shields.io/github/stars/nedbat/coveragepy.svg?logo=github From dd98bb1ebb687e4ff22e255ee97ec8e8cca5e1f6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 1 Sep 2019 21:34:20 -0400 Subject: [PATCH 628/952] Reporting methods shouldn't permanently change the configuration --- CHANGES.rst | 4 ++++ coverage/config.py | 7 +++++- coverage/control.py | 53 ++++++++++++++++++++++++++++++--------------- tests/test_api.py | 11 ++++++++++ 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5d0ff1266..ec79353fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -28,6 +28,10 @@ Unreleased "numbits." The :mod:`coverage.numbits` module provides functions for working with them. +- The reporting methods used to permanently apply their arguments to the + configuration of the Coverage object. Now they no longer do. The arguments + affect the operation of the method, but do not persist. + - A class named "test_something" no longer confuses the ``test_function`` dynamic context setting. Fixes `issue 829`_. diff --git a/coverage/config.py b/coverage/config.py index 516fe1b9f..85493df13 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -4,6 +4,7 @@ """Config file for coverage.py""" import collections +import copy import os import re @@ -215,7 +216,7 @@ def __init__(self): self.xml_output = "coverage.xml" self.xml_package_depth = 99 - # Defaults for [JSON] + # Defaults for [json] self.json_output = "coverage.json" self.json_pretty_print = False self.json_show_contexts = False @@ -318,6 +319,10 @@ def from_file(self, filename, our_file): return used + def copy(self): + """Return a copy of the configuration.""" + return copy.deepcopy(self) + CONFIG_FILE_OPTIONS = [ # These are *args for _set_attr_from_config_option: # (attr, where, type_="") diff --git a/coverage/control.py b/coverage/control.py index d59884566..c342eca47 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -4,6 +4,7 @@ """Core control stuff for coverage.py.""" import atexit +import contextlib import os import os.path import platform @@ -41,6 +42,21 @@ os = isolate_module(os) +@contextlib.contextmanager +def override_config(cov, **kwargs): + """Temporarily tweak the configuration of `cov`. + + The arguments are applied to `cov.config` with the `from_args` method. + At the end of the with-statement, the old configuration is restored. + """ + original_config = cov.config + cov.config = cov.config.copy() + try: + cov.config.from_args(**kwargs) + yield + finally: + cov.config = original_config + class Coverage(object): """Programmatic access to coverage.py. @@ -781,13 +797,14 @@ def report( Returns a float, the total percentage covered. """ - self.config.from_args( + with override_config( + self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, show_missing=show_missing, skip_covered=skip_covered, report_contexts=contexts, - ) - reporter = SummaryReporter(self) - return reporter.report(morfs, outfile=file) + ): + reporter = SummaryReporter(self) + return reporter.report(morfs, outfile=file) def annotate( self, morfs=None, directory=None, ignore_errors=None, @@ -803,12 +820,12 @@ def annotate( See :meth:`report` for other arguments. """ - self.config.from_args( + with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, report_contexts=contexts, - ) - reporter = AnnotateReporter(self) - reporter.report(morfs, directory=directory) + ): + reporter = AnnotateReporter(self) + reporter.report(morfs, directory=directory) def html_report(self, morfs=None, directory=None, ignore_errors=None, omit=None, include=None, extra_css=None, title=None, @@ -836,13 +853,13 @@ def html_report(self, morfs=None, directory=None, ignore_errors=None, changing the files in the report folder. """ - self.config.from_args( + with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, html_dir=directory, extra_css=extra_css, html_title=title, skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, - ) - reporter = HtmlReporter(self) - return reporter.report(morfs) + ): + reporter = HtmlReporter(self) + return reporter.report(morfs) def xml_report( self, morfs=None, outfile=None, ignore_errors=None, @@ -860,11 +877,11 @@ def xml_report( Returns a float, the total percentage covered. """ - self.config.from_args( + with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, xml_output=outfile, report_contexts=contexts, - ) - return render_report(self.config.xml_output, XmlReporter(self), morfs) + ): + return render_report(self.config.xml_output, XmlReporter(self), morfs) def json_report( self, morfs=None, outfile=None, ignore_errors=None, @@ -881,12 +898,12 @@ def json_report( Returns a float, the total percentage covered. """ - self.config.from_args( + with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, json_show_contexts=show_contexts - ) - return render_report(self.config.json_output, JsonReporter(self), morfs) + ): + return render_report(self.config.json_output, JsonReporter(self), morfs) def sys_info(self): """Return a list of (key, value) pairs showing internal information.""" diff --git a/tests/test_api.py b/tests/test_api.py index 301257dc4..4f2484412 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -918,3 +918,14 @@ def test_pytestcov_parallel(self): def test_pytestcov_parallel_append(self): self.pretend_to_be_pytestcov(append=True) + + +class ImmutableConfigTest(CoverageTest): + """Check that reporting methods don't permanently change the configuration.""" + def test_config_doesnt_change(self): + self.make_file("simple.py", "a = 1") + cov = coverage.Coverage() + self.start_import_stop(cov, "simple") + self.assertEqual(cov.get_option("report:show_missing"), False) + cov.report(show_missing=True) + self.assertEqual(cov.get_option("report:show_missing"), False) From 6099f8270630f880cbf169396c9435949fe4aa01 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 2 Sep 2019 09:36:55 -0400 Subject: [PATCH 629/952] cwd logging didn't have pid/tid info on it --- coverage/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/debug.py b/coverage/debug.py index 6e6b1df1a..9dded1d97 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -254,7 +254,7 @@ def __init__(self, outfile, show_process, filters): self.filters = list(filters) if self.show_process: - self.filters.append(CwdTracker().filter) + self.filters.insert(0, CwdTracker().filter) self.write("New process: executable: %r\n" % (sys.executable,)) self.write("New process: cmd: %r\n" % (getattr(sys, 'argv', None),)) if hasattr(os, 'getppid'): From 1c8e6daef332f70095796f7788dc0f0e9ce1484b Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 2 Sep 2019 09:37:36 -0400 Subject: [PATCH 630/952] Logging executemany failed if data was a generator --- coverage/sqldata.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 10d4e577c..400b7f04e 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -881,6 +881,7 @@ def execute(self, sql, parameters=()): def executemany(self, sql, data): if self.debug: + data = list(data) self.debug.write("Executing many {!r} with {} rows".format(sql, len(data))) return self.con.executemany(sql, data) From fc610d1932b0ab492238e41f447213ca1235e027 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 2 Sep 2019 09:38:20 -0400 Subject: [PATCH 631/952] Log some indication of the script being executed --- coverage/backward.py | 5 +++++ coverage/debug.py | 9 ++++++++- coverage/sqldata.py | 8 +++++--- tests/test_debug.py | 9 +++++++++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/coverage/backward.py b/coverage/backward.py index 34ab2f1aa..587595453 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -64,6 +64,11 @@ # in Python versions earlier than 3.3. from pipes import quote as shlex_quote +try: + import reprlib +except ImportError: + import repr as reprlib + # A function to iterate listlessly over a dict's items, and one to get the # items as a list. try: diff --git a/coverage/debug.py b/coverage/debug.py index 9dded1d97..de5d93195 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -15,7 +15,7 @@ except ImportError: import thread as _thread -from coverage.backward import StringIO +from coverage.backward import reprlib, StringIO from coverage.misc import isolate_module os = isolate_module(os) @@ -163,6 +163,13 @@ def dump_stack_frames(limit=None, out=None, skip=0): out.write("\n") +def clipped_repr(text, numchars=50): + """`repr(text)`, but limited to `numchars`.""" + r = reprlib.Repr() + r.maxstring = numchars + return r.repr(text) + + def short_id(id64): """Given a 64-bit id, make a shorter 16-bit one.""" id16 = 0 diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 400b7f04e..3142ee3f8 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -19,7 +19,7 @@ import zlib from coverage.backward import get_thread_id, iitems, to_bytes, to_string -from coverage.debug import NoDebugging, SimpleReprMixin +from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr from coverage.files import PathAliases from coverage.misc import CoverageException, file_be_gone, filename_suffix, isolate_module from coverage.misc import contract @@ -42,7 +42,7 @@ # 6: Key-value in meta. # 7: line_map -> line_bits -SCHEMA = """ +SCHEMA = """\ CREATE TABLE coverage_schema ( -- One row, to record the version of the schema in this db. version integer @@ -887,7 +887,9 @@ def executemany(self, sql, data): def executescript(self, script): if self.debug: - self.debug.write("Executing script with {} chars".format(len(script))) + self.debug.write("Executing script with {} chars: {}".format( + len(script), clipped_repr(script, 100), + )) self.con.executescript(script) def dump(self): diff --git a/tests/test_debug.py b/tests/test_debug.py index e54a54e75..e3b929f63 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -10,6 +10,7 @@ import coverage from coverage.backward import StringIO from coverage.debug import filter_text, info_formatter, info_header, short_id, short_stack +from coverage.debug import clipped_repr from coverage.env import C_TRACER from tests.coveragetest import CoverageTest @@ -61,6 +62,14 @@ def test_short_id(id64, id16): assert short_id(id64) == id16 +@pytest.mark.parametrize("text, numchars, result", [ + ("hello", 10, "'hello'"), + ("0123456789abcdefghijklmnopqrstuvwxyz", 15, "'01234...vwxyz'"), +]) +def test_clipped_repr(text, numchars, result): + assert clipped_repr(text, numchars) == result + + @pytest.mark.parametrize("text, filters, result", [ ("hello", [], "hello"), ("hello\n", [], "hello\n"), From 49958ac11fe3b441db2a6c1c691a7abdbf9cdd08 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 2 Sep 2019 09:38:40 -0400 Subject: [PATCH 632/952] 'sql' is a new debug option --- doc/cmd.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/cmd.rst b/doc/cmd.rst index 1aca89a88..25a9dd20e 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -538,6 +538,8 @@ to log: * ``self``: annotate each debug message with the object printing the message. +* ``sql``: log the SQL statements used for recording data. + * ``sys``: before starting, dump all the system and environment information, as with :ref:`coverage debug sys `. From b168b68a641e32b1114d4a5a668b08a01e48c8e6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 3 Sep 2019 13:34:39 -0400 Subject: [PATCH 633/952] --debug=self now goes on a second line for better readability --- coverage/debug.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/debug.py b/coverage/debug.py index de5d93195..c5fee6835 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -72,11 +72,11 @@ def write(self, msg): `msg` is the line to write. A newline will be appended. """ + self.output.write(msg+"\n") if self.should('self'): caller_self = inspect.stack()[1][0].f_locals.get('self') if caller_self is not None: - msg = "[self: {!r}] {}".format(caller_self, msg) - self.output.write(msg+"\n") + self.output.write("self: {!r}\n".format(caller_self)) if self.should('callers'): dump_stack_frames(out=self.output, skip=1) self.output.flush() From 48d95f9510328769faa174f569548f5c63513d95 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 3 Sep 2019 13:35:37 -0400 Subject: [PATCH 634/952] Need to set a sqlite attribute on a real sqlite connection --- coverage/sqldata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 3142ee3f8..babfcb8dd 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -537,7 +537,7 @@ def update(self, other_data, aliases=None): cur.close() with self._connect() as conn: - conn.isolation_level = 'IMMEDIATE' + conn.con.isolation_level = 'IMMEDIATE' # Get all tracers in the DB. Files not in the tracers are assumed # to have an empty string tracer. Since Sqlite does not support From d9bc47d0db97ea47ebbe1b775947049792e8aa00 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 3 Sep 2019 13:36:10 -0400 Subject: [PATCH 635/952] Updating with an empty data is ok With no arc or line data, it used to choose lines arbitrarily, which would fail if an empty data was updating an arc data. --- coverage/sqldata.py | 40 ++++++++++++++++++++++++---------------- tests/test_data.py | 16 ++++++++++++++++ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index babfcb8dd..06ccff521 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -411,6 +411,8 @@ def add_arcs(self, arc_data): ) def _choose_lines_or_arcs(self, lines=False, arcs=False): + assert lines or arcs + assert not (lines and arcs) if lines and self._has_arcs: raise CoverageException("Can't add lines to existing arc data") if arcs and self._has_lines: @@ -480,6 +482,8 @@ def update(self, other_data, aliases=None): If `aliases` is provided, it's a `PathAliases` object that is used to re-map paths to match the local machine's. """ + if self._debug.should('dataop'): + self._debug.write("Updating with data from %r" % (getattr(other_data, '_filename', '???'),)) if self._has_lines and other_data._has_arcs: raise CoverageException("Can't combine arc data with line data") if self._has_arcs and other_data._has_lines: @@ -608,23 +612,27 @@ def update(self, other_data, aliases=None): lines[key] = numbits cur.close() - self._choose_lines_or_arcs(arcs=bool(arcs), lines=bool(lines)) + if arcs: + self._choose_lines_or_arcs(arcs=True) - # Write the combined data. - conn.executemany( - 'insert or ignore into arc ' - '(file_id, context_id, fromno, tono) values (?, ?, ?, ?)', - arc_rows - ) - conn.execute("delete from line_bits") - conn.executemany( - "insert into line_bits " - "(file_id, context_id, numbits) values (?, ?, ?)", - [ - (file_ids[file], context_ids[context], numbits) - for (file, context), numbits in lines.items() - ] - ) + # Write the combined data. + conn.executemany( + 'insert or ignore into arc ' + '(file_id, context_id, fromno, tono) values (?, ?, ?, ?)', + arc_rows + ) + + if lines: + self._choose_lines_or_arcs(lines=True) + conn.execute("delete from line_bits") + conn.executemany( + "insert into line_bits " + "(file_id, context_id, numbits) values (?, ?, ?)", + [ + (file_ids[file], context_ids[context], numbits) + for (file, context), numbits in lines.items() + ] + ) conn.executemany( 'insert or ignore into tracer (file_id, tracer) values (?, ?)', ((file_ids[filename], tracer) for filename, tracer in tracer_map.items()) diff --git a/tests/test_data.py b/tests/test_data.py index e09aaf447..eb389e353 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -428,6 +428,22 @@ def test_update_file_tracer_vs_no_file_tracer(self): with self.assertRaisesRegex(CoverageException, msg): covdata2.update(covdata1) + def test_update_lines_empty(self): + covdata1 = CoverageData(suffix='1') + covdata1.add_lines(LINES_1) + + covdata2 = CoverageData(suffix='2') + covdata1.update(covdata2) + self.assert_line_counts(covdata1, SUMMARY_1) + + def test_update_arcs_empty(self): + covdata1 = CoverageData(suffix='1') + covdata1.add_arcs(ARCS_3) + + covdata2 = CoverageData(suffix='2') + covdata1.update(covdata2) + self.assert_line_counts(covdata1, SUMMARY_3) + def test_asking_isnt_measuring(self): # Asking about an unmeasured file shouldn't make it seem measured. covdata = CoverageData() From 0b27364b24aabbaf52dff3ad4842ec07971f9adb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 6 Sep 2019 13:54:10 -0400 Subject: [PATCH 636/952] Clarify some docs --- doc/cmd.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/cmd.rst b/doc/cmd.rst index 25a9dd20e..bffa147c9 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -92,6 +92,12 @@ Python ``-m`` switch:: $ coverage run -m packagename.modulename arg1 arg2 blah blah ..your program's output.. blah blah +.. note:: + + In most cases, the program to use here is a test runner, not your program + you are trying to measure. The test runner will run your tests and coverage + will measure the coverage of your code along the way. + If you want :ref:`branch coverage ` measurement, use the ``--branch`` flag. Otherwise only statement coverage is measured. From 692a0fc2ad8bbd8a3c4009460783e49e9558605a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 9 Sep 2019 05:38:03 -0400 Subject: [PATCH 637/952] A little more in the db schema docs --- Makefile | 1 + coverage/numbits.py | 7 ++++--- doc/dbschema.rst | 12 ++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 996b7d4f4..b0f3ccf1e 100644 --- a/Makefile +++ b/Makefile @@ -124,6 +124,7 @@ docreqs: tox -q -e doc --notest dochtml: docreqs + .tox/doc/bin/python doc/check_copied_from.py doc/*.rst $(SPHINXBUILD) -b html doc doc/_build/html @echo @echo "Build finished. The HTML pages are in doc/_build/html." diff --git a/coverage/numbits.py b/coverage/numbits.py index 4b340c8e0..504452d77 100644 --- a/coverage/numbits.py +++ b/coverage/numbits.py @@ -150,10 +150,11 @@ def register_sqlite_functions(connection): conn = sqlite3.connect('example.db') register_sqlite_functions(conn) c = conn.cursor() + # Kind of a nonsense query: find all the files and contexts that + # executed line 47 in any file: c.execute( - "select lb.file_id, lb.context_id from line_bits lb" - "where num_in_numbits(?, lb.numbits)", - (interesting_line_number,) + "select file_id, context_id from line_bits where num_in_numbits(?, numbits)", + (47,) ) """ connection.create_function("numbits_union", 2, numbits_union) diff --git a/doc/dbschema.rst b/doc/dbschema.rst index c57de92ec..34f4a92e7 100644 --- a/doc/dbschema.rst +++ b/doc/dbschema.rst @@ -14,6 +14,18 @@ For most needs, the :class:`.CoverageData` API will be sufficient, and should be preferred to accessing the database directly. Only advanced uses will need to use the database. +The schema can change without changing the major version of coverage.py, so be +careful when accessing the database directly. The `coverage_schema` table has +the schema number of the database. The schema described here corresponds to: + +.. copied_from: coverage/sqldata.py + +.. code:: + + SCHEMA_VERSION = 7 + +.. end_copied_from + You can use SQLite tools such as the :mod:`sqlite3 ` module in the Python standard library to access the data. Some data is stored in a packed format that will need custom functions to access. See From c49e739380095a6939ab135626dbedf166204d58 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 9 Sep 2019 05:47:04 -0400 Subject: [PATCH 638/952] Anchors for each config setting. #842 --- doc/config.rst | 79 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/doc/config.rst b/doc/config.rst index 43d5a27e4..e332f50aa 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -7,17 +7,6 @@ Configuration reference ======================= -.. :history: 20100223T201600, new for 3.3 -.. :history: 20100725T211700, updated for 3.4. -.. :history: 20100824T092900, added ``precision``. -.. :history: 20110604T184400, updated for 3.5. -.. :history: 20110827T212700, updated for 3.5.1 -.. :history: 20130926T222300, updated for 3.6.1 -.. :history: 20140925T064700, updated for 4.0a1 -.. :history: 20150124T173400, updated for 4.0a4 -.. :history: 20150802T174600, updated for 4.0b1 - - Coverage.py options can be specified in a configuration file. This makes it easier to re-run coverage.py with consistent settings, and also allows for specification of options that are otherwise only available in the @@ -111,9 +100,13 @@ Here's a sample configuration file:: These values are generally used when running product code, though some apply to more than one command. +.. _config_run_branch: + ``branch`` (boolean, default False): whether to measure :ref:`branch coverage ` in addition to statement coverage. +.. _config_run_command_line: + ``command_line`` (string): the command-line to run your program. This will be used if you run ``coverage run`` with no further arguments. Coverage.py options cannot be specified here, other than ``-m`` to indicate the module to @@ -121,6 +114,8 @@ run. .. versionadded:: 5.0 +.. _config_run_concurrency: + ``concurrency`` (multi-string, default "thread"): the name concurrency libraries in use by the product code. If your program uses `multiprocessing`_, `gevent`_, `greenlet`_, or `eventlet`_, you must name that library in this @@ -135,14 +130,20 @@ Before version 4.2, this option only accepted a single string. .. versionadded:: 4.0 +.. _config_run_context: + ``context`` (string): the static context to record for this coverage run. See :ref:`contexts` for more information .. versionadded:: 5.0 +.. _config_run_cover_pylib: + ``cover_pylib`` (boolean, default False): whether to measure the Python standard library. +.. _config_run_data_file: + ``data_file`` (string, default ".coverage"): the name of the data file to use for storing or reporting coverage. This value can include a path to another directory. @@ -153,31 +154,47 @@ directory. that can be disabled include a short string at the end, the name of the warning. See :ref:`cmd_warnings` for specific warnings. +.. _config_run_debug: + ``debug`` (multi-string): a list of debug options. See :ref:`the run --debug option ` for details. +.. _config_run_include: + ``include`` (multi-string): a list of file name patterns, the files to include in measurement or reporting. Ignored if ``source`` is set. See :ref:`source` for details. +.. _config_run_note: + ``note`` (string): an arbitrary string that will be written to the data file. You can use the :meth:`.CoverageData.run_infos` method to retrieve this string from a data file. +.. _config_run_omit: + ``omit`` (multi-string): a list of file name patterns, the files to leave out of measurement or reporting. See :ref:`source` for details. +.. _config_run_parallel: + ``parallel`` (boolean, default False): append the machine name, process id and random number to the data file name to simplify collecting data from many processes. See :ref:`cmd_combining` for more information. +.. _config_run_plugins: + ``plugins`` (multi-string): a list of plugin package names. See :ref:`plugins` for more information. +.. _config_run_source: + ``source`` (multi-string): a list of packages or directories, the source to measure during execution. If set, ``include`` is ignored. See :ref:`source` for details. +.. _config_run_timid: + ``timid`` (boolean, default False): use a simpler but slower trace method. This uses PyTracer instead of CTracer, and is only needed in very unusual circumstances. Try this if you get seemingly impossible results. @@ -221,44 +238,64 @@ See :ref:`cmd_combining` for more information. Values common to many kinds of reporting. +.. _config_report_exclude_lines: + ``exclude_lines`` (multi-string): a list of regular expressions. Any line of your source code that matches one of these regexes is excluded from being reported as missing. More details are in :ref:`excluding`. If you use this option, you are replacing all the exclude regexes, so you'll need to also supply the "pragma: no cover" regex if you still want to use it. +.. _config_report_fail_under: + ``fail_under`` (float): a target coverage percentage. If the total coverage measurement is under this value, then exit with a status code of 2. If you specify a non-integral value, you must also set ``[report] precision`` properly to make use of the decimal places. A setting of 100 will fail any value under 100, regardless of the number of decimal places of precision. +.. _config_report_ignore_errors: + ``ignore_errors`` (boolean, default False): ignore source code that can't be found, emitting a warning instead of an exception. +.. _config_report_include: + ``include`` (multi-string): a list of file name patterns, the files to include in reporting. See :ref:`source` for details. +.. _config_report_omit: + ``omit`` (multi-string): a list of file name patterns, the files to leave out of reporting. See :ref:`source` for details. +.. _config_report_partial_branches: + ``partial_branches`` (multi-string): a list of regular expressions. Any line of code that matches one of these regexes is excused from being reported as a partial branch. More details are in :ref:`branch`. If you use this option, you are replacing all the partial branch regexes so you'll need to also supply the "pragma: no branch" regex if you still want to use it. +.. _config_report_precision: + ``precision`` (integer): the number of digits after the decimal point to display for reported coverage percentages. The default is 0, displaying for example "87%". A value of 2 will display percentages like "87.32%". This setting also affects the interpretation of the ``fail_under`` setting. +.. _config_report_show_missing: + ``show_missing`` (boolean, default False): when running a summary report, show missing lines. See :ref:`cmd_summary` for more information. +.. _config_report_skip_covered: + ``skip_covered`` (boolean, default False): Don't include files in the report that are 100% covered files. See :ref:`cmd_summary` for more information. +.. _config_report_sort: + ``sort`` (string, default "Name"): Sort the text report by the named column. Allowed values are "Name", "Stmts", "Miss", "Branch", "BrPart", or "Cover". @@ -271,18 +308,26 @@ Allowed values are "Name", "Stmts", "Miss", "Branch", "BrPart", or "Cover". Values particular to HTML reporting. The values in the ``[report]`` section also apply to HTML output, where appropriate. +.. _config_html_directory: + ``directory`` (string, default "htmlcov"): where to write the HTML report files. +.. _config_html_show_context: + ``show_contexts`` (boolean): should the HTML report include an indication on each line of which contexts executed the line. See :ref:`dynamic_contexts` for details. +.. _config_html_extra_css: + ``extra_css`` (string): the path to a file of CSS to apply to the HTML report. The file will be copied into the HTML output directory. Don't name it "style.css". This CSS is in addition to the CSS normally used, though you can overwrite as many of the rules as you like. +.. _config_html_title: + ``title`` (string, default "Coverage report"): the title to use for the report. Note this is text, not HTML. @@ -295,8 +340,12 @@ Note this is text, not HTML. Values particular to XML reporting. The values in the ``[report]`` section also apply to XML output, where appropriate. +.. _config_xml_output: + ``output`` (string, default "coverage.xml"): where to write the XML report. +.. _config_xml_package_depth: + ``package_depth`` (integer, default 99): controls which directories are identified as packages in the report. Directories deeper than this depth are not reported as packages. The default is that all directories are reported as @@ -311,12 +360,20 @@ packages. Values particular to JSON reporting. The values in the ``[report]`` section also apply to JSON output, where appropriate. +.. versionadded:: 5.0 + +.. _config_json_output: + ``output`` (string, default "coverage.json"): where to write the JSON file. +.. _config_json_pretty_print: + ``pretty_print`` (boolean, default false): controls if the JSON is outputted with whitespace formatted for human consumption (True) or for minimum file size (False). +.. _config_json_show_contexts: + ``show_contexts`` (boolean, default false): should the JSON report include an indication of which contexts executed each line. See :ref:`dynamic_contexts` for details. From d3c36ea874de1cb902fbab9caa30b118d268b6c1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 9 Sep 2019 07:25:59 -0400 Subject: [PATCH 639/952] Document the module-level stuff. #837 --- coverage/misc.py | 2 +- doc/api.rst | 1 + doc/api_coverage.rst | 10 +--------- doc/api_module.rst | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 42 insertions(+), 10 deletions(-) create mode 100644 doc/api_module.rst diff --git a/coverage/misc.py b/coverage/misc.py index 00e88fdb8..3423170f9 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -340,7 +340,7 @@ class BaseCoverageException(Exception): class CoverageException(BaseCoverageException): - """A run-of-the-mill exception specific to coverage.py.""" + """An exception raised by a coverage.py function.""" pass diff --git a/doc/api.rst b/doc/api.rst index eb63719ce..6da5d4cb8 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -49,6 +49,7 @@ only. :ref:`dbschema` explains more. :maxdepth: 1 api_coverage + api_module api_plugin api_coveragedata dbschema diff --git a/doc/api_coverage.rst b/doc/api_coverage.rst index 334336050..0a0e056f2 100644 --- a/doc/api_coverage.rst +++ b/doc/api_coverage.rst @@ -9,17 +9,9 @@ The Coverage class .. :history: 20150802T174800, new file for 4.0b1 .. module:: coverage + :noindex: .. autoclass:: Coverage :members: :exclude-members: sys_info :special-members: __init__ - - -Starting coverage.py automatically -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This function is used to start coverage measurement automatically when Python -starts. See :ref:`subprocess` for details. - -.. autofunction:: process_startup diff --git a/doc/api_module.rst b/doc/api_module.rst new file mode 100644 index 000000000..0c993632c --- /dev/null +++ b/doc/api_module.rst @@ -0,0 +1,39 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +.. _api_module: + +coverage module +--------------- + +.. module:: coverage + +The most important thing in the coverage module is the +:class:`coverage.Coverage` class, described in :ref:`api_coverage`, but there +are a few other things also. + + +.. data:: version_info + +A tuple of five elements, similar to :data:`sys.version_info +`: *major*, *minor*, *micro*, *releaselevel*, and +*serial*. All values except *releaselevel* are integers; the release level is +``'alpha'``, ``'beta'``, ``'candidate'``, or ``'final'``. Unlike +:data:`sys.version_info `, the elements are not +available by name. + +.. data:: __version__ + +A string with the version of coverage.py, for example, ``"5.0b2"``. + +.. autoclass:: CoverageException + + +Starting coverage.py automatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This function is used to start coverage measurement automatically when Python +starts. See :ref:`subprocess` for details. + +.. autofunction:: process_startup + From c447cc814c0b4e08e1307e89df66a3bffa45c0c6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 9 Sep 2019 07:29:49 -0400 Subject: [PATCH 640/952] Fix a reference --- doc/contexts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/contexts.rst b/doc/contexts.rst index 8f7854f9f..d676a9db5 100644 --- a/doc/contexts.rst +++ b/doc/contexts.rst @@ -56,7 +56,7 @@ There are three ways to enable dynamic contexts: plugin, or * another tool (such as a test runner) can call the - :meth:`Coverage.switch_context` method to set the context explicitly. + :meth:`.Coverage.switch_context` method to set the context explicitly. The ``[run] dynamic_context`` setting has only one option now. Set it to ``test_function`` to start a new dynamic context for every test function:: From 3532b93a971bac212054b4e5669339303b794570 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 9 Sep 2019 11:23:48 -0400 Subject: [PATCH 641/952] Correct some function names and docstrings. #843 --- coverage/control.py | 5 +++-- coverage/inorout.py | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index c342eca47..6830afba7 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -665,9 +665,10 @@ def _post_save_work(self): if not self._data and self._warn_no_data: self._warn("No data was collected.", slug="no-data-collected") - # Find files that were never executed at all. + # Touch all the files that could have executed, so that we can + # mark completely unexecuted files as 0% covered. if self._data: - for file_path, plugin_name in self._inorout.find_unexecuted_files(): + for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files(): self._data.touch_file(file_path, plugin_name) if self.config.note: diff --git a/coverage/inorout.py b/coverage/inorout.py index c31e9206b..2a19ba691 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -395,8 +395,8 @@ def _warn_about_unmeasured_code(self, pkg): slug="module-not-measured", ) - def find_unexecuted_files(self): - """Find files in the areas of interest that weren't traced. + def find_possibly_unexecuted_files(self): + """Find files in the areas of interest that might be untraced. Yields pairs: file path, and responsible plug-in name. """ @@ -405,11 +405,11 @@ def find_unexecuted_files(self): not module_has_file(sys.modules[pkg])): continue pkg_file = source_for_file(sys.modules[pkg].__file__) - for ret in self._find_unexecuted_files(canonical_path(pkg_file)): + for ret in self._find_executable_files(canonical_path(pkg_file)): yield ret for src in self.source: - for ret in self._find_unexecuted_files(src): + for ret in self._find_executable_files(src): yield ret def _find_plugin_files(self, src_dir): @@ -418,11 +418,14 @@ def _find_plugin_files(self, src_dir): for x_file in plugin.find_executable_files(src_dir): yield x_file, plugin._coverage_plugin_name - def _find_unexecuted_files(self, src_dir): - """Find unexecuted files in `src_dir`. + def _find_executable_files(self, src_dir): + """Find executable files in `src_dir`. - Search for files in `src_dir` that are probably importable, - and add them as unexecuted files in `self.data`. + Search for files in `src_dir` that can be executed because they + are probably importable. Don't include ones that have been omitted + by the configuration. + + Yield the file path, and the plugin name that handles the file. """ py_files = ((py_file, None) for py_file in find_python_files(src_dir)) From 15842b97cd9a7b9232936e4c558ddb2d27231412 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 14 Sep 2019 08:58:21 -0400 Subject: [PATCH 642/952] Polish up substitute_variables based on a lightning talk --- coverage/config.py | 2 +- coverage/misc.py | 50 +++++++++++++++++++++++----------------------- tests/test_misc.py | 1 + 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/coverage/config.py b/coverage/config.py index 85493df13..7d6911458 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -87,7 +87,7 @@ def get(self, section, option, *args, **kwargs): # pylint: disable=argume raise configparser.NoOptionError v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs) - v = substitute_variables(v) + v = substitute_variables(v, os.environ) return v def getlist(self, section, option): diff --git a/coverage/misc.py b/coverage/misc.py index 3423170f9..4ac24ff0f 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -246,7 +246,7 @@ def _needs_to_implement(that, func_name): ) -def substitute_variables(text, variables=os.environ): +def substitute_variables(text, variables): """Substitute ``${VAR}`` variables in `text` with their values. Variables in the text can take a number of shell-inspired forms:: @@ -255,42 +255,42 @@ def substitute_variables(text, variables=os.environ): ${VAR} ${VAR?} strict: an error if VAR isn't defined. ${VAR-missing} defaulted: "missing" if VAR isn't defined. + $$ just a dollar sign. - A dollar can be inserted with ``$$``. - - `variables` is a dictionary of variable values, defaulting to the - environment variables. + `variables` is a dictionary of variable values. Returns the resulting text with values substituted. """ + dollar_pattern = r"""(?x) # Use extended regex syntax + \$ # A dollar sign, + (?: # then + (?P\w+) | # a plain word, + (?P\$) | # or a dollar sign. + { # or a {-wrapped + (?P\w+) # word, + (?: + (?P\?) | # with a strict marker + -(?P[^}]*) # or a default value + )? # maybe. + } + ) + """ + def dollar_replace(m): """Called for each $replacement.""" # Only one of the groups will have matched, just get its text. - word = next(v for v in m.group('v1', 'v2', 'char') if v) + word = next(w for w in m.group('w1', 'w2', 'dollar') if w) if word == "$": return "$" + elif word in variables: + return variables[word] + elif m.group('strict'): + msg = "Variable {} is undefined: {!r}".format(word, text) + raise CoverageException(msg) else: - strict = bool(m.group('strict')) - if strict: - if word not in variables: - msg = "Variable {} is undefined: {!r}".format(word, text) - raise CoverageException(msg) - return variables.get(word, m.group('defval') or '') + return m.group('defval') - dollar_pattern = r"""(?x) # Use extended regex syntax - \$(?: # A dollar sign, then - (?P\w+) | # a plain word, - (?P\$) | # or a dollar sign. - { # or a {-wrapped word, - (?P\w+) - (?: - (?P\?) | # with a strict marker - -(?P[^}]*) # or a default value - )? - } - ) - """ text = re.sub(dollar_pattern, dollar_replace, text) return text diff --git a/tests/test_misc.py b/tests/test_misc.py index d27772814..896dbf477 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -145,6 +145,7 @@ def undecorated(a=None, b=None): ("Ill-formed: ${%5} ${{HI}} ${", "Ill-formed: ${%5} ${{HI}} ${"), ("Strict: ${FOO?} is there", "Strict: fooey is there"), ("Defaulted: ${WUT-missing}!", "Defaulted: missing!"), + ("Defaulted empty: ${WUT-}!", "Defaulted empty: !"), ]) def test_substitute_variables(before, after): assert substitute_variables(before, VARS) == after From 04aac1294977121c0444021bc3ea0e080faf0921 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 14 Sep 2019 08:58:47 -0400 Subject: [PATCH 643/952] Trim blank --- doc/api_module.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/api_module.rst b/doc/api_module.rst index 0c993632c..c3da90fba 100644 --- a/doc/api_module.rst +++ b/doc/api_module.rst @@ -36,4 +36,3 @@ This function is used to start coverage measurement automatically when Python starts. See :ref:`subprocess` for details. .. autofunction:: process_startup - From d8c5066deb9f1b8937103c364e48e7e1b3bcb02c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 15 Sep 2019 07:41:42 -0400 Subject: [PATCH 644/952] Python tracer supports dynamic contexts. #846 --- CHANGES.rst | 4 ++++ coverage/pytracer.py | 38 ++++++++++++++++++++++++++++++++++---- tests/test_context.py | 21 --------------------- tests/test_plugins.py | 35 ----------------------------------- 4 files changed, 38 insertions(+), 60 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index ec79353fb..a97f2f27b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -24,6 +24,9 @@ Unreleased in `issue 720`_. The new ``coverage json`` command writes raw and summarized data to a JSON file. Thanks, Matt Bachmann. +- Dynamic contexts are now supported in the Python tracer, which is important + for PyPy users. Closes `issue 846`_. + - The compact line number representation introduced in 5.0a6 is called a "numbits." The :mod:`coverage.numbits` module provides functions for working with them. @@ -46,6 +49,7 @@ Unreleased .. _issue 822: https://github.com/nedbat/coveragepy/issues/822 .. _issue 834: https://github.com/nedbat/coveragepy/issues/834 .. _issue 829: https://github.com/nedbat/coveragepy/issues/829 +.. _issue 846: https://github.com/nedbat/coveragepy/issues/846 .. _changes_50a6: diff --git a/coverage/pytracer.py b/coverage/pytracer.py index e64d7f55a..1b9e4d71a 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -40,6 +40,7 @@ def __init__(self): self.trace_arcs = False self.should_trace = None self.should_trace_cache = None + self.should_start_context = None self.warn = None # The threading module to use, if any. self.threading = None @@ -47,6 +48,8 @@ def __init__(self): self.cur_file_dict = None self.last_line = 0 # int, but uninitialized. self.cur_file_name = None + self.context = None + self.started_context = False self.data_stack = [] self.last_exc_back = None @@ -96,14 +99,35 @@ def _trace(self, frame, event, arg_unused): if self.trace_arcs and self.cur_file_dict: pair = (self.last_line, -self.last_exc_firstlineno) self.cur_file_dict[pair] = None - self.cur_file_dict, self.cur_file_name, self.last_line = self.data_stack.pop() + self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = ( + self.data_stack.pop() + ) self.last_exc_back = None if event == 'call': - # Entering a new function context. Decide if we should trace + # Should we start a new context? + if self.should_start_context and self.context is None: + context_maybe = self.should_start_context(frame) + if context_maybe is not None: + self.context = context_maybe + self.started_context = True + self.switch_context(self.context) + else: + self.started_context = False + else: + self.started_context = False + + # Entering a new frame. Decide if we should trace # in this file. self._activity = True - self.data_stack.append((self.cur_file_dict, self.cur_file_name, self.last_line)) + self.data_stack.append( + ( + self.cur_file_dict, + self.cur_file_name, + self.last_line, + self.started_context, + ) + ) filename = frame.f_code.co_filename self.cur_file_name = filename disp = self.should_trace_cache.get(filename) @@ -146,7 +170,13 @@ def _trace(self, frame, event, arg_unused): first = frame.f_code.co_firstlineno self.cur_file_dict[(self.last_line, -first)] = None # Leaving this function, pop the filename stack. - self.cur_file_dict, self.cur_file_name, self.last_line = self.data_stack.pop() + self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = ( + self.data_stack.pop() + ) + # Leaving a context? + if self.started_context: + self.context = None + self.switch_context(None) elif event == 'exception': self.last_exc_back = frame.f_back self.last_exc_firstlineno = frame.f_code.co_firstlineno diff --git a/tests/test_context.py b/tests/test_context.py index 9c8d46057..eb8ee9c56 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -10,7 +10,6 @@ from coverage import env from coverage.context import qualname_from_frame from coverage.data import CoverageData -from coverage.misc import CoverageException from tests.coveragetest import CoverageTest @@ -107,11 +106,6 @@ def test_combining_arc_contexts(self): class DynamicContextTest(CoverageTest): """Tests of dynamically changing contexts.""" - def setUp(self): - if not env.C_TRACER: - self.skipTest("Only the C tracer supports dynamic contexts") - super(DynamicContextTest, self).setUp() - SOURCE = """\ def helper(lineno): x = 2 @@ -178,21 +172,6 @@ def test_static_and_dynamic(self): data.lines(fname, ["stat|two_tests.test_two"]), self.TEST_TWO_LINES) -class DynamicContextWithPythonTracerTest(CoverageTest): - """The Python tracer doesn't do dynamic contexts at all.""" - - run_in_temp_dir = False - - def test_python_tracer_fails_properly(self): - if env.C_TRACER: - self.skipTest("This test is specifically about the Python tracer.") - cov = coverage.Coverage() - cov.set_option("run:dynamic_context", "test_function") - msg = r"Can't support dynamic contexts with PyTracer" - with self.assertRaisesRegex(CoverageException, msg): - cov.start() - - def get_qualname(): """Helper to return qualname_from_frame for the caller.""" stack = inspect.stack()[1:] diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 416f05a2e..eb51c1b95 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -887,11 +887,6 @@ def test_configurer_plugin(self): class DynamicContextPluginTest(CoverageTest): """Tests of plugins that implement `dynamic_context`.""" - def setUp(self): - if not env.C_TRACER: - self.skipTest("Plugins are only supported with the C tracer.") - super(DynamicContextPluginTest, self).setUp() - def make_plugin_capitalized_testnames(self, filename): """Create a dynamic context plugin that capitalizes the part after 'test_'.""" self.make_file(filename, """\ @@ -1112,33 +1107,3 @@ def test_multiple_plugins(self): [2, 8], data.lines(filenames['rendering.py'], contexts=["renderer:span"]), ) - - -class DynamicContextPluginOtherTracersTest(CoverageTest): - """Tests of plugins that implement `dynamic_context`.""" - - def setUp(self): - if env.C_TRACER: - self.skipTest("These tests are for tracers not implemented in C.") - super(DynamicContextPluginOtherTracersTest, self).setUp() - - def test_other_tracer_support(self): - self.make_file("context_plugin.py", """\ - from coverage import CoveragePlugin - - class Plugin(CoveragePlugin): - def dynamic_context(self, frame): - return frame.f_code.co_name - - def coverage_init(reg, options): - reg.add_dynamic_context(Plugin()) - """) - - cov = coverage.Coverage() - cov.set_option("run:plugins", ['context_plugin']) - - msg = "Can't support dynamic contexts with PyTracer" - with self.assertRaisesRegex(CoverageException, msg): - cov.start() - - cov.stop() # pragma: nested From 12d2d68223a7685a10061906607a0b5e58a73380 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 15 Sep 2019 09:52:35 -0400 Subject: [PATCH 645/952] Avoid a test that recent pypy3 can't run properly --- tests/test_process.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_process.py b/tests/test_process.py index be7ae446e..4298244ae 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -766,6 +766,9 @@ def test_lang_c(self): if env.JYTHON: # Jython as of 2.7.1rc3 won't compile a filename that isn't utf8. self.skipTest("Jython can't handle this test") + if env.PYPY and env.PY3 and env.PYPYVERSION[:3] >= (7, 1, 1): # pragma: obscure + # https://bitbucket.org/pypy/pypy/issues/3074/compile-fails-on-non-ascii-filename-if + self.skipTest("Avoid getfilesystemencoding problem on pypy3") # 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 From 0140c505eebe75035dbdb4723d233d15f439bc20 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Fri, 20 Sep 2019 19:58:12 -0400 Subject: [PATCH 646/952] Remove unused attribute --- coverage/python.py | 1 - 1 file changed, 1 deletion(-) diff --git a/coverage/python.py b/coverage/python.py index ed467e61b..81aa66ba1 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -166,7 +166,6 @@ def __init__(self, morf, coverage=None): self._source = None self._parser = None - self._statements = None self._excluded = None def __repr__(self): From 341e737bd5010ffd794b029b3051a7ae210bfef2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 21 Sep 2019 07:21:05 -0400 Subject: [PATCH 647/952] Exit arcs have to be corrected to first lines We've long remapped line numbers to the first line of a multi-line statement. But exit line numbers (negative numbers) were not remapped. This meant we were needlessly chasing weirdnesses in implementations. But the actual results of running coverage always remapped results to the first line, so there's no point in tracking the unmapped line numbers in our tests. --- coverage/parser.py | 6 +++- tests/test_arcs.py | 70 +++++++++++++++++----------------------------- 2 files changed, 30 insertions(+), 46 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 12c2d0a5f..f0d378c6b 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -207,7 +207,11 @@ def _raw_parse(self): def first_line(self, line): """Return the first line number of the statement including `line`.""" - return self._multiline.get(line, line) + if line < 0: + line = -self._multiline.get(-line, -line) + else: + line = self._multiline.get(line, line) + return line def first_lines(self, lines): """Map the line numbers in `lines` to the correct first line of the diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 638478abc..bf17f7123 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -103,7 +103,7 @@ def test_multiline(self): b = \\ 6 """, - arcz="-21 15 5-2", + arcz=".1 15 5.", ) def test_if_return(self): @@ -444,10 +444,6 @@ def test_other_comprehensions(self): ) def test_multiline_dict_comp(self): - if env.PYVERSION < (3, 5): - arcz = "-42 2B B-4 2-4" - else: - arcz = "-32 2B B-3 2-3" # Multiline dict comp: self.check_coverage("""\ # comment @@ -462,13 +458,9 @@ def test_multiline_dict_comp(self): } x = 11 """, - arcz=arcz, + arcz="-22 2B B-2 2-2" ) # Multi dict comp: - if env.PYVERSION < (3, 5): - arcz = "-42 2F F-4 2-4" - else: - arcz = "-32 2F F-3 2-3" self.check_coverage("""\ # comment d = \\ @@ -486,7 +478,7 @@ def test_multiline_dict_comp(self): } x = 15 """, - arcz=arcz, + arcz="-22 2F F-2 2-2" ) @@ -1204,11 +1196,6 @@ class MiscArcTest(CoverageTest): """Miscellaneous arc-measuring tests.""" def test_dict_literal(self): - if env.PYVERSION < (3, 5): - arcz = ".1 19 9." - else: - # Python 3.5 changed how dict literals are constructed. - arcz = "-21 19 9-2" self.check_coverage("""\ d = { 'a': 2, @@ -1220,7 +1207,7 @@ def test_dict_literal(self): } assert d """, - arcz=arcz, + arcz=".1 19 9.", ) self.check_coverage("""\ d = \\ @@ -1233,7 +1220,7 @@ def test_dict_literal(self): } assert d """, - arcz="-21 19 9-2", + arcz=".1 19 9.", ) def test_unpacked_literals(self): @@ -1251,7 +1238,7 @@ def test_unpacked_literals(self): } assert weird['b'] == 3 """, - arcz="-21 15 5A A-2" + arcz=".1 15 5A A." ) self.check_coverage("""\ l = [ @@ -1265,40 +1252,35 @@ def test_unpacked_literals(self): ] assert weird[1] == 3 """, - arcz="-21 15 5A A-2" + arcz=".1 15 5A A." ) 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 # opcodes. # Note that we no longer interpret bytecode at all, but it couldn't # hurt to keep the test... - code = """\ - data = [ - """ + "".join("""\ - [ - {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}], - """.format(i=i) for i in range(2000) - ) + """\ - ] - - print(len(data)) - """ - self.check_coverage( - code, - arcs=[(-3, 1), (1, 4004), (4004, -3)], - arcs_missing=[], arcs_unpredicted=[], - ) + for n in [10, 50, 100, 500, 1000, 2000]: + code = """\ + data = [ + """ + "".join("""\ + [ + {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}, {i}], + """.format(i=i) for i in range(n) + ) + """\ + ] + + print(len(data)) + """ + self.check_coverage(code, arcs=[(-1, 1), (1, 2*n+4), (2*n+4, -1)]) + self.assertEqual(self.stdout().split()[-1], str(n)) def test_partial_generators(self): # https://bitbucket.org/ned/coveragepy/issues/475/generator-expression-is-marked-as-not # Line 2 is executed completely. - # Line 3 is started but not finished, because zip ends when #2 ends. + # Line 3 is started but not finished, because zip ends before it finishes. # Line 4 is never started. cov = self.check_coverage("""\ def f(a, b): @@ -1307,7 +1289,7 @@ def f(a, b): e = (k for k in b) # 4 return dict(zip(c, d)) - f(['a', 'b'], [1, 2]) + f(['a', 'b'], [1, 2, 3]) """, arcz=".1 17 7. .2 23 34 45 5. -22 2-2 -33 3-3 -44 4-4", arcz_missing="3-3 -44 4-4", @@ -1441,7 +1423,7 @@ def test_multiline_lambda(self): ) assert fn(10) == 18 """, - arcz="-42 2A A-4 2-4", + arcz="-22 2A A-2 2-2", ) def test_unused_lambdas_are_confusing_bug_90(self): @@ -1489,9 +1471,7 @@ def test_lambda_in_dict(self): if k & 1: v() """, - arcz=".1 12 23 3A AB BC BA CA A. -43 -53 -63 -73 3-4 3-5 3-6 3-7", - arcz_missing="-43 3-4 -63 3-6", - arcz_unpredicted="", + arcz=".1 12 23 3A AB BC BA CA A. -33 3-3", ) From 5d7ca92398a4bdf974d92aa19e90c1e918b6990f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 21 Sep 2019 15:17:17 -0400 Subject: [PATCH 648/952] Doc changes for 5.0a7 --- CHANGES.rst | 6 ++++-- README.rst | 6 +++--- doc/conf.py | 2 +- doc/index.rst | 6 +----- setup.py | 2 -- 5 files changed, 9 insertions(+), 13 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index a97f2f27b..aa76d551e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,8 +17,10 @@ development at the same time, like 4.5.x and 5.0. .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- -Unreleased ----------- +.. _changes_50a7: + +Version 5.0a7 --- 2019-09-21 +---------------------------- - Data can now be "reported" in JSON format, for programmatic use, as requested in `issue 720`_. The new ``coverage json`` command writes raw and summarized diff --git a/README.rst b/README.rst index 9382bf90e..cf3a279f2 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: * CPython 2.7. -* CPython 3.5 through alpha 3.8. +* CPython 3.5 through beta 3.8. * PyPy2 7.0 and PyPy3 7.0. * Jython 2.7.1, though not for reporting. * IronPython 2.7.7, though not for reporting. @@ -32,8 +32,8 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on .. _GitHub: https://github.com/nedbat/coveragepy -**New in 5.0:** SQLite data storage, contexts, dropped support for Python 2.6, -3.3 and 3.4. +**New in 5.0:** SQLite data storage, JSON report, contexts, dropped support for +Python 2.6, 3.3 and 3.4. For Enterprise diff --git a/doc/conf.py b/doc/conf.py index 4baf66acc..2018b57e3 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -61,7 +61,7 @@ # The short X.Y version. version = '5.0' # CHANGEME # The full version, including alpha/beta/rc tags. -release = '5.0a6' # CHANGEME +release = '5.0a7' # CHANGEME # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/index.rst b/doc/index.rst index d1eace30a..78106b306 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -28,17 +28,13 @@ not. .. ifconfig:: prerelease - The latest version is coverage.py 5.0a6, released July 16, 2019. + The latest version is coverage.py 5.0a7, released September 21, 2019. It is supported on: * Python versions 2.7, 3.5, 3.6, 3.7, and beta 3.8. * PyPy2 7.0 and PyPy3 7.0. - * Jython 2.7.1, though only for running code, not reporting. - - * IronPython 2.7.7, though only for running code, not reporting. - **This is a pre-release build. The usual warnings about possible bugs apply.** The latest stable version is coverage.py 4.5.4, `described here`_. diff --git a/setup.py b/setup.py index b1d086be5..c500ae6e3 100644 --- a/setup.py +++ b/setup.py @@ -34,8 +34,6 @@ Programming Language :: Python :: 3.8 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy -Programming Language :: Python :: Implementation :: Jython -Programming Language :: Python :: Implementation :: IronPython Topic :: Software Development :: Quality Assurance Topic :: Software Development :: Testing """ From c238b8520a060a3639fd5a51b87eb11420a9ebd4 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 21 Sep 2019 16:40:33 -0400 Subject: [PATCH 649/952] Scrub another place --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index cf3a279f2..aab83ebec 100644 --- a/README.rst +++ b/README.rst @@ -22,8 +22,6 @@ Coverage.py runs on many versions of Python: * CPython 2.7. * CPython 3.5 through beta 3.8. * PyPy2 7.0 and PyPy3 7.0. -* Jython 2.7.1, though not for reporting. -* IronPython 2.7.7, though not for reporting. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. From 69c519c42f0818e0020b88ccfc46b30f3b4f7775 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 21 Sep 2019 16:52:00 -0400 Subject: [PATCH 650/952] Bump version --- CHANGES.rst | 6 ++++++ coverage/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index aa76d551e..0be214e84 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,12 @@ development at the same time, like 4.5.x and 5.0. .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- +Unreleased +---------- + +Nothing yet. + + .. _changes_50a7: Version 5.0a7 --- 2019-09-21 diff --git a/coverage/version.py b/coverage/version.py index 122e02ef7..ebe82c3be 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 0, 0, 'alpha', 7) +version_info = (5, 0, 0, 'alpha', 8) def _make_version(major, minor, micro, releaselevel, serial): From 1f876d049a20fa04c76dcef42562de1d1f43e3af Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 1 Oct 2019 06:45:13 -0400 Subject: [PATCH 651/952] Subprocesses only use config file options --- doc/subprocess.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/subprocess.rst b/doc/subprocess.rst index 060689592..34d7f5bac 100644 --- a/doc/subprocess.rst +++ b/doc/subprocess.rst @@ -22,6 +22,10 @@ examines the ``COVERAGE_PROCESS_START`` environment variable, and if it is set, begins coverage measurement. The environment variable's value will be used as the name of the :ref:`configuration file ` to use. +.. note:: + The subprocess only sees options in the configuration file. Options set on + the command line will not be used in the subprocesses. + When using this technique, be sure to set the parallel option to true so that multiple coverage.py runs will each write their data to a distinct file. From 46387c0fb8781ec6ada320de19aeee6c1d39823f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 2 Oct 2019 13:23:33 -0400 Subject: [PATCH 652/952] A better word --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 0be214e84..adcccb1ae 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,7 +7,7 @@ Change history for Coverage.py These changes are listed in decreasing version number order. Note this can be different from a strict chronological order when there are two branches in -development at the same time, like 4.5.x and 5.0. +development at the same time, such as 4.5.x and 5.0. .. When updating the "Unreleased" header to a specific version, use this .. format. Don't forget the jump target: From 4c6a1f3746b9d9772cae7b415fc456422b4a8bd6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 2 Oct 2019 13:23:40 -0400 Subject: [PATCH 653/952] Clearer logic --- coverage/html.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage/html.py b/coverage/html.py index 253bda1ce..e560f76ba 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -90,10 +90,10 @@ def data_for_file(self, fr, analysis): missing_branch_arcs = analysis.missing_branch_arcs() arcs_executed = analysis.arcs_executed() - contexts_by_lineno = collections.defaultdict(list) if self.config.show_contexts: - # Lookup line number contexts. contexts_by_lineno = analysis.data.contexts_by_lineno(fr.filename) + else: + contexts_by_lineno = collections.defaultdict(list) lines = [] From 079b6d8f145f50918461a4568d9a961e1eb38024 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 2 Oct 2019 10:27:06 -0400 Subject: [PATCH 654/952] Setting query context has to be done with a separate method call --- coverage/sqldata.py | 58 +++++++++++++------------------ tests/test_api.py | 16 ++++----- tests/test_context.py | 64 ++++++++++++++++++++++------------- tests/test_data.py | 16 +++++---- tests/test_plugins.py | 79 +++++++++++++++++-------------------------- 5 files changed, 112 insertions(+), 121 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 06ccff521..95af60726 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -192,7 +192,6 @@ def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=N self._current_context = None self._current_context_id = None - self._query_contexts = None self._query_context_ids = None def _choose_filename(self): @@ -714,32 +713,27 @@ def file_tracer(self, filename): return row[0] or "" return "" # File was measured, but no tracer associated. + def set_query_context(self, context): + self._start_using() + with self._connect() as con: + cur = con.execute("select id from context where context = ?", (context,)) + self._query_context_ids = [row[0] for row in cur.fetchall()] def set_query_contexts(self, contexts): """Set query contexts for future `lines`, `arcs` etc. calls.""" + self._start_using() if contexts: - self._query_context_ids = self._get_query_context_ids(contexts) - else: - self._query_context_ids = None - self._query_contexts = contexts - - def _get_query_context_ids(self, contexts=None): - if contexts is not None: - if not contexts: - return None - self._start_using() with self._connect() as con: context_clause = ' or '.join(['context glob ?'] * len(contexts)) cur = con.execute("select id from context where " + context_clause, contexts) - return [row[0] for row in cur.fetchall()] - elif self._query_contexts: - return self._query_context_ids - return None + self._query_context_ids = [row[0] for row in cur.fetchall()] + else: + self._query_context_ids = None - def lines(self, filename, contexts=None): + def lines(self, filename): self._start_using() if self.has_arcs(): - arcs = self.arcs(filename, contexts=contexts) + arcs = self.arcs(filename) if arcs is not None: all_lines = itertools.chain.from_iterable(arcs) return list(set(l for l in all_lines if l > 0)) @@ -751,18 +745,17 @@ def lines(self, filename, contexts=None): else: query = "select numbits from line_bits where file_id = ?" data = [file_id] - context_ids = self._get_query_context_ids(contexts) - if context_ids is not None: - ids_array = ', '.join('?'*len(context_ids)) + if self._query_context_ids is not None: + ids_array = ', '.join('?' * len(self._query_context_ids)) query += " and context_id in (" + ids_array + ")" - data += context_ids + data += self._query_context_ids bitmaps = list(con.execute(query, data)) nums = set() for row in bitmaps: nums.update(numbits_to_nums(row[0])) return sorted(nums) - def arcs(self, filename, contexts=None): + def arcs(self, filename): self._start_using() with self._connect() as con: file_id = self._file_id(filename) @@ -771,11 +764,10 @@ def arcs(self, filename, contexts=None): else: query = "select distinct fromno, tono from arc where file_id = ?" data = [file_id] - context_ids = self._get_query_context_ids(contexts) - if context_ids is not None: - ids_array = ', '.join('?'*len(context_ids)) + if self._query_context_ids is not None: + ids_array = ', '.join('?' * len(self._query_context_ids)) query += " and context_id in (" + ids_array + ")" - data += context_ids + data += self._query_context_ids arcs = con.execute(query, data) return list(arcs) @@ -793,11 +785,10 @@ def contexts_by_lineno(self, filename): "where arc.file_id = ? and arc.context_id = context.id" ) data = [file_id] - context_ids = self._get_query_context_ids() - if context_ids is not None: - ids_array = ', '.join('?'*len(context_ids)) + if self._query_context_ids is not None: + ids_array = ', '.join('?' * len(self._query_context_ids)) query += " and arc.context_id in (" + ids_array + ")" - data += context_ids + data += self._query_context_ids for fromno, tono, context in con.execute(query, data): if context not in lineno_contexts_map[fromno]: lineno_contexts_map[fromno].append(context) @@ -810,11 +801,10 @@ def contexts_by_lineno(self, filename): "and file_id = ?" ) data = [file_id] - context_ids = self._get_query_context_ids() - if context_ids is not None: - ids_array = ', '.join('?'*len(context_ids)) + if self._query_context_ids is not None: + ids_array = ', '.join('?' * len(self._query_context_ids)) query += " and l.context_id in (" + ids_array + ")" - data += context_ids + data += self._query_context_ids for numbits, context in con.execute(query, data): for lineno in numbits_to_nums(numbits): lineno_contexts_map[lineno].append(context) diff --git a/tests/test_api.py b/tests/test_api.py index 4f2484412..b21435484 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -539,10 +539,10 @@ def test_switch_context_testrunner(self): filenames = self.get_measured_filenames(data) suite_filename = filenames['testsuite.py'] - self.assertEqual( - [2, 8], data.lines(suite_filename, contexts=["multiply_six"])) - self.assertEqual( - [2, 5], data.lines(suite_filename, contexts=["multiply_zero"])) + data.set_query_context("multiply_six") + self.assertEqual([2, 8], data.lines(suite_filename)) + data.set_query_context("multiply_zero") + self.assertEqual([2, 5], data.lines(suite_filename)) def test_switch_context_with_static(self): # This test simulates a coverage-aware test runner, @@ -579,10 +579,10 @@ def test_switch_context_with_static(self): filenames = self.get_measured_filenames(data) suite_filename = filenames['testsuite.py'] - self.assertEqual( - [2, 8], data.lines(suite_filename, contexts=["mysuite|multiply_six"])) - self.assertEqual( - [2, 5], data.lines(suite_filename, contexts=["mysuite|multiply_zero"])) + data.set_query_context("mysuite|multiply_six") + self.assertEqual([2, 8], data.lines(suite_filename)) + data.set_query_context("mysuite|multiply_zero") + self.assertEqual([2, 5], data.lines(suite_filename)) def test_switch_context_unstarted(self): # Coverage must be started to switch context diff --git a/tests/test_context.py b/tests/test_context.py index eb8ee9c56..72739ad01 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -72,10 +72,14 @@ def test_combining_line_contexts(self): fred = full_names['red.py'] fblue = full_names['blue.py'] - self.assertEqual(combined.lines(fred, contexts=['red']), self.LINES) - self.assertEqual(combined.lines(fred, contexts=['blue']), []) - self.assertEqual(combined.lines(fblue, contexts=['red']), []) - self.assertEqual(combined.lines(fblue, contexts=['blue']), self.LINES) + def assert_combined_lines(filename, context, lines): + combined.set_query_context(context) + self.assertEqual(combined.lines(filename), lines) + + assert_combined_lines(fred, 'red', self.LINES) + assert_combined_lines(fred, 'blue', []) + assert_combined_lines(fblue, 'red', []) + assert_combined_lines(fblue, 'blue', self.LINES) def test_combining_arc_contexts(self): red_data, blue_data = self.run_red_blue(branch=True) @@ -92,15 +96,23 @@ def test_combining_arc_contexts(self): fred = full_names['red.py'] fblue = full_names['blue.py'] - self.assertEqual(combined.lines(fred, contexts=['red']), self.LINES) - self.assertEqual(combined.lines(fred, contexts=['blue']), []) - self.assertEqual(combined.lines(fblue, contexts=['red']), []) - self.assertEqual(combined.lines(fblue, contexts=['blue']), self.LINES) + def assert_combined_lines(filename, context, lines): + combined.set_query_context(context) + self.assertEqual(combined.lines(filename), lines) + + assert_combined_lines(fred, 'red', self.LINES) + assert_combined_lines(fred, 'blue', []) + assert_combined_lines(fblue, 'red', []) + assert_combined_lines(fblue, 'blue', self.LINES) + + def assert_combined_arcs(filename, context, lines): + combined.set_query_context(context) + self.assertEqual(combined.arcs(filename), lines) - self.assertEqual(combined.arcs(fred, contexts=['red']), self.ARCS) - self.assertEqual(combined.arcs(fred, contexts=['blue']), []) - self.assertEqual(combined.arcs(fblue, contexts=['red']), []) - self.assertEqual(combined.arcs(fblue, contexts=['blue']), self.ARCS) + assert_combined_arcs(fred, 'red', self.ARCS) + assert_combined_arcs(fred, 'blue', []) + assert_combined_arcs(fblue, 'red', []) + assert_combined_arcs(fblue, 'blue', self.ARCS) class DynamicContextTest(CoverageTest): @@ -145,12 +157,14 @@ def test_dynamic_alone(self): self.assertCountEqual( data.measured_contexts(), ["", "two_tests.test_one", "two_tests.test_two"]) - self.assertCountEqual(data.lines(fname, [""]), self.OUTER_LINES) - self.assertCountEqual( - data.lines(fname, ["two_tests.test_one"]), - self.TEST_ONE_LINES) - self.assertCountEqual( - data.lines(fname, ["two_tests.test_two"]), self.TEST_TWO_LINES) + + def assert_context_lines(context, lines): + data.set_query_context(context) + self.assertCountEqual(lines, data.lines(fname)) + + assert_context_lines("", self.OUTER_LINES) + assert_context_lines("two_tests.test_one", self.TEST_ONE_LINES) + assert_context_lines("two_tests.test_two", self.TEST_TWO_LINES) def test_static_and_dynamic(self): self.make_file("two_tests.py", self.SOURCE) @@ -164,12 +178,14 @@ def test_static_and_dynamic(self): self.assertCountEqual( data.measured_contexts(), ["stat", "stat|two_tests.test_one", "stat|two_tests.test_two"]) - self.assertCountEqual( - data.lines(fname, ["stat"]), self.OUTER_LINES) - self.assertCountEqual( - data.lines(fname, ["stat|two_tests.test_one"]), self.TEST_ONE_LINES) - self.assertCountEqual( - data.lines(fname, ["stat|two_tests.test_two"]), self.TEST_TWO_LINES) + + def assert_context_lines(context, lines): + data.set_query_context(context) + self.assertCountEqual(lines, data.lines(fname)) + + assert_context_lines("stat", self.OUTER_LINES) + assert_context_lines("stat|two_tests.test_one", self.TEST_ONE_LINES) + assert_context_lines("stat|two_tests.test_two", self.TEST_TWO_LINES) def get_qualname(): diff --git a/tests/test_data.py b/tests/test_data.py index eb389e353..71e18bb97 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -201,8 +201,10 @@ def test_lines_with_contexts(self): covdata.set_context('test_a') covdata.add_lines(LINES_1) self.assertEqual(covdata.lines('a.py'), [1, 2]) - self.assertEqual(covdata.lines('a.py', contexts=['test*']), [1, 2]) - self.assertEqual(covdata.lines('a.py', contexts=['other*']), []) + covdata.set_query_contexts(['test*']) + self.assertEqual(covdata.lines('a.py'), [1, 2]) + covdata.set_query_contexts(['other*']) + self.assertEqual(covdata.lines('a.py'), []) def test_contexts_by_lineno_with_lines(self): covdata = CoverageData() @@ -250,11 +252,11 @@ def test_arcs_with_contexts(self): covdata = CoverageData() covdata.set_context('test_x') covdata.add_arcs(ARCS_3) - self.assertEqual( - covdata.arcs('x.py'), [(-1, 1), (1, 2), (2, 3), (3, -1)]) - self.assertEqual(covdata.arcs( - 'x.py', contexts=['test*']), [(-1, 1), (1, 2), (2, 3), (3, -1)]) - self.assertEqual(covdata.arcs('x.py', contexts=['other*']), []) + self.assertEqual(covdata.arcs('x.py'), [(-1, 1), (1, 2), (2, 3), (3, -1)]) + covdata.set_query_contexts(['test*']) + self.assertEqual(covdata.arcs('x.py'), [(-1, 1), (1, 2), (2, 3), (3, -1)]) + covdata.set_query_contexts(['other*']) + self.assertEqual(covdata.arcs('x.py'), []) def test_contexts_by_lineno_with_arcs(self): covdata = CoverageData() diff --git a/tests/test_plugins.py b/tests/test_plugins.py index eb51c1b95..22796dc3a 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -990,18 +990,12 @@ def test_plugin_standalone(self): ['', 'doctest:HTML_TAG', 'test:HTML_TAG', 'test:RENDERERS'], sorted(data.measured_contexts()), ) - self.assertEqual( - [2], - data.lines(filenames['rendering.py'], contexts=["doctest:HTML_TAG"]), - ) - self.assertEqual( - [2], - data.lines(filenames['rendering.py'], contexts=["test:HTML_TAG"]), - ) - self.assertEqual( - [2, 5, 8, 11], - data.lines(filenames['rendering.py'], contexts=["test:RENDERERS"]), - ) + data.set_query_context("doctest:HTML_TAG") + self.assertEqual([2], data.lines(filenames['rendering.py'])) + data.set_query_context("test:HTML_TAG") + self.assertEqual([2], data.lines(filenames['rendering.py'])) + data.set_query_context("test:RENDERERS") + self.assertEqual([2, 5, 8, 11], data.lines(filenames['rendering.py'])) def test_static_context(self): self.make_plugin_capitalized_testnames('plugin_tests.py') @@ -1041,22 +1035,21 @@ def test_plugin_with_test_function(self): # labeled by plugin_tests. data = cov.get_data() filenames = self.get_measured_filenames(data) - self.assertEqual( - ['', 'doctest:HTML_TAG', 'testsuite.test_html_tag', 'testsuite.test_renderers'], - sorted(data.measured_contexts()), - ) - self.assertEqual( - [2], - data.lines(filenames['rendering.py'], contexts=["doctest:HTML_TAG"]), - ) - self.assertEqual( - [2], - data.lines(filenames['rendering.py'], contexts=["testsuite.test_html_tag"]), - ) - self.assertEqual( - [2, 5, 8, 11], - data.lines(filenames['rendering.py'], contexts=["testsuite.test_renderers"]), - ) + expected = [ + '', + 'doctest:HTML_TAG', + 'testsuite.test_html_tag', + 'testsuite.test_renderers', + ] + self.assertEqual(expected, sorted(data.measured_contexts())) + + def assert_context_lines(context, lines): + data.set_query_context(context) + self.assertEqual(lines, data.lines(filenames['rendering.py'])) + + assert_context_lines("doctest:HTML_TAG", [2]) + assert_context_lines("testsuite.test_html_tag", [2]) + assert_context_lines("testsuite.test_renderers", [2, 5, 8, 11]) def test_multiple_plugins(self): self.make_plugin_capitalized_testnames('plugin_tests.py') @@ -1087,23 +1080,13 @@ def test_multiple_plugins(self): 'test:RENDERERS', ] self.assertEqual(expected, sorted(data.measured_contexts())) - self.assertEqual( - [2], - data.lines(filenames['rendering.py'], contexts=["test:HTML_TAG"]), - ) - self.assertEqual( - [2, 5, 8, 11], - data.lines(filenames['rendering.py'], contexts=["test:RENDERERS"]), - ) - self.assertEqual( - [2], - data.lines(filenames['rendering.py'], contexts=["doctest:HTML_TAG"]), - ) - self.assertEqual( - [2, 5], - data.lines(filenames['rendering.py'], contexts=["renderer:paragraph"]), - ) - self.assertEqual( - [2, 8], - data.lines(filenames['rendering.py'], contexts=["renderer:span"]), - ) + + def assert_context_lines(context, lines): + data.set_query_context(context) + self.assertEqual(lines, data.lines(filenames['rendering.py'])) + + assert_context_lines("test:HTML_TAG", [2]) + assert_context_lines("test:RENDERERS", [2, 5, 8, 11]) + assert_context_lines("doctest:HTML_TAG", [2]) + assert_context_lines("renderer:paragraph", [2, 5]) + assert_context_lines("renderer:span", [2, 8]) From 484e3639deabeccd4bbb3c2183e0e6c0f9407057 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 2 Oct 2019 13:49:47 -0400 Subject: [PATCH 655/952] Context patterns are regexes, not globs --- CHANGES.rst | 7 ++++++- coverage/cmdline.py | 4 ++-- coverage/sqldata.py | 32 +++++++++++++++++++++++++++----- tests/test_html.py | 2 +- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index adcccb1ae..5e8ac6726 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,12 @@ development at the same time, such as 4.5.x and 5.0. Unreleased ---------- -Nothing yet. +- The :class:`.CoverageData` API has changed how queries are limited to + specific contexts. Now you use :meth:`.CoverageData.set_query_context` to + set a single exact-match string, or :meth:`.CoverageData.set_query_contexts` + to set a list of regular expressions to match contexts. This changes the + command-line ``--contexts`` option to use regular expressions instead of + filename-style wildcards. .. _changes_50a7: diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 2bec4ea8c..66d4dc3cb 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -107,10 +107,10 @@ class Opts(object): ) contexts = optparse.make_option( '', '--contexts', action='store', - metavar="PAT1,PAT2,...", + metavar="REGEX1,REGEX2,...", help=( "Only display data from lines covered in the given contexts. " - "Accepts shell-style wildcards, which must be quoted." + "Accepts Python regexes, which must be quoted." ), ) output_xml = optparse.make_option( diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 95af60726..044ddbf13 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -14,6 +14,7 @@ import glob import itertools import os +import re import sqlite3 import sys import zlib @@ -21,9 +22,8 @@ from coverage.backward import get_thread_id, iitems, to_bytes, to_string from coverage.debug import NoDebugging, SimpleReprMixin, clipped_repr from coverage.files import PathAliases -from coverage.misc import CoverageException, file_be_gone, filename_suffix, isolate_module -from coverage.misc import contract -from coverage.numbits import nums_to_numbits, numbits_to_nums, numbits_union +from coverage.misc import CoverageException, contract, file_be_gone, filename_suffix, isolate_module +from coverage.numbits import numbits_to_nums, numbits_union, nums_to_numbits from coverage.version import __version__ os = isolate_module(os) @@ -714,17 +714,33 @@ def file_tracer(self, filename): return "" # File was measured, but no tracer associated. def set_query_context(self, context): + """Set the context for subsequent querying. + + The next `lines`, `arcs`, or `contexts_by_lineno` calls will be limited + to only one context. `context` is a string which must match a context + exactly. If it does not, no exception is raised, but queries will + return no data. + + """ self._start_using() with self._connect() as con: cur = con.execute("select id from context where context = ?", (context,)) self._query_context_ids = [row[0] for row in cur.fetchall()] def set_query_contexts(self, contexts): - """Set query contexts for future `lines`, `arcs` etc. calls.""" + """Set the contexts for subsequent querying. + + The next `lines`, `arcs`, or `contexts_by_lineno` calls will be limited + to the specified contexts. `contexts` is a list of Python regular + expressions. Contexts will be matched using :func:`re.search `. + Data will be included in query results if they are part of any of the + contexts matched. + + """ self._start_using() if contexts: with self._connect() as con: - context_clause = ' or '.join(['context glob ?'] * len(contexts)) + context_clause = ' or '.join(['context regexp ?'] * len(contexts)) cur = con.execute("select id from context where " + context_clause, contexts) self._query_context_ids = [row[0] for row in cur.fetchall()] else: @@ -842,6 +858,7 @@ def connect(self): if self.debug: self.debug.write("Connecting to {!r}".format(self.filename)) self.con = sqlite3.connect(filename, check_same_thread=False) + self.con.create_function('REGEXP', 2, _regexp) # This pragma makes writing faster. It disables rollbacks, but we never need them. # PyPy needs the .close() calls here, or sqlite gets twisted up: @@ -893,3 +910,8 @@ def executescript(self, script): def dump(self): """Return a multi-line string, the dump of the database.""" return "\n".join(self.con.iterdump()) + + +def _regexp(text, pattern): + """A regexp function for SQLite.""" + return re.search(text, pattern) is not None diff --git a/tests/test_html.py b/tests/test_html.py index d30279a5f..36bfad08f 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1099,7 +1099,7 @@ def test_filtered_dynamic_contexts(self): cov = coverage.Coverage(source=["."]) cov.set_option("run:dynamic_context", "test_function") cov.set_option("html:show_contexts", True) - cov.set_option("report:contexts", ["*test_one*"]) + cov.set_option("report:contexts", ["test_one"]) mod = self.start_import_stop(cov, "two_tests") d = html_data_from_cov(cov, mod) From 812d523def2374033e90bd99b66807155b7ba88d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 2 Oct 2019 16:19:57 -0400 Subject: [PATCH 656/952] Doc changes for 5.0a8 --- CHANGES.rst | 6 ++++-- README.rst | 2 +- doc/conf.py | 2 +- doc/index.rst | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5e8ac6726..301f606fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,8 +17,10 @@ development at the same time, such as 4.5.x and 5.0. .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- -Unreleased ----------- +.. _changes_50a8: + +Version 5.0a8 --- 2019-10-02 +---------------------------- - The :class:`.CoverageData` API has changed how queries are limited to specific contexts. Now you use :meth:`.CoverageData.set_query_context` to diff --git a/README.rst b/README.rst index aab83ebec..2fdd06203 100644 --- a/README.rst +++ b/README.rst @@ -20,7 +20,7 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: * CPython 2.7. -* CPython 3.5 through beta 3.8. +* CPython 3.5 through 3.8. * PyPy2 7.0 and PyPy3 7.0. Documentation is on `Read the Docs`_. Code repository and issue tracker are on diff --git a/doc/conf.py b/doc/conf.py index 2018b57e3..daa8ab334 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -61,7 +61,7 @@ # The short X.Y version. version = '5.0' # CHANGEME # The full version, including alpha/beta/rc tags. -release = '5.0a7' # CHANGEME +release = '5.0a8' # CHANGEME # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/index.rst b/doc/index.rst index 78106b306..fd201e0d2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -28,10 +28,10 @@ not. .. ifconfig:: prerelease - The latest version is coverage.py 5.0a7, released September 21, 2019. + The latest version is coverage.py 5.0a8, released October 2, 2019. It is supported on: - * Python versions 2.7, 3.5, 3.6, 3.7, and beta 3.8. + * Python versions 2.7, 3.5, 3.6, 3.7, and 3.8. * PyPy2 7.0 and PyPy3 7.0. From 671c3800c7a6495e971f682958ca0b52cab5bf3c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 5 Oct 2019 22:06:19 -0400 Subject: [PATCH 657/952] GitHub funding --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 5841ddbc7..5e7ec8760 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: nedbat tidelift: pypi/coverage From c7a086c5a0c8c66deeb7befd8d76183cd45ac74f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 6 Oct 2019 16:12:15 -0400 Subject: [PATCH 658/952] Quiet pylint --- coverage/sqldata.py | 8 +++++--- igor.py | 1 - setup.py | 6 +++--- tests/test_context.py | 3 +++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 044ddbf13..499f43c34 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -256,8 +256,8 @@ def _read_db(self): self._has_arcs = bool(int(row[0])) self._has_lines = not self._has_arcs - for path, id in db.execute("select path, id from file"): - self._file_map[path] = id + for path, file_id in db.execute("select path, id from file"): + self._file_map[path] = file_id def _connect(self): """Get the SqliteDb object to use.""" @@ -482,7 +482,9 @@ def update(self, other_data, aliases=None): re-map paths to match the local machine's. """ if self._debug.should('dataop'): - self._debug.write("Updating with data from %r" % (getattr(other_data, '_filename', '???'),)) + self._debug.write("Updating with data from %r" % ( + getattr(other_data, '_filename', '???'), + )) if self._has_lines and other_data._has_arcs: raise CoverageException("Can't combine arc data with line data") if self._has_arcs and other_data._has_lines: diff --git a/igor.py b/igor.py index 5d7828c32..e86c5ff9e 100644 --- a/igor.py +++ b/igor.py @@ -229,7 +229,6 @@ def do_zip_mods(): def do_install_egg(): """Install the egg1 egg for tests.""" # I am pretty certain there are easier ways to install eggs... - # pylint: disable=import-error,no-name-in-module cur_dir = os.getcwd() os.chdir("tests/eggsrc") with ignore_warnings(): diff --git a/setup.py b/setup.py index c500ae6e3..09d4eb170 100644 --- a/setup.py +++ b/setup.py @@ -11,9 +11,9 @@ # Setuptools has to be imported before distutils, or things break. from setuptools import setup -from distutils.core import Extension # pylint: disable=no-name-in-module, import-error, wrong-import-order -from distutils.command.build_ext import build_ext # pylint: disable=no-name-in-module, import-error, wrong-import-order -from distutils import errors # pylint: disable=no-name-in-module, wrong-import-order +from distutils.core import Extension # pylint: disable=wrong-import-order +from distutils.command.build_ext import build_ext # pylint: disable=wrong-import-order +from distutils import errors # pylint: disable=wrong-import-order # Get or massage our metadata. We exec coverage/version.py so we can avoid diff --git a/tests/test_context.py b/tests/test_context.py index 72739ad01..b002b603e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -73,6 +73,7 @@ def test_combining_line_contexts(self): fblue = full_names['blue.py'] def assert_combined_lines(filename, context, lines): + # pylint: disable=cell-var-from-loop combined.set_query_context(context) self.assertEqual(combined.lines(filename), lines) @@ -97,6 +98,7 @@ def test_combining_arc_contexts(self): fblue = full_names['blue.py'] def assert_combined_lines(filename, context, lines): + # pylint: disable=cell-var-from-loop combined.set_query_context(context) self.assertEqual(combined.lines(filename), lines) @@ -106,6 +108,7 @@ def assert_combined_lines(filename, context, lines): assert_combined_lines(fblue, 'blue', self.LINES) def assert_combined_arcs(filename, context, lines): + # pylint: disable=cell-var-from-loop combined.set_query_context(context) self.assertEqual(combined.arcs(filename), lines) From 0f84f34beeb9539eae771a8749dd4add1d46843d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 6 Oct 2019 17:18:45 -0400 Subject: [PATCH 659/952] Add some needed details to the Coverage docstrings --- coverage/control.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/coverage/control.py b/coverage/control.py index 6830afba7..1a31bba86 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -153,7 +153,8 @@ def __init__( by coverage. Importing measured files before coverage is started can mean that code is missed. - `context` is a string to use as the context label for collected data. + `context` is a string to use as the :ref:`static context ` + label for collected data. .. versionadded:: 4.0 The `concurrency` parameter. @@ -509,7 +510,7 @@ def _atexit(self): self.save() def erase(self): - """Erase previously-collected coverage data. + """Erase previously collected coverage data. This removes the in-memory data collected in this session as well as discarding the data file. @@ -526,14 +527,15 @@ def erase(self): def switch_context(self, new_context): """Switch to a new dynamic context. - `new_context` is a string to use as the context label - for collected data. If a :ref:`static context ` is in - use, the static and dynamic context labels will be joined together with - a pipe character. + `new_context` is a string to use as the + :ref:`dynamic context ` label for collected data. + If a :ref:`static context ` is in use, the static and + dynamic context labels will be joined together with a pipe character. Coverage collection must be started already. .. versionadded:: 5.0 + """ if not self._started: raise CoverageException( # pragma: only jython @@ -792,11 +794,22 @@ def report( If `skip_covered` is true, don't report on files with 100% coverage. + `contexts` is a list of regular expressions. Only data from + :ref:`dynamic contexts ` that match one of those + expressions (using :func:`re.search `) will be + included in the report. + All of the arguments default to the settings read from the :ref:`configuration file `. Returns a float, the total percentage covered. + .. versionadded:: 4.0 + The `skip_covered` parameter. + + .. versionadded:: 5.0 + The `contexts` parameter. + """ with override_config( self, @@ -898,6 +911,8 @@ def json_report( Returns a float, the total percentage covered. + .. versionadded:: 5.0 + """ with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, From 54a783eb61b6b73f2de7766456311647981919cd Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 6 Oct 2019 17:19:00 -0400 Subject: [PATCH 660/952] Slowly making progress on sqldata docstrings --- coverage/sqldata.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 499f43c34..c2285b7ab 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -195,6 +195,7 @@ def __init__(self, basename=None, suffix=None, no_disk=False, warn=None, debug=N self._query_context_ids = None def _choose_filename(self): + """Set self._filename based on inited attributes.""" if self._no_disk: self._filename = ":memory:" else: @@ -204,6 +205,7 @@ def _choose_filename(self): self._filename += "." + suffix def _reset(self): + """Reset our attributes.""" if self._dbs: for db in self._dbs.values(): db.close() @@ -213,6 +215,10 @@ def _reset(self): self._current_context_id = None def _create_db(self): + """Create a db file that doesn't exist yet. + + Initializes the schema and certain metadata. + """ if self._debug.should('dataio'): self._debug.write("Creating data file {!r}".format(self._filename)) self._dbs[get_thread_id()] = db = SqliteDb(self._filename, self._debug) @@ -229,6 +235,7 @@ def _create_db(self): ) def _open_db(self): + """Open an existing db file, and read its metadata.""" if self._debug.should('dataio'): self._debug.write("Opening data file {!r}".format(self._filename)) self._dbs[get_thread_id()] = SqliteDb(self._filename, self._debug) From 8ad2ddf936df8d6083d481f59198af5008ce39f1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 6 Oct 2019 17:25:54 -0400 Subject: [PATCH 661/952] Context-switching plugins should be listed in debug output --- coverage/control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/coverage/control.py b/coverage/control.py index 1a31bba86..474b86823 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -946,6 +946,7 @@ def plugin_info(plugins): ('CTracer', 'available' if CTracer else "unavailable"), ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), ('plugins.configurers', plugin_info(self._plugins.configurers)), + ('plugins.context_switchers', plugin_info(self._plugins.context_switchers)), ('configs_attempted', self.config.attempted_config_files), ('configs_read', self.config.config_files_read), ('config_file', self.config.config_file), From ca8281a3be8569e4740179846280d498cdeed61c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 8 Oct 2019 13:06:46 -0400 Subject: [PATCH 662/952] Bump version --- CHANGES.rst | 6 ++++++ coverage/version.py | 2 +- howto.txt | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 301f606fb..97ac799e4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,12 @@ development at the same time, such as 4.5.x and 5.0. .. Version 7.8.1 --- 2021-07-27 .. ---------------------------- +Unreleased +---------- + +- Nothing yet. + + .. _changes_50a8: Version 5.0a8 --- 2019-10-02 diff --git a/coverage/version.py b/coverage/version.py index ebe82c3be..9ee21faf9 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 0, 0, 'alpha', 8) +version_info = (5, 0, 0, 'alpha', 9) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/howto.txt b/howto.txt index 9b19b72bd..c2d7919df 100644 --- a/howto.txt +++ b/howto.txt @@ -65,6 +65,8 @@ - git tag coverage-3.0.1 - git push - git push --tags +- Update Tidelift: + - make upload_relnotes - Bump version: - coverage/version.py - increment version number @@ -84,8 +86,6 @@ - wait for the new tag build to finish successfully. - visit https://readthedocs.org/dashboard/coverage/advanced/ - change the default version to the new version -- Update Tidelift: - - make upload_relnotes - Visit the fixed issues on GitHub and mention the version it was fixed in. - Announce on coveragepy-announce@googlegroups.com . - Announce on TIP. From b13facec8f8a02d2a99f254d3b3bc92ce1a33bd3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 8 Oct 2019 19:27:11 -0400 Subject: [PATCH 663/952] Sqlite3 info in 'coverage debug sys' --- CHANGES.rst | 2 +- coverage/control.py | 2 ++ coverage/sqldata.py | 12 +++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 97ac799e4..7e607cad9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -20,7 +20,7 @@ development at the same time, such as 4.5.x and 5.0. Unreleased ---------- -- Nothing yet. +- Added sqlite3 module version information to ``coverage debug sys`` output. .. _changes_50a8: diff --git a/coverage/control.py b/coverage/control.py index 474b86823..7be28413b 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -976,6 +976,8 @@ def plugin_info(plugins): if self._inorout: info.extend(self._inorout.sys_info()) + info.extend(CoverageData.sys_info()) + return info diff --git a/coverage/sqldata.py b/coverage/sqldata.py index c2285b7ab..4739292f4 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -3,7 +3,6 @@ """Sqlite coverage data.""" -# TODO: get sys_info for data class, so we can see sqlite version etc # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow # TODO: should writes be batched? @@ -838,6 +837,17 @@ def contexts_by_lineno(self, filename): def run_infos(self): return [] # TODO + @classmethod + def sys_info(cls): + """Our information for Coverage.sys_info. + + Returns a list of (key, value) pairs. + """ + return [ + ('sqlite3_version', sqlite3.version), + ('sqlite3_sqlite_version', sqlite3.sqlite_version), + ] + class SqliteDb(SimpleReprMixin): """A simple abstraction over a SQLite database. From 6de76885ca9da9354cd8f8883be0901dcbf0dbb2 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 8 Oct 2019 22:04:01 -0400 Subject: [PATCH 664/952] Tie up a loose end in the docs --- coverage/sqldata.py | 1 - doc/contexts.rst | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 4739292f4..791dc77bb 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -5,7 +5,6 @@ # TODO: factor out dataop debugging to a wrapper class? # TODO: make sure all dataop debugging is in place somehow -# TODO: should writes be batched? # TODO: run_info import collections diff --git a/doc/contexts.rst b/doc/contexts.rst index d676a9db5..f81d2a389 100644 --- a/doc/contexts.rst +++ b/doc/contexts.rst @@ -99,4 +99,4 @@ Raw data -------- For more advanced reporting or analysis, the .coverage data file is a SQLite -database. TODO: explain the schema. +database. See :ref:`dbschema` for details. From 190d8bcea6085014b2ae6eff7256cc344419b19d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 10 Oct 2019 05:30:00 -0400 Subject: [PATCH 665/952] The C tracer can do dynamic contexts, so don't skip this test --- tests/test_html.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_html.py b/tests/test_html.py index 36bfad08f..7c1c74f0e 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1046,10 +1046,6 @@ def html_data_from_cov(cov, morf): class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML reports with shown contexts.""" - def setUp(self): - if not env.C_TRACER: - self.skipTest("Only the C tracer supports dynamic contexts") - super(HtmlWithContextsTest, self).setUp() SOURCE = """\ def helper(lineno): From 032ac34a2388f60777431bb54cbcd7075dae7afd Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 10 Oct 2019 05:28:39 -0400 Subject: [PATCH 666/952] A script for comparing run times --- lab/compare_times.sh | 63 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100755 lab/compare_times.sh diff --git a/lab/compare_times.sh b/lab/compare_times.sh new file mode 100755 index 000000000..c5f20bd95 --- /dev/null +++ b/lab/compare_times.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +# A suggestion about how to get less hyperfine output: +# https://github.com/sharkdp/hyperfine/issues/223 +HYPERFINE='hyperfine -w 1 -s basic -r 10' + +cat > sourcefile1.py << EOF +import random + +def get_random_number(): + return random.randint(5, 20) +EOF + +cat > test_file1.py << EOF +import pytest +import sourcefile1 + +tests = tuple(f'test{i}' for i in range(1000)) + +@pytest.mark.parametrize("input_str", tests) +def test_speed(input_str): + print(input_str) + number = sourcefile1.get_random_number() + assert number <= 20 + assert number >= 5 +EOF + +rm -f .coveragerc + +$HYPERFINE 'python -m pytest test_file1.py' + +echo "Coverage 4.5.4" +pip install -q coverage==4.5.4 +$HYPERFINE 'python -m coverage run -m pytest test_file1.py' +$HYPERFINE 'python -m coverage run --branch -m pytest test_file1.py' +$HYPERFINE 'python -m pytest --cov=. --cov-report= test_file1.py' +$HYPERFINE 'python -m pytest --cov=. --cov-report= --cov-branch test_file1.py' + +echo "Coverage 5.0a8, no contexts" +pip install -q coverage==5.0a8 +$HYPERFINE 'python -m coverage run -m pytest test_file1.py' +$HYPERFINE 'python -m coverage run --branch -m pytest test_file1.py' +$HYPERFINE 'python -m pytest --cov=. --cov-report= test_file1.py' +$HYPERFINE 'python -m pytest --cov=. --cov-report= --cov-branch test_file1.py' + +echo "Coverage 5.0a8, with test contexts" +cat > .coveragerc < Date: Thu, 10 Oct 2019 06:16:39 -0400 Subject: [PATCH 667/952] Warn if asked to show context but none were measured. #851 --- CHANGES.rst | 6 ++++++ coverage/html.py | 3 +++ tests/test_html.py | 31 ++++++++++++++++++++----------- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7e607cad9..eb130b3e5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,6 +22,12 @@ Unreleased - Added sqlite3 module version information to ``coverage debug sys`` output. +- Asking the HTML report to show contexts (``[html] show_contexts=True`` or + ``coverage html --show-contexts``) will issue a warning if there were no + contexts measured (`issue 851`_). + +.. _issue 851: https://github.com/nedbat/coveragepy/issues/851 + .. _changes_50a8: diff --git a/coverage/html.py b/coverage/html.py index e560f76ba..5b4c6ca9b 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -82,6 +82,9 @@ def __init__(self, cov): self.config = self.coverage.config data = self.coverage.get_data() self.has_arcs = data.has_arcs() + if self.config.show_contexts: + if data.measured_contexts() == set([""]): + self.coverage._warn("No contexts were measured") data.set_query_contexts(self.config.report_contexts) def data_for_file(self, fr, analysis): diff --git a/tests/test_html.py b/tests/test_html.py index 7c1c74f0e..29f81a054 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1035,18 +1035,18 @@ def test_unicode(self): ) -def html_data_from_cov(cov, morf): - """Get HTML report data from a `Coverage` object for a morf.""" - datagen = coverage.html.HtmlDataGeneration(cov) - for fr, analysis in get_analysis_to_report(cov, [morf]): - # This will only loop once, so it's fine to return inside the loop. - file_data = datagen.data_for_file(fr, analysis) - return file_data - - class HtmlWithContextsTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML reports with shown contexts.""" + def html_data_from_cov(self, cov, morf): + """Get HTML report data from a `Coverage` object for a morf.""" + with self.assert_warnings(cov, []): + datagen = coverage.html.HtmlDataGeneration(cov) + for fr, analysis in get_analysis_to_report(cov, [morf]): + # This will only loop once, so it's fine to return inside the loop. + file_data = datagen.data_for_file(fr, analysis) + return file_data + SOURCE = """\ def helper(lineno): x = 2 @@ -1082,7 +1082,7 @@ def test_dynamic_contexts(self): cov.set_option("run:dynamic_context", "test_function") cov.set_option("html:show_contexts", True) mod = self.start_import_stop(cov, "two_tests") - d = html_data_from_cov(cov, mod) + d = self.html_data_from_cov(cov, mod) context_labels = ['(empty)', 'two_tests.test_one', 'two_tests.test_two'] expected_lines = [self.OUTER_LINES, self.TEST_ONE_LINES, self.TEST_TWO_LINES] @@ -1097,10 +1097,19 @@ def test_filtered_dynamic_contexts(self): cov.set_option("html:show_contexts", True) cov.set_option("report:contexts", ["test_one"]) mod = self.start_import_stop(cov, "two_tests") - d = html_data_from_cov(cov, mod) + d = self.html_data_from_cov(cov, mod) context_labels = ['(empty)', 'two_tests.test_one', 'two_tests.test_two'] expected_lines = [[], self.TEST_ONE_LINES, []] for label, expected in zip(context_labels, expected_lines): actual = [ld.number for ld in d.lines if label in (ld.contexts or ())] assert sorted(expected) == sorted(actual) + + def test_no_contexts_warns_no_contexts(self): + # If no contexts were collected, then show_contexts emits a warning. + self.make_file("two_tests.py", self.SOURCE) + cov = coverage.Coverage(source=["."]) + cov.set_option("html:show_contexts", True) + self.start_import_stop(cov, "two_tests") + with self.assert_warnings(cov, ["No contexts were measured"]): + cov.html_report() From f1505afb5212b8291dffe607e6cdfa565fceb359 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 13 Oct 2019 10:56:26 -0400 Subject: [PATCH 668/952] Save copies of support files so we can view gold files properly --- MANIFEST.in | 8 +- Makefile | 1 + tests/gold/README.rst | 18 + tests/gold/html/Makefile | 21 + tests/gold/html/support/coverage_html.js | 584 ++++++++++++++++++ .../jquery.ba-throttle-debounce.min.js | 9 + tests/gold/html/support/jquery.hotkeys.js | 99 +++ tests/gold/html/support/jquery.isonscreen.js | 53 ++ tests/gold/html/support/jquery.min.js | 4 + .../html/support/jquery.tablesorter.min.js | 2 + tests/gold/html/support/keybd_closed.png | Bin 0 -> 112 bytes tests/gold/html/support/keybd_open.png | Bin 0 -> 112 bytes tests/gold/html/support/style.css | 124 ++++ 13 files changed, 919 insertions(+), 4 deletions(-) create mode 100644 tests/gold/README.rst create mode 100644 tests/gold/html/Makefile create mode 100644 tests/gold/html/support/coverage_html.js create mode 100644 tests/gold/html/support/jquery.ba-throttle-debounce.min.js create mode 100644 tests/gold/html/support/jquery.hotkeys.js create mode 100644 tests/gold/html/support/jquery.isonscreen.js create mode 100644 tests/gold/html/support/jquery.min.js create mode 100644 tests/gold/html/support/jquery.tablesorter.min.js create mode 100644 tests/gold/html/support/keybd_closed.png create mode 100644 tests/gold/html/support/keybd_open.png create mode 100644 tests/gold/html/support/style.css diff --git a/MANIFEST.in b/MANIFEST.in index b30f88512..7a84cf207 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -28,21 +28,21 @@ include tox_wheels.ini include .editorconfig include .readthedocs.yml -recursive-include ci *.* +recursive-include ci * exclude ci/*.token recursive-include coverage/fullcoverage *.py recursive-include coverage/ctracer *.c *.h recursive-include doc *.py *.pip *.rst *.txt *.png -recursive-include doc/_static *.* +recursive-include doc/_static * prune doc/_build recursive-include requirements *.pip recursive-include tests *.py *.tok -recursive-include tests/gold *.* -recursive-include tests js/*.* qunit/*.* +recursive-include tests/gold * +recursive-include tests js/* qunit/* prune tests/eggsrc/build global-exclude *.py[co] diff --git a/Makefile b/Makefile index b0f3ccf1e..acd99f12e 100644 --- a/Makefile +++ b/Makefile @@ -28,6 +28,7 @@ clean: -rm -rf .tox_kits -rm -rf .cache .pytest_cache .hypothesis -rm -rf $$TMPDIR/coverage_test + -make -C tests/gold/html clean sterile: clean -rm -rf .tox* diff --git a/tests/gold/README.rst b/tests/gold/README.rst new file mode 100644 index 000000000..5d946fe1a --- /dev/null +++ b/tests/gold/README.rst @@ -0,0 +1,18 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +Gold files +========== + +These are files used in comparisons for some of the tests. Code to support +these comparisons is in tests/goldtest.py. + +If gold tests are failing, it can useful to set the COVERAGE_KEEP_TMP +environment variable. If set, the test working directories at +$TMPDIR/coverage_test are kept after the tests are run, so that you can +manually inspect the differences. + +The saved HTML files in the html directories can't be viewed properly without +the supporting CSS and Javascript files. But we don't want to save copies of +those files in every subdirectory. There's a Makefile in the html directory +for working with the saved copies of the support files. diff --git a/tests/gold/html/Makefile b/tests/gold/html/Makefile new file mode 100644 index 000000000..ef870fc99 --- /dev/null +++ b/tests/gold/html/Makefile @@ -0,0 +1,21 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +help: + @echo "Available targets:" + @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' + +complete: ## Copy support files into directories so the HTML can be viewed properly. + @for sub in *; do \ + if [[ -f $$sub/index.html ]]; then \ + echo Copying into $$sub ; \ + cp -n support/* $$sub ; \ + fi ; \ + done ; \ + true # because the for loop exits with 1 for some reason. + +clean: ## Remove the effects of this Makefile. + git clean -fq . + +update-support: ## Copy latest support files here for posterity. + cp ../../../coverage/htmlfiles/*.{css,js,png} support diff --git a/tests/gold/html/support/coverage_html.js b/tests/gold/html/support/coverage_html.js new file mode 100644 index 000000000..4f6eb89e4 --- /dev/null +++ b/tests/gold/html/support/coverage_html.js @@ -0,0 +1,584 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// Find all the elements with shortkey_* class, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + $("*[class*='shortkey_']").each(function (i, e) { + $.each($(e).attr("class").split(" "), function (i, c) { + if (/^shortkey_/.test(c)) { + $(document).bind('keydown', c.substr(9), function () { + $(e).click(); + }); + } + }); + }); +}; + +// Create the events for the help panel. +coverage.wire_up_help_panel = function () { + $("#keyboard_icon").click(function () { + // Show the help panel, and position it so the keyboard icon in the + // panel is in the same place as the keyboard icon in the header. + $(".help_panel").show(); + var koff = $("#keyboard_icon").offset(); + var poff = $("#panel_icon").position(); + $(".help_panel").offset({ + top: koff.top-poff.top, + left: koff.left-poff.left + }); + }); + $("#panel_icon").click(function () { + $(".help_panel").hide(); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Cache elements. + var table = $("table.index"); + var table_rows = table.find("tbody tr"); + var table_row_names = table_rows.find("td.name a"); + var no_rows = $("#no_rows"); + + // Create a duplicate table footer that we can modify with dynamic summed values. + var table_footer = $("table.index tfoot tr"); + var table_dynamic_footer = table_footer.clone(); + table_dynamic_footer.attr('class', 'total_dynamic hidden'); + table_footer.after(table_dynamic_footer); + + // Observe filter keyevents. + $("#filter").on("keyup change", $.debounce(150, function (event) { + var filter_value = $(this).val(); + + if (filter_value === "") { + // Filter box is empty, remove all filtering. + table_rows.removeClass("hidden"); + + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + + // Hide placeholder, show table. + if (no_rows.length > 0) { + no_rows.hide(); + } + table.show(); + + } + else { + // Filter table items by value. + var hidden = 0; + var shown = 0; + + // Hide / show elements. + $.each(table_row_names, function () { + var element = $(this).parents("tr"); + + if ($(this).text().indexOf(filter_value) === -1) { + // hide + element.addClass("hidden"); + hidden++; + } + else { + // show + element.removeClass("hidden"); + shown++; + } + }); + + // Show placeholder if no rows will be displayed. + if (no_rows.length > 0) { + if (shown === 0) { + // Show placeholder, hide table. + no_rows.show(); + table.hide(); + } + else { + // Hide placeholder, show table. + no_rows.hide(); + table.show(); + } + } + + // Manage dynamic header: + if (hidden > 0) { + // Calculate new dynamic sum values based on visible rows. + for (var column = 2; column < 20; column++) { + // Calculate summed value. + var cells = table_rows.find('td:nth-child(' + column + ')'); + if (!cells.length) { + // No more columns...! + break; + } + + var sum = 0, numer = 0, denom = 0; + $.each(cells.filter(':visible'), function () { + var ratio = $(this).data("ratio"); + if (ratio) { + var splitted = ratio.split(" "); + numer += parseInt(splitted[0], 10); + denom += parseInt(splitted[1], 10); + } + else { + sum += parseInt(this.innerHTML, 10); + } + }); + + // Get footer cell element. + var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); + + // Set value into dynamic footer cell element. + if (cells[0].innerHTML.indexOf('%') > -1) { + // Percentage columns use the numerator and denominator, + // and adapt to the number of decimal places. + var match = /\.([0-9]+)/.exec(cells[0].innerHTML); + var places = 0; + if (match) { + places = match[1].length; + } + var pct = numer * 100 / denom; + footer_cell.text(pct.toFixed(places) + '%'); + } + else { + footer_cell.text(sum); + } + } + + // Hide standard footer, show dynamic footer. + table_footer.addClass("hidden"); + table_dynamic_footer.removeClass("hidden"); + } + else { + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + } + } + })); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + $("#filter").trigger("change"); +}; + +// Loaded on index.html +coverage.index_ready = function ($) { + // Look for a localStorage item containing previous sort settings: + var sort_list = []; + var storage_name = "COVERAGE_INDEX_SORT"; + var stored_list = localStorage.getItem(storage_name); + + if (stored_list) { + sort_list = JSON.parse('[[' + stored_list + ']]'); + } + + // Create a new widget which exists only to save and restore + // the sort order: + $.tablesorter.addWidget({ + id: "persistentSort", + + // Format is called by the widget before displaying: + format: function (table) { + if (table.config.sortList.length === 0 && sort_list.length > 0) { + // This table hasn't been sorted before - we'll use + // our stored settings: + $(table).trigger('sorton', [sort_list]); + } + else { + // This is not the first load - something has + // already defined sorting so we'll just update + // our stored value to match: + sort_list = table.config.sortList; + } + } + }); + + // Configure our tablesorter to handle the variable number of + // columns produced depending on report options: + var headers = []; + var col_count = $("table.index > thead > tr > th").length; + + headers[0] = { sorter: 'text' }; + for (i = 1; i < col_count-1; i++) { + headers[i] = { sorter: 'digit' }; + } + headers[col_count-1] = { sorter: 'percent' }; + + // Enable the table sorter: + $("table.index").tablesorter({ + widgets: ['persistentSort'], + headers: headers + }); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + coverage.wire_up_filter(); + + // Watch for page unload events so we can save the final sort settings: + $(window).unload(function () { + localStorage.setItem(storage_name, sort_list.toString()) + }); +}; + +// -- pyfile stuff -- + +coverage.pyfile_ready = function ($) { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === 'n') { + $(frag).addClass('highlight'); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + $(document) + .bind('keydown', 'j', coverage.to_next_chunk_nicely) + .bind('keydown', 'k', coverage.to_prev_chunk_nicely) + .bind('keydown', '0', coverage.to_top) + .bind('keydown', '1', coverage.to_first_chunk) + ; + + $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); + $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); + $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); + $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + + coverage.init_scroll_markers(); + + // Rebuild scroll markers when the window height changes. + $(window).resize(coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (btn, cls) { + btn = $(btn); + var show = "show_"+cls; + if (btn.hasClass(show)) { + $("#source ." + cls).removeClass(show); + btn.removeClass(show); + } + else { + $("#source ." + cls).addClass(show); + btn.addClass(show); + } + coverage.build_scroll_markers(); +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return $("#t" + n); +}; + +// Return the nth line number div. +coverage.num_elt = function (n) { + return $("#n" + n); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + var klass = line_elt.attr('class'); + if (klass) { + var m = klass.match(/\bshow_\w+\b/); + if (m) { + return m[0]; + } + } + return null; +}; + +coverage.to_next_chunk = function () { + var c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + var c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 0 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Return the line number of the line nearest pixel position pos +coverage.line_at_pos = function (pos) { + var l1 = coverage.line_elt(1), + l2 = coverage.line_elt(2), + result; + if (l1.length && l2.length) { + var l1_top = l1.offset().top, + line_height = l2.offset().top - l1_top, + nlines = (pos - l1_top) / line_height; + if (nlines < 1) { + result = 1; + } + else { + result = Math.ceil(nlines); + } + } + else { + result = 1; + } + return result; +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + var top = coverage.line_elt(coverage.sel_begin); + var next = coverage.line_elt(coverage.sel_end-1); + + return ( + (top.isOnScreen() ? 1 : 0) + + (next.isOnScreen() ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: select the top line on + // the screen. + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (probe_line.length === 0) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + var c = coverage; + + // Highlight the lines in the chunk + $(".linenos .highlight").removeClass("highlight"); + for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { + c.num_elt(probe).addClass("highlight"); + } + + c.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + // Need to move the page. The html,body trick makes it scroll in all + // browsers, got it from http://stackoverflow.com/questions/3042651 + var top = coverage.line_elt(coverage.sel_begin); + var top_pos = parseInt(top.offset().top, 10); + coverage.scroll_window(top_pos - 30); + } +}; + +coverage.scroll_window = function (to_pos) { + $("html,body").animate({scrollTop: to_pos}, 200); +}; + +coverage.finish_scrolling = function () { + $("html,body").stop(true, true); +}; + +coverage.init_scroll_markers = function () { + var c = coverage; + // Init some variables + c.lines_len = $('td.text p').length; + c.body_h = $('body').height(); + c.header_h = $('div#header').height(); + + // Build html + c.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + var c = coverage, + min_line_height = 3, + max_line_height = 10, + visible_window_h = $(window).height(); + + c.lines_to_mark = $('td.text').find('p.show_run, p.show_mis, p.show_exc, p.show_par'); + $('#scroll_marker').remove(); + // Don't build markers if the window has no scroll bar. + if (c.body_h <= visible_window_h) { + return; + } + + $("body").append("

 
"); + var scroll_marker = $('#scroll_marker'), + marker_scale = scroll_marker.height() / c.body_h, + line_height = scroll_marker.height() / c.lines_len; + + // Line height must be between the extremes. + if (line_height > min_line_height) { + if (line_height > max_line_height) { + line_height = max_line_height; + } + } + else { + line_height = min_line_height; + } + + var previous_line = -99, + last_mark, + last_top, + offsets = {}; + + // Calculate line offsets outside loop to prevent relayouts + c.lines_to_mark.each(function() { + offsets[this.id] = $(this).offset().top; + }); + c.lines_to_mark.each(function () { + var id_name = $(this).attr('id'), + line_top = Math.round(offsets[id_name] * marker_scale), + line_number = parseInt(id_name.substring(1, id_name.length)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.css({ + 'height': line_top + line_height - last_top + }); + } + else { + // Add colored line in scroll_marker block. + scroll_marker.append('
'); + last_mark = $('#m' + line_number); + last_mark.css({ + 'height': line_height, + 'top': line_top + }); + last_top = line_top; + } + + previous_line = line_number; + }); +}; diff --git a/tests/gold/html/support/jquery.ba-throttle-debounce.min.js b/tests/gold/html/support/jquery.ba-throttle-debounce.min.js new file mode 100644 index 000000000..648fe5d3c --- /dev/null +++ b/tests/gold/html/support/jquery.ba-throttle-debounce.min.js @@ -0,0 +1,9 @@ +/* + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); diff --git a/tests/gold/html/support/jquery.hotkeys.js b/tests/gold/html/support/jquery.hotkeys.js new file mode 100644 index 000000000..09b21e03c --- /dev/null +++ b/tests/gold/html/support/jquery.hotkeys.js @@ -0,0 +1,99 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ + +(function(jQuery){ + + jQuery.hotkeys = { + version: "0.8", + + specialKeys: { + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" + }, + + shiftNums: { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + } + }; + + function keyHandler( handleObj ) { + // Only care when a possible input has been specified + if ( typeof handleObj.data !== "string" ) { + return; + } + + var origHandler = handleObj.handler, + keys = handleObj.data.toLowerCase().split(" "); + + handleObj.handler = function( event ) { + // Don't fire in text-accepting inputs that we didn't directly bind to + if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || + event.target.type === "text") ) { + return; + } + + // Keypress represents characters, not special keys + var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], + character = String.fromCharCode( event.which ).toLowerCase(), + key, modif = "", possible = {}; + + // check combinations (alt|ctrl|shift+anything) + if ( event.altKey && special !== "alt" ) { + modif += "alt+"; + } + + if ( event.ctrlKey && special !== "ctrl" ) { + modif += "ctrl+"; + } + + // TODO: Need to make sure this works consistently across platforms + if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { + modif += "meta+"; + } + + if ( event.shiftKey && special !== "shift" ) { + modif += "shift+"; + } + + if ( special ) { + possible[ modif + special ] = true; + + } else { + possible[ modif + character ] = true; + possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if ( modif === "shift+" ) { + possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; + } + } + + for ( var i = 0, l = keys.length; i < l; i++ ) { + if ( possible[ keys[i] ] ) { + return origHandler.apply( this, arguments ); + } + } + }; + } + + jQuery.each([ "keydown", "keyup", "keypress" ], function() { + jQuery.event.special[ this ] = { add: keyHandler }; + }); + +})( jQuery ); diff --git a/tests/gold/html/support/jquery.isonscreen.js b/tests/gold/html/support/jquery.isonscreen.js new file mode 100644 index 000000000..0182ebd21 --- /dev/null +++ b/tests/gold/html/support/jquery.isonscreen.js @@ -0,0 +1,53 @@ +/* Copyright (c) 2010 + * @author Laurence Wheway + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * @version 1.2.0 + */ +(function($) { + jQuery.extend({ + isOnScreen: function(box, container) { + //ensure numbers come in as intgers (not strings) and remove 'px' is it's there + for(var i in box){box[i] = parseFloat(box[i])}; + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( box.left+box.width-container.left > 0 && + box.left < container.width+container.left && + box.top+box.height-container.top > 0 && + box.top < container.height+container.top + ) return true; + return false; + } + }) + + + jQuery.fn.isOnScreen = function (container) { + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( $(this).offset().left+$(this).width()-container.left > 0 && + $(this).offset().left < container.width+container.left && + $(this).offset().top+$(this).height()-container.top > 0 && + $(this).offset().top < container.height+container.top + ) return true; + return false; + } +})(jQuery); diff --git a/tests/gold/html/support/jquery.min.js b/tests/gold/html/support/jquery.min.js new file mode 100644 index 000000000..d1608e37f --- /dev/null +++ b/tests/gold/html/support/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; +if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("