From e29eff0b53c4016df33caa715e2765c5d9613928 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 19 Dec 2020 18:28:17 -0500 Subject: [PATCH 01/41] Bump the version --- CHANGES.rst | 6 ++++++ coverage/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a4f13ff05..2f6214c43 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,12 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. .. Version 9.8.1 --- 2027-07-27 .. ---------------------------- +Unreleased +---------- + +Nothing yet. + + .. _changes_531: Version 5.3.1 --- 2020-12-19 diff --git a/coverage/version.py b/coverage/version.py index f10206db2..f6340f423 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, 3, 1, "final", 0) +version_info = (5, 3, 2, "alpha", 0) def _make_version(major, minor, micro, releaselevel, serial): From 34b665620889f5389178d0f37e8061c8ffa5759c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 19 Dec 2020 19:57:32 -0500 Subject: [PATCH 02/41] Update doc packages --- doc/requirements.pip | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/requirements.pip b/doc/requirements.pip index 26d03b8f5..eea4c8f99 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -3,11 +3,10 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ doc8==0.8.1 -pyenchant==3.1.1 -sphinx==2.4.3 +pyenchant==3.2.0 +sphinx==3.3.1 sphinx-rst-builder==0.0.3 -# 5.x requires Sphinx 3 -sphinxcontrib-spelling==4.3.0 +sphinxcontrib-spelling==7.1.0 sphinx_rtd_theme==0.5.0 -sphinx-autobuild==0.7.1 -sphinx-tabs==1.2.0 +sphinx-autobuild==2020.9.1 +sphinx-tabs==1.3.0 From 7493db8309b64a5b1239ad628119094c119532cc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 Dec 2020 18:28:08 -0500 Subject: [PATCH 03/41] Revert "Silence previously unreported pylint warnings" This reverts commit 9169aeadf5cf9e4fc30cd76ef53c0dff2ec946ef. Pylint reports different errors on Mac and Linux! https://github.com/PyCQA/pylint/issues/3489 Put things back to where Mac is a clean report. --- coverage/pytracer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 7d7a519b7..44bfc8d6a 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -107,7 +107,7 @@ def _trace(self, frame, event, arg_unused): if event == 'call': # Should we start a new context? if self.should_start_context and self.context is None: - context_maybe = self.should_start_context(frame) # pylint: disable=not-callable + context_maybe = self.should_start_context(frame) if context_maybe is not None: self.context = context_maybe self.started_context = True @@ -132,15 +132,15 @@ def _trace(self, frame, event, arg_unused): self.cur_file_name = filename disp = self.should_trace_cache.get(filename) if disp is None: - disp = self.should_trace(filename, frame) # pylint: disable=not-callable - self.should_trace_cache[filename] = disp # pylint: disable=unsupported-assignment-operation + disp = self.should_trace(filename, frame) + self.should_trace_cache[filename] = disp self.cur_file_dict = None if disp.trace: tracename = disp.source_filename - if tracename not in self.data: # pylint: disable=unsupported-membership-test - self.data[tracename] = {} # pylint: disable=unsupported-assignment-operation - self.cur_file_dict = self.data[tracename] # pylint: disable=unsubscriptable-object + if tracename not in self.data: + self.data[tracename] = {} + self.cur_file_dict = self.data[tracename] # The call event is really a "start frame" event, and happens for # function calls and re-entering generators. The f_lasti field is # -1 for calls, and a real offset for generators. Use <0 as the @@ -227,7 +227,7 @@ def stop(self): # has changed to None. dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable - self.warn( # pylint: disable=not-callable + self.warn( "Trace function changed, measurement is likely wrong: %r" % (tf,), slug="trace-changed", ) From 40990ace4c08969f3d598399ffb5e2fac54ade66 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 Dec 2020 18:31:23 -0500 Subject: [PATCH 04/41] Run pylint on Mac to keep things consistent --- .github/workflows/quality.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index ad45b2eef..fbd3d8328 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -16,7 +16,10 @@ defaults: jobs: lint: name: Pylint etc - runs-on: ubuntu-latest + # Because pylint can report different things on different OS's (!) + # (https://github.com/PyCQA/pylint/issues/3489), run this on Mac where local + # pylint gets run. + runs-on: macos-latest steps: - name: "Check out the repo" From 12c5fcd57fd1cce3bc3563732f5502f5e943c0e0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2021 11:28:42 -0500 Subject: [PATCH 05/41] This test doesn't work on Mac either. --- tests/test_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_process.py b/tests/test_process.py index 249beb001..52255ac2c 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -743,7 +743,7 @@ def test_fullcoverage(self): # pragma: no metacov self.assertGreater(line_counts(data)['os.py'], 50) @xfail( - env.PYPY3 and (env.PYPYVERSION >= (7, 1, 1)) and env.LINUX, + env.PYPY3 and (env.PYPYVERSION >= (7, 1, 1)), "https://bitbucket.org/pypy/pypy/issues/3074" ) def test_lang_c(self): From 7ff93a9740da5dec4eba6c6cad288d25a472d75a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 20 Dec 2020 20:19:49 -0500 Subject: [PATCH 06/41] Use set literals --- coverage/collector.py | 2 +- coverage/html.py | 2 +- coverage/misc.py | 2 +- coverage/parser.py | 18 +++++++++--------- coverage/phystokens.py | 2 +- coverage/sqldata.py | 4 ++-- tests/plugin1.py | 2 +- tests/test_collector.py | 2 +- tests/test_parser.py | 12 ++++++------ tests/test_plugins.py | 6 +++--- tests/test_results.py | 6 +++--- tests/test_testing.py | 6 +++--- 12 files changed, 32 insertions(+), 32 deletions(-) diff --git a/coverage/collector.py b/coverage/collector.py index 9333d66a8..a4f1790dd 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -55,7 +55,7 @@ class Collector(object): _collectors = [] # The concurrency settings we support here. - SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"]) + SUPPORTED_CONCURRENCIES = {"greenlet", "eventlet", "gevent", "thread"} def __init__( self, should_trace, check_include, should_start_context, file_mapper, diff --git a/coverage/html.py b/coverage/html.py index 247d2ae19..ef50b56b3 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -84,7 +84,7 @@ def __init__(self, cov): data = self.coverage.get_data() self.has_arcs = data.has_arcs() if self.config.show_contexts: - if data.measured_contexts() == set([""]): + if data.measured_contexts() == {""}: self.coverage._warn("No contexts were measured") data.set_query_contexts(self.config.report_contexts) diff --git a/coverage/misc.py b/coverage/misc.py index 5c4381ab6..96573f7a4 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -77,7 +77,7 @@ def new_contract(*args, **kwargs): 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(",")) + argnameset = {name.strip() for name in argnames.split(",")} def _wrapper(*args, **kwargs): vals = [kwargs.get(name) for name in argnameset] assert sum(val is not None for val in vals) == 1 diff --git a/coverage/parser.py b/coverage/parser.py index e3e431490..f5a8ddd92 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -220,7 +220,7 @@ def first_lines(self, lines): Returns a set of the first lines. """ - return set(self.first_line(l) for l in lines) + return {self.first_line(l) for l in lines} def translate_lines(self, lines): """Implement `FileReporter.translate_lines`.""" @@ -520,7 +520,7 @@ class AstArcAnalyzer(object): def __init__(self, text, statements, multiline): self.root_node = ast.parse(neuter_encoding_declaration(text)) # TODO: I think this is happening in too many places. - self.statements = set(multiline.get(l, l) for l in statements) + self.statements = {multiline.get(l, l) for l in statements} self.multiline = multiline if AST_DUMP: # pragma: debugging @@ -626,10 +626,10 @@ def _line__Module(self, node): return 1 # The node types that just flow to the next node with no complications. - OK_TO_DEFAULT = set([ + OK_TO_DEFAULT = { "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global", "Import", "ImportFrom", "Nonlocal", "Pass", "Print", - ]) + } @contract(returns='ArcStarts') def add_arcs(self, node): @@ -661,7 +661,7 @@ def add_arcs(self, node): print("*** Unhandled: {}".format(node)) # Default for simple statements: one exit from this node. - return set([ArcStart(self.line_for_node(node))]) + return {ArcStart(self.line_for_node(node))} @one_of("from_start, prev_starts") @contract(returns='ArcStarts') @@ -677,7 +677,7 @@ def add_body_arcs(self, body, from_start=None, prev_starts=None): """ if prev_starts is None: - prev_starts = set([from_start]) + prev_starts = {from_start} for body_node in body: lineno = self.line_for_node(body_node) first_line = self.multiline.get(lineno, lineno) @@ -890,7 +890,7 @@ def _handle_decorated(self, node): self.add_arc(last, lineno) last = lineno # The body is handled in collect_arcs. - return set([ArcStart(last)]) + return {ArcStart(last)} _handle__ClassDef = _handle_decorated @@ -984,7 +984,7 @@ def _handle__Try(self, node): # If there are `except` clauses, then raises in the try body # will already jump to them. Start this set over for raises in # `except` and `else`. - try_block.raise_from = set([]) + try_block.raise_from = set() else: self.block_stack.pop() @@ -1079,7 +1079,7 @@ def _combine_finally_starts(self, starts, exits): if start.cause is not None: causes.append(start.cause.format(lineno=start.lineno)) cause = " or ".join(causes) - exits = set(ArcStart(xit.lineno, cause) for xit in exits) + exits = {ArcStart(xit.lineno, cause) for xit in exits} return exits @contract(returns='ArcStarts') diff --git a/coverage/phystokens.py b/coverage/phystokens.py index b6866e7dd..54378b3bc 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -87,7 +87,7 @@ def source_token_lines(source): """ - ws_tokens = set([token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL]) + ws_tokens = {token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL} line = [] col = 0 diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 7a3b5c795..b28b83b4f 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -784,7 +784,7 @@ def measured_contexts(self): """ self._start_using() with self._connect() as con: - contexts = set(row[0] for row in con.execute("select distinct(context) from context")) + contexts = {row[0] for row in con.execute("select distinct(context) from context")} return contexts def file_tracer(self, filename): @@ -857,7 +857,7 @@ def lines(self, filename): 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)) + return list({l for l in all_lines if l > 0}) with self._connect() as con: file_id = self._file_id(filename) diff --git a/tests/plugin1.py b/tests/plugin1.py index a070af367..3283fbdae 100644 --- a/tests/plugin1.py +++ b/tests/plugin1.py @@ -44,7 +44,7 @@ def line_number_range(self, frame): class FileReporter(coverage.FileReporter): """Dead-simple FileReporter.""" def lines(self): - return set([105, 106, 107, 205, 206, 207]) + return {105, 106, 107, 205, 206, 207} def coverage_init(reg, options): # pylint: disable=unused-argument diff --git a/tests/test_collector.py b/tests/test_collector.py index 9989b2292..f7e8a4c45 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -45,6 +45,6 @@ def otherfunc(x): self.start_import_stop(cov, "f2") # Double-check that our files were checked. - abs_files = set(os.path.abspath(f) for f in should_trace_hook.filenames) + abs_files = {os.path.abspath(f) for f in should_trace_hook.filenames} self.assertIn(os.path.abspath("f1.py"), abs_files) self.assertIn(os.path.abspath("f2.py"), abs_files) diff --git a/tests/test_parser.py b/tests/test_parser.py index 0e6a0859c..5fb3bc1f6 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -171,11 +171,11 @@ def meth(self): 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]) + raw_statements = {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]) self.assertEqual(parser.raw_statements, raw_statements) - self.assertEqual(parser.statements, set([8])) + self.assertEqual(parser.statements, {8}) def test_class_decorator_pragmas(self): parser = self.parse_source("""\ @@ -188,8 +188,8 @@ class Bar(object): def __init__(self): self.x = 8 """) - self.assertEqual(parser.raw_statements, set([1, 2, 3, 5, 6, 7, 8])) - self.assertEqual(parser.statements, set([1, 2, 3])) + self.assertEqual(parser.raw_statements, {1, 2, 3, 5, 6, 7, 8}) + self.assertEqual(parser.statements, {1, 2, 3}) def test_empty_decorated_function(self): parser = self.parse_source("""\ @@ -463,7 +463,7 @@ def test_missing_line_ending(self): """) parser = self.parse_file("normal.py") - self.assertEqual(parser.statements, set([1])) + self.assertEqual(parser.statements, {1}) self.make_file("abrupt.py", """\ out, err = subprocess.Popen( @@ -476,4 +476,4 @@ def test_missing_line_ending(self): self.assertEqual(f.read()[-1], ")") parser = self.parse_file("abrupt.py") - self.assertEqual(parser.statements, set([1])) + self.assertEqual(parser.statements, {1}) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d14f5c472..1f224695e 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -400,12 +400,12 @@ def test_plugin2_with_branch(self): # have 7 lines in it. If render() was called with line number 4, # then the plugin will claim that lines 4 and 5 were executed. analysis = cov._analyze("foo_7.html") - self.assertEqual(analysis.statements, set([1, 2, 3, 4, 5, 6, 7])) + self.assertEqual(analysis.statements, {1, 2, 3, 4, 5, 6, 7}) # Plugins don't do branch coverage yet. self.assertEqual(analysis.has_arcs(), True) self.assertEqual(analysis.arc_possibilities(), []) - self.assertEqual(analysis.missing, set([1, 2, 3, 6, 7])) + self.assertEqual(analysis.missing, {1, 2, 3, 6, 7}) def test_plugin2_with_text_report(self): self.make_render_and_caller() @@ -553,7 +553,7 @@ def line_number_range(self, frame): class MyReporter(coverage.FileReporter): def lines(self): - return set([99, 999, 9999]) + return {99, 999, 9999} def coverage_init(reg, options): reg.add_file_tracer(Plugin()) diff --git a/tests/test_results.py b/tests/test_results.py index 86806cfd9..377c150bd 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -114,7 +114,7 @@ def test_should_fail_under_invalid_value(): @pytest.mark.parametrize("statements, lines, result", [ - (set([1,2,3,4,5,10,11,12,13,14]), set([1,2,5,10,11,13,14]), "1-2, 5-11, 13-14"), + ({1,2,3,4,5,10,11,12,13,14}, {1,2,5,10,11,13,14}, "1-2, 5-11, 13-14"), ([1,2,3,4,5,10,11,12,13,14,98,99], [1,2,5,10,11,13,14,99], "1-2, 5-11, 13-14, 99"), ([1,2,3,4,98,99,100,101,102,103,104], [1,2,99,102,103,104], "1-2, 99, 102-104"), ([17], [17], "17"), @@ -128,8 +128,8 @@ def test_format_lines(statements, lines, result): @pytest.mark.parametrize("statements, lines, arcs, result", [ ( - set([1,2,3,4,5,10,11,12,13,14]), - set([1,2,5,10,11,13,14]), + {1,2,3,4,5,10,11,12,13,14}, + {1,2,5,10,11,13,14}, (), "1-2, 5-11, 13-14" ), diff --git a/tests/test_testing.py b/tests/test_testing.py index 2fda956bf..c5d464309 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -33,11 +33,11 @@ class TestingTest(TestCase): def test_assert_count_equal(self): self.assertCountEqual(set(), set()) - self.assertCountEqual(set([1,2,3]), set([3,1,2])) + self.assertCountEqual({1,2,3}, {3,1,2}) with self.assertRaises(AssertionError): - self.assertCountEqual(set([1,2,3]), set()) + self.assertCountEqual({1,2,3}, set()) with self.assertRaises(AssertionError): - self.assertCountEqual(set([1,2,3]), set([4,5,6])) + self.assertCountEqual({1,2,3}, {4,5,6}) class CoverageTestTest(CoverageTest): From 051bdf23b5d86a15ada43523c00f8f3462063ad6 Mon Sep 17 00:00:00 2001 From: Judson Neer Date: Tue, 5 Jan 2021 08:53:19 -0800 Subject: [PATCH 07/41] Always output TOTAL line. --- CHANGES.rst | 5 +-- CONTRIBUTORS.txt | 1 + coverage/summary.py | 4 +-- tests/test_api.py | 12 +++++-- tests/test_concurrency.py | 4 +-- tests/test_plugins.py | 2 ++ tests/test_process.py | 12 +++++-- tests/test_summary.py | 73 +++++++++++++++++++++++++-------------- 8 files changed, 76 insertions(+), 37 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2f6214c43..2e68f6b95 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -24,8 +24,9 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. Unreleased ---------- -Nothing yet. - +- The text report produced by ``coverage report`` now always outputs a TOTAL + line, even if only one Python file is reported. This makes regex parsing + of the output easier. Thanks, Judson Neer. .. _changes_531: diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 3e52e45e9..44b4f557d 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -79,6 +79,7 @@ Jon Chappell Jon Dufresne Joseph Tate Josh Williams +Judson Neer Julian Berman Julien Voisin Justas Sadzevičius diff --git a/coverage/summary.py b/coverage/summary.py index 986cd2f2d..0c7fa5519 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -120,8 +120,8 @@ def report(self, morfs, outfile=None): for line in lines: self.writeout(line[0]) - # Write a TOTAl line if we had more than one file. - if self.total.n_files > 1: + # Write a TOTAL line if we had at least one file. + if self.total.n_files > 0: self.writeout(rule) args = ("TOTAL", self.total.n_statements, self.total.n_missing) if self.branches: diff --git a/tests/test_api.py b/tests/test_api.py index 3552f8f48..f8b7b4b2c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -306,9 +306,11 @@ def test_completely_zero_reporting(self): # Name Stmts Miss Cover # -------------------------------- # foo/bar.py 1 1 0% + # -------------------------------- + # TOTAL 1 1 0% - last = self.last_line_squeezed(self.stdout()).replace("\\", "/") - self.assertEqual("foo/bar.py 1 1 0%", last) + last = self.last_line_squeezed(self.stdout()) + self.assertEqual("TOTAL 1 1 0%", last) def test_cov4_data_file(self): cov4_data = ( @@ -587,6 +589,8 @@ def test_source_and_include_dont_conflict(self): Name Stmts Miss Cover --------------------------- b.py 1 0 100% + --------------------------- + TOTAL 1 0 100% """) self.assertEqual(expected, self.stdout()) @@ -1049,6 +1053,8 @@ def pretend_to_be_nose_with_cover(self, erase=False, cd=False): Name Stmts Miss Cover Missing -------------------------------------------- no_biggie.py 4 1 75% 4 + -------------------------------------------- + TOTAL 4 1 75% """)) if cd: os.chdir("..") @@ -1092,6 +1098,8 @@ def pretend_to_be_pytestcov(self, append): Name Stmts Miss Cover ----------------------------- prog.py 4 1 75% + ----------------------------- + TOTAL 4 1 75% """)) self.assert_file_count(".coverage", 0) self.assert_file_count(".coverage.*", 1) diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 7109f1707..2469e2968 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -409,7 +409,7 @@ def try_multiprocessing_code( out = self.run_command("coverage report -m") last_line = self.squeezed_lines(out)[-1] - self.assertRegex(last_line, r"multi.py \d+ 0 100%") + self.assertRegex(last_line, r"TOTAL \d+ 0 100%") def test_multiprocessing_simple(self): nprocs = 3 @@ -466,7 +466,7 @@ def try_multiprocessing_code_with_branching(self, code, expected_out): out = self.run_command("coverage report -m") last_line = self.squeezed_lines(out)[-1] - self.assertRegex(last_line, r"multi.py \d+ 0 \d+ 0 100%") + self.assertRegex(last_line, r"TOTAL \d+ 0 \d+ 0 100%") def test_multiprocessing_with_branching(self): nprocs = 3 diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 1f224695e..813d370e3 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -522,6 +522,8 @@ def coverage_init(reg, options): 'Name Stmts Miss Cover Missing', '-----------------------------------------------', 'unsuspecting.py 6 3 50% 2, 4, 6', + '-----------------------------------------------', + 'TOTAL 6 3 50%', ] self.assertEqual(expected, report) self.assertEqual(total, 50) diff --git a/tests/test_process.py b/tests/test_process.py index 52255ac2c..9fb930dee 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -341,6 +341,8 @@ def test_combine_with_rc(self): Name Stmts Miss Cover ------------------------------- b_or_c.py 8 0 100% + ------------------------------- + TOTAL 8 0 100% """)) def test_combine_with_aliases(self): @@ -1231,7 +1233,7 @@ def setUp(self): def test_report_43_is_ok(self): st, out = self.run_command_status("coverage report --fail-under=43") self.assertEqual(st, 0) - self.assertEqual(self.last_line_squeezed(out), "forty_two_plus.py 7 4 43%") + self.assertEqual(self.last_line_squeezed(out), "TOTAL 7 4 43%") def test_report_43_is_not_ok(self): st, out = self.run_command_status("coverage report --fail-under=44") @@ -1305,6 +1307,8 @@ def test_accented_dot_py(self): u"Name Stmts Miss Cover\n" u"----------------------------\n" u"h\xe2t.py 1 0 100%\n" + u"----------------------------\n" + u"TOTAL 1 0 100%\n" ) if env.PY2: @@ -1348,8 +1352,10 @@ def test_accented_directory(self): report_expected = ( u"Name Stmts Miss Cover\n" u"-----------------------------------\n" - u"\xe2%saccented.py 1 0 100%%\n" % os.sep - ) + u"\xe2%saccented.py 1 0 100%%\n" + u"-----------------------------------\n" + u"TOTAL 1 0 100%%\n" + ) % os.sep if env.PY2: report_expected = report_expected.encode(output_encoding()) diff --git a/tests/test_summary.py b/tests/test_summary.py index eb25a4d80..feaa0fe0b 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -70,13 +70,15 @@ def test_report_just_one(self): # Name Stmts Miss Cover # ------------------------------- # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertNotIn("/coverage/", report) self.assertNotIn("/tests/modules/covmod1.py ", report) self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mycode.py 4 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 4 0 100%") def test_report_wildcard(self): # Try reporting using wildcards to get the modules. @@ -87,13 +89,15 @@ def test_report_wildcard(self): # Name Stmts Miss Cover # ------------------------------- # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertNotIn("/coverage/", report) self.assertNotIn("/tests/modules/covmod1.py ", report) self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mycode.py 4 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 4 0 100%") def test_report_omitting(self): # Try reporting while omitting some modules @@ -105,13 +109,15 @@ def test_report_omitting(self): # Name Stmts Miss Cover # ------------------------------- # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertNotIn("/coverage/", report) self.assertNotIn("/tests/modules/covmod1.py ", report) self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mycode.py 4 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 4 0 100%") def test_report_including(self): # Try reporting while including some modules @@ -122,13 +128,15 @@ def test_report_including(self): # Name Stmts Miss Cover # ------------------------------- # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertNotIn("/coverage/", report) self.assertNotIn("/tests/modules/covmod1.py ", report) self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mycode.py 4 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 4 0 100%") def test_run_source_vs_report_include(self): # https://github.com/nedbat/coveragepy/issues/621 @@ -179,11 +187,13 @@ def branch(x): # Name Stmts Miss Branch BrPart Cover # ----------------------------------------------- - # mybranch.py 5 0 2 1 85% + # mybranch.py 5 0 2 1 86% + # ----------------------------------------------- + # TOTAL 5 0 2 1 86% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertIn("mybranch.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mybranch.py 5 0 2 1 86%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 5 0 2 1 86%") def test_report_show_missing(self): self.make_file("mymissing.py", """\ @@ -209,10 +219,13 @@ def missing(x, y): # Name Stmts Miss Cover Missing # -------------------------------------------- # mymissing.py 14 3 79% 3-4, 10 + # -------------------------------------------- + # TOTAL 14 3 79% 3-4, 10 - self.assertEqual(self.line_count(report), 3) - self.assertIn("mymissing.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mymissing.py 14 3 79% 3-4, 10") + self.assertEqual(self.line_count(report), 5) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[2], "mymissing.py 14 3 79% 3-4, 10") + self.assertEqual(squeezed[4], "TOTAL 14 3 79%") def test_report_show_missing_branches(self): self.make_file("mybranch.py", """\ @@ -231,10 +244,13 @@ def branch(x, y): # Name Stmts Miss Branch BrPart Cover Missing # ---------------------------------------------------------- # mybranch.py 6 0 4 2 80% 2->4, 4->exit + # ---------------------------------------------------------- + # TOTAL 6 0 4 2 80% - self.assertEqual(self.line_count(report), 3) - self.assertIn("mybranch.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mybranch.py 6 0 4 2 80% 2->4, 4->exit") + self.assertEqual(self.line_count(report), 5) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[2], "mybranch.py 6 0 4 2 80% 2->4, 4->exit") + self.assertEqual(squeezed[4], "TOTAL 6 0 4 2 80%") def test_report_show_missing_branches_and_lines(self): self.make_file("main.py", """\ @@ -394,12 +410,14 @@ def foo(): # Name Stmts Miss Branch BrPart Cover # ------------------------------------------- + # ----------------------------------------- + # TOTAL 3 0 0 0 100% # # 1 file skipped due to complete coverage. - self.assertEqual(self.line_count(report), 4, report) + self.assertEqual(self.line_count(report), 6, report) squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + self.assertEqual(squeezed[5], "1 file skipped due to complete coverage.") def test_report_skip_covered_longfilename(self): self.make_file("long_______________filename.py", """ @@ -413,14 +431,16 @@ def foo(): # Name Stmts Miss Branch BrPart Cover # ----------------------------------------- + # ----------------------------------------- + # TOTAL 3 0 0 0 100% # # 1 file skipped due to complete coverage. - self.assertEqual(self.line_count(report), 4, report) + self.assertEqual(self.line_count(report), 6, report) lines = self.report_lines(report) self.assertEqual(lines[0], "Name Stmts Miss Branch BrPart Cover") squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + self.assertEqual(squeezed[5], "1 file skipped due to complete coverage.") def test_report_skip_covered_no_data(self): report = self.report_from_command("coverage report --skip-covered") @@ -472,9 +492,10 @@ def test_report_skip_empty_no_data(self): # # 1 empty file skipped. - self.assertEqual(self.line_count(report), 4, report) - lines = self.report_lines(report) - self.assertEqual(lines[3], "1 empty file skipped.") + self.assertEqual(self.line_count(report), 6, report) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[3], "TOTAL 0 0 100%") + self.assertEqual(squeezed[5], "1 empty file skipped.") def test_report_precision(self): self.make_file(".coveragerc", """\ @@ -621,7 +642,7 @@ def test_report_no_extension(self): out = self.run_command("coverage run --source=. xxx") self.assertEqual(out, "xxx: 3 4 0 7\n") report = self.report_from_command("coverage report") - self.assertEqual(self.last_line_squeezed(report), "xxx 7 1 86%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 7 1 86%") def test_report_with_chdir(self): self.make_file("chdir.py", """\ @@ -635,7 +656,7 @@ def test_report_with_chdir(self): out = self.run_command("coverage run --source=. chdir.py") self.assertEqual(out, "Line One\nLine Two\nhello\n") report = self.report_from_command("coverage report") - self.assertEqual(self.last_line_squeezed(report), "chdir.py 5 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 5 0 100%") def get_report(self, cov): """Get the report from `cov`, and canonicalize it.""" From f0930815952f9fb90ff038e5335932e5a474bb28 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2021 18:40:46 -0500 Subject: [PATCH 08/41] Say a little more about always outputing TOTAL --- CHANGES.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 2e68f6b95..ae10409c1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,7 +26,13 @@ Unreleased - The text report produced by ``coverage report`` now always outputs a TOTAL line, even if only one Python file is reported. This makes regex parsing - of the output easier. Thanks, Judson Neer. + of the output easier. Thanks, Judson Neer. This had been requested a number + of times (`issue 1086`_, `issue 922`_, `issue 732`_). + +.. _issue 1086: https://github.com/nedbat/coveragepy/issues/1086 +.. _issue 732: https://github.com/nedbat/coveragepy/issues/732 +.. _issue 922: https://github.com/nedbat/coveragepy/issues/922 + .. _changes_531: From 812d978f95560d1b8e86032681c531e6332ad76d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 9 Jan 2021 09:11:52 -0500 Subject: [PATCH 09/41] tox_wheels.ini was deleted in November --- MANIFEST.in | 1 - 1 file changed, 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 60da201de..049ee1fd9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -22,7 +22,6 @@ include metacov.ini include pylintrc include setup.py include tox.ini -include tox_wheels.ini include .editorconfig include .readthedocs.yml From fddad86fa5412da63a048012e33e52fb3855523f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 9 Jan 2021 09:12:38 -0500 Subject: [PATCH 10/41] Indicate the git revision if running a local build --- igor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/igor.py b/igor.py index 31d4bacc2..1df63eca2 100644 --- a/igor.py +++ b/igor.py @@ -328,6 +328,10 @@ def print_banner(label): if '__pypy__' in sys.builtin_module_names: version += " (pypy %s)" % ".".join(str(v) for v in sys.pypy_version_info) + rev = platform.python_revision() + if rev: + version += " (rev {})".format(rev) + try: which_python = os.path.relpath(sys.executable) except ValueError: From 331f66ba52c08add313a932badc03d7523685c7c Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 20 Dec 2020 18:36:43 -0500 Subject: [PATCH 11/41] Tests of statements after raise and return --- tests/test_coverage.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 02b577e52..89c8e9f4e 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -368,6 +368,16 @@ def test_raise(self): """, [1,2,5,6], "") + def test_raise_followed_by_statement(self): + self.check_coverage("""\ + try: + raise Exception("hello") + a = 3 + except: + pass + """, + [1,2,3,4,5], "3") + def test_return(self): self.check_coverage("""\ def fn(): @@ -401,6 +411,18 @@ def fn(): """, [1,2,3,7,8], "") + def test_return_followed_by_statement(self): + self.check_coverage("""\ + def fn(): + a = 1 + return a + a = 2 + + x = fn() + assert(x == 1) + """, + [1,2,3,4,6,7], "4") + def test_yield(self): self.check_coverage("""\ def gen(): From d230aca5a299373d784a1eb29e64587308726ca9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 20 Dec 2020 18:38:08 -0500 Subject: [PATCH 12/41] So that set_env can show me one more variable --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 077d4c1fd..317dea42f 100644 --- a/tox.ini +++ b/tox.ini @@ -48,6 +48,7 @@ commands = python igor.py test_with_tracer c {posargs} [testenv:anypy] +# $set_env.py: COVERAGE_PYTHON - The custom Python for "tox -e anypy" # For running against my own builds of CPython, or any other specific Python. basepython = {env:COVERAGE_PYTHON} From 868d86878da0446c2776b008652baa6e5e87feb7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 21 Dec 2020 07:15:11 -0500 Subject: [PATCH 13/41] show_pyc: Flags changed in 3.9 --- lab/show_pyc.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/lab/show_pyc.py b/lab/show_pyc.py index 7573c1c31..b4dc7cd7c 100644 --- a/lab/show_pyc.py +++ b/lab/show_pyc.py @@ -1,6 +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 +""" +Dump the contents of a .pyc file. + +The output will only be correct if run with the same version of Python that +produced the .pyc. + +""" + import binascii import dis import marshal @@ -55,15 +63,31 @@ def show_py_text(text, fname=""): ('CO_ITERABLE_COROUTINE', 0x00100), ('CO_ASYNC_GENERATOR', 0x00200), ('CO_GENERATOR_ALLOWED', 0x01000), - ('CO_FUTURE_DIVISION', 0x02000), - ('CO_FUTURE_ABSOLUTE_IMPORT', 0x04000), - ('CO_FUTURE_WITH_STATEMENT', 0x08000), - ('CO_FUTURE_PRINT_FUNCTION', 0x10000), - ('CO_FUTURE_UNICODE_LITERALS', 0x20000), - ('CO_FUTURE_BARRY_AS_BDFL', 0x40000), - ('CO_FUTURE_GENERATOR_STOP', 0x80000), ] +if sys.version_info < (3, 9): + CO_FLAGS += [ + ('CO_FUTURE_DIVISION', 0x02000), + ('CO_FUTURE_ABSOLUTE_IMPORT', 0x04000), + ('CO_FUTURE_WITH_STATEMENT', 0x08000), + ('CO_FUTURE_PRINT_FUNCTION', 0x10000), + ('CO_FUTURE_UNICODE_LITERALS', 0x20000), + ('CO_FUTURE_BARRY_AS_BDFL', 0x40000), + ('CO_FUTURE_GENERATOR_STOP', 0x80000), + ] +else: + CO_FLAGS += [ + ('CO_FUTURE_DIVISION', 0x0020000), + ('CO_FUTURE_ABSOLUTE_IMPORT', 0x0040000), + ('CO_FUTURE_WITH_STATEMENT', 0x0080000), + ('CO_FUTURE_PRINT_FUNCTION', 0x0100000), + ('CO_FUTURE_UNICODE_LITERALS', 0x0200000), + ('CO_FUTURE_BARRY_AS_BDFL', 0x0400000), + ('CO_FUTURE_GENERATOR_STOP', 0x0800000), + ('CO_FUTURE_ANNOTATIONS', 0x1000000), + ] + + def show_code(code, indent='', number=None): label = "" if number is not None: From 71053d9757bc0299e4825df62d54ae2799c5f139 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 21 Dec 2020 07:16:43 -0500 Subject: [PATCH 14/41] show_pyc: Use 4-space indents --- lab/show_pyc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lab/show_pyc.py b/lab/show_pyc.py index b4dc7cd7c..d0ed8224c 100644 --- a/lab/show_pyc.py +++ b/lab/show_pyc.py @@ -93,7 +93,7 @@ def show_code(code, indent='', number=None): if number is not None: label = "%d: " % number print("%s%scode" % (indent, label)) - indent += ' ' + indent += " " print("%sname %r" % (indent, code.co_name)) print("%sargcount %d" % (indent, code.co_argcount)) print("%snlocals %d" % (indent, code.co_nlocals)) @@ -104,9 +104,9 @@ def show_code(code, indent='', number=None): print("%sconsts" % indent) for i, const in enumerate(code.co_consts): if type(const) == types.CodeType: - show_code(const, indent+' ', number=i) + show_code(const, indent+" ", number=i) else: - print(" %s%d: %r" % (indent, i, const)) + print(" %s%d: %r" % (indent, i, const)) print("%snames %r" % (indent, code.co_names)) print("%svarnames %r" % (indent, code.co_varnames)) print("%sfreevars %r" % (indent, code.co_freevars)) From 840537a7dc0cbaaea5b65ee02a463d559afa20bc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 21 Dec 2020 07:35:21 -0500 Subject: [PATCH 15/41] show_pyc: Interpret co_lnotab, and show co_lines() --- lab/show_pyc.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lab/show_pyc.py b/lab/show_pyc.py index d0ed8224c..2e21eb643 100644 --- a/lab/show_pyc.py +++ b/lab/show_pyc.py @@ -114,6 +114,14 @@ def show_code(code, indent='', number=None): print("%sfilename %r" % (indent, code.co_filename)) print("%sfirstlineno %d" % (indent, code.co_firstlineno)) show_hex("lnotab", code.co_lnotab, indent=indent) + print(" %s%s" % (indent, ", ".join("%r:%r" % (line, byte) for byte, line in lnotab_interpreted(code)))) + if hasattr(code, "co_linetable"): + show_hex("linetable", code.co_linetable, indent=indent) + if hasattr(code, "co_lines"): + print(" %sco_lines %s" % ( + indent, + ", ".join("%r:%r-%r" % (line, start, end) for start, end, line in code.co_lines()) + )) def show_hex(label, h, indent): h = binascii.hexlify(h) @@ -124,6 +132,34 @@ def show_hex(label, h, indent): for i in range(0, len(h), 60): print("%s %s" % (indent, h[i:i+60].decode('ascii'))) +if sys.version_info >= (3,): + def bytes_to_ints(bytes_value): + return bytes_value +else: + def bytes_to_ints(bytes_value): + for byte in bytes_value: + yield ord(byte) + +def lnotab_interpreted(code): + # Adapted from dis.py in the standard library. + byte_increments = bytes_to_ints(code.co_lnotab[0::2]) + line_increments = bytes_to_ints(code.co_lnotab[1::2]) + + last_line_num = None + line_num = code.co_firstlineno + byte_num = 0 + for byte_incr, line_incr in zip(byte_increments, line_increments): + if byte_incr: + if line_num != last_line_num: + yield (byte_num, line_num) + last_line_num = line_num + byte_num += byte_incr + if sys.version_info >= (3, 6) and line_incr >= 0x80: + line_incr -= 0x100 + line_num += line_incr + if line_num != last_line_num: + yield (byte_num, line_num) + def flag_words(flags, flag_defs): words = [] for word, flag in flag_defs: From 70e3345811b945fdcfe4f92ba534ed2d8ad850af Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 21 Dec 2020 21:33:59 -0500 Subject: [PATCH 16/41] 2506 is fixed? --- tests/test_coverage.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 89c8e9f4e..5d5c6ee5a 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -457,11 +457,12 @@ def test_continue(self): """, [1,2,3,4,5], "4") - def test_strange_unexecuted_continue(self): # pragma: not covered + def test_strange_unexecuted_continue(self): # Peephole optimization of jumps to jumps can mean that some statements # never hit the line tracer. The behavior is different in different - # versions of Python, so don't run this test: - self.skipTest("Expected failure: peephole optimization of jumps to jumps") + # versions of Python, so be careful when running this test. + if env.PY2: + self.skipTest("Expected failure: peephole optimization of jumps to jumps") self.check_coverage("""\ a = b = c = 0 for n in range(100): @@ -485,7 +486,9 @@ def test_strange_unexecuted_continue(self): # pragma: not covered c += 1 assert a == 33 and b == 50 and c == 50 """, - [1,2,3,4,5,6,8,9,10, 12,13,14,15,16,17,19,20,21], "") + lines=[1,2,3,4,5,6,8,9,10, 12,13,14,15,16,17,19,20,21], + missing=["", "6"], + ) def test_import(self): self.check_coverage("""\ From 92dae9e44caa538f184585f4d907d2124732f8a0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 22 Dec 2020 14:29:29 -0500 Subject: [PATCH 17/41] Use co_lines() if it's available --- coverage/parser.py | 55 +++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index f5a8ddd92..007f7599a 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -389,34 +389,35 @@ def child_parsers(self): """ return (ByteParser(self.text, code=c) for c in code_objects(self.code)) - def _bytes_lines(self): - """Map byte offsets to line numbers in `code`. - - Uses co_lnotab described in Python/compile.c to map byte offsets to - line numbers. Produces a sequence: (b0, l0), (b1, l1), ... - - Only byte offsets that correspond to line numbers are included in the - results. + def _line_numbers(self): + """Yield the line numbers possible in this code object. + Uses co_lnotab described in Python/compile.c to find the + line numbers. Produces a sequence: l0, l1, ... """ - # Adapted from dis.py in the standard library. - byte_increments = bytes_to_ints(self.code.co_lnotab[0::2]) - line_increments = bytes_to_ints(self.code.co_lnotab[1::2]) - - last_line_num = None - line_num = self.code.co_firstlineno - byte_num = 0 - for byte_incr, line_incr in zip(byte_increments, line_increments): - if byte_incr: - if line_num != last_line_num: - yield (byte_num, line_num) - last_line_num = line_num - byte_num += byte_incr - if env.PYBEHAVIOR.negative_lnotab and line_incr >= 0x80: - line_incr -= 0x100 - line_num += line_incr - if line_num != last_line_num: - yield (byte_num, line_num) + if hasattr(self.code, "co_lines"): + for _, _, line in self.code.co_lines(): + if line is not None: + yield line + else: + # Adapted from dis.py in the standard library. + byte_increments = bytes_to_ints(self.code.co_lnotab[0::2]) + line_increments = bytes_to_ints(self.code.co_lnotab[1::2]) + + last_line_num = None + line_num = self.code.co_firstlineno + byte_num = 0 + for byte_incr, line_incr in zip(byte_increments, line_increments): + if byte_incr: + if line_num != last_line_num: + yield line_num + last_line_num = line_num + byte_num += byte_incr + if env.PYBEHAVIOR.negative_lnotab and line_incr >= 0x80: + line_incr -= 0x100 + line_num += line_incr + if line_num != last_line_num: + yield line_num def _find_statements(self): """Find the statements in `self.code`. @@ -427,7 +428,7 @@ def _find_statements(self): """ for bp in self.child_parsers(): # Get all of the lineno information from this code. - for _, l in bp._bytes_lines(): + for l in bp._line_numbers(): yield l From 0482302f1b2afbf9e638db713a7aa11cb272792d Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 23 Dec 2020 16:23:25 -0500 Subject: [PATCH 18/41] Python 3.10 doesn't compile statments after unconditional jumps. This includes break/continue/return/raise. --- coverage/env.py | 7 ++ tests/test_arcs.py | 37 +++++++-- tests/test_coverage.py | 168 +++++++++++++++++++++-------------------- 3 files changed, 123 insertions(+), 89 deletions(-) diff --git a/coverage/env.py b/coverage/env.py index 80153ecf1..5c612413a 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -85,6 +85,13 @@ class PYBEHAVIOR(object): # Python 3.9a1 made sys.argv[0] and other reported files absolute paths. report_absolute_files = (PYVERSION >= (3, 9)) + # Lines after break/continue/return/raise are no longer compiled into the + # bytecode. They used to be marked as missing, now they aren't executable. + omit_after_jump = pep626 + + # PyPy has always omitted statements after return. + omit_after_return = omit_after_jump or PYPY + # Coverage.py specifics. # Are we using the C-implemented trace function? diff --git a/tests/test_arcs.py b/tests/test_arcs.py index c16f3fa7e..849dbc4a0 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -215,6 +215,13 @@ def test_nested_loop(self): ) def test_break(self): + if env.PYBEHAVIOR.omit_after_jump: + arcz = ".1 12 23 35 15 5." + arcz_missing = "15" + else: + arcz = ".1 12 23 35 15 41 5." + arcz_missing = "15 41" + self.check_coverage("""\ for i in range(10): a = i @@ -222,9 +229,17 @@ def test_break(self): a = 99 assert a == 0 # 5 """, - arcz=".1 12 23 35 15 41 5.", arcz_missing="15 41") + arcz=arcz, arcz_missing=arcz_missing + ) def test_continue(self): + if env.PYBEHAVIOR.omit_after_jump: + arcz = ".1 12 23 31 15 5." + arcz_missing = "" + else: + arcz = ".1 12 23 31 15 41 5." + arcz_missing = "41" + self.check_coverage("""\ for i in range(10): a = i @@ -232,7 +247,8 @@ def test_continue(self): a = 99 assert a == 9 # 5 """, - arcz=".1 12 23 31 15 41 5.", arcz_missing="41") + arcz=arcz, arcz_missing=arcz_missing + ) def test_nested_breaks(self): self.check_coverage("""\ @@ -495,6 +511,14 @@ def test_try_except(self): assert a == 3 and b == 1 """, arcz=".1 12 23 36 45 56 6.", arcz_missing="45 56") + + def test_raise_followed_by_statement(self): + if env.PYBEHAVIOR.omit_after_jump: + arcz = ".1 12 23 34 46 67 78 8." + arcz_missing = "" + else: + arcz = ".1 12 23 34 46 58 67 78 8." + arcz_missing = "58" self.check_coverage("""\ a, b = 1, 1 try: @@ -505,8 +529,7 @@ def test_try_except(self): b = 7 assert a == 3 and b == 7 """, - arcz=".1 12 23 34 46 58 67 78 8.", - arcz_missing="58", + arcz=arcz, arcz_missing=arcz_missing, ) def test_hidden_raise(self): @@ -579,15 +602,15 @@ def test_try_finally(self): try: a = 4 raise Exception("Yikes!") - a = 6 + # line 6 finally: c = 8 except: d = 10 # A assert a == 4 and c == 8 and d == 10 # B """, - arcz=".1 12 23 34 45 58 68 89 8B 9A AB B.", - arcz_missing="68 8B", + arcz=".1 12 23 34 45 58 89 9A AB B.", + arcz_missing="", ) def test_finally_in_loop(self): diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 5d5c6ee5a..b56d3b59e 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -369,6 +369,12 @@ def test_raise(self): [1,2,5,6], "") def test_raise_followed_by_statement(self): + if env.PYBEHAVIOR.omit_after_jump: + lines = [1,2,4,5] + missing = "" + else: + lines = [1,2,3,4,5] + missing = "3" self.check_coverage("""\ try: raise Exception("hello") @@ -376,7 +382,7 @@ def test_raise_followed_by_statement(self): except: pass """, - [1,2,3,4,5], "3") + lines=lines, missing=missing) def test_return(self): self.check_coverage("""\ @@ -412,16 +418,23 @@ def fn(): [1,2,3,7,8], "") def test_return_followed_by_statement(self): + if env.PYBEHAVIOR.omit_after_return: + lines = [1,2,3,6,7] + missing = "" + else: + lines = [1,2,3,4,6,7] + missing = "4" self.check_coverage("""\ def fn(): - a = 1 - return a a = 2 + return a + a = 4 x = fn() - assert(x == 1) + assert(x == 2) """, - [1,2,3,4,6,7], "4") + lines=lines, missing=missing, + ) def test_yield(self): self.check_coverage("""\ @@ -438,6 +451,13 @@ def gen(): [1,2,3,6,8,9], "") def test_break(self): + if env.PYBEHAVIOR.omit_after_jump: + lines = [1,2,3,5] + missing = "" + else: + lines = [1,2,3,4,5] + missing = "4" + self.check_coverage("""\ for x in range(10): a = 2 + x @@ -445,9 +465,16 @@ def test_break(self): a = 4 assert a == 2 """, - [1,2,3,4,5], "4") + lines=lines, missing=missing) def test_continue(self): + if env.PYBEHAVIOR.omit_after_jump: + lines = [1,2,3,5] + missing = "" + else: + lines = [1,2,3,4,5] + missing = "4" + self.check_coverage("""\ for x in range(10): a = 2 + x @@ -455,7 +482,7 @@ def test_continue(self): a = 4 assert a == 11 """, - [1,2,3,4,5], "4") + lines=lines, missing=missing) def test_strange_unexecuted_continue(self): # Peephole optimization of jumps to jumps can mean that some statements @@ -920,10 +947,9 @@ def test_while(self): while a: b += 1 break - b = 99 assert a == 3 and b == 1 """, - [1,2,3,4,5,6], "5") + [1,2,3,4,5], "") def test_while_else(self): # Take the else branch. @@ -944,12 +970,11 @@ def test_while_else(self): b += 1 a -= 1 break - b = 123 else: b = 99 assert a == 2 and b == 1 """, - [1,2,3,4,5,6,8,9], "6-8") + [1,2,3,4,5,7,8], "7") def test_split_while(self): self.check_coverage("""\ @@ -994,10 +1019,9 @@ def test_for(self): for i in [1,2,3,4,5]: a += i break - a = 99 assert a == 1 """, - [1,2,3,4,5,6], "5") + [1,2,3,4,5], "") def test_for_else(self): self.check_coverage("""\ @@ -1014,12 +1038,11 @@ def test_for_else(self): for i in range(5): a += i+1 break - a = 99 else: a = 123 assert a == 1 """, - [1,2,3,4,5,7,8], "5-7") + [1,2,3,4,6,7], "6") def test_split_for(self): self.check_coverage("""\ @@ -1101,6 +1124,19 @@ def test_try_except(self): arcz=".1 12 23 45 58 37 78 8.", arcz_missing="45 58", ) + + def test_try_except_stranded_else(self): + if env.PYBEHAVIOR.omit_after_jump: + # The else can't be reached because the try ends with a raise. + lines = [1,2,3,4,5,6,9] + missing = "" + arcz = ".1 12 23 34 45 56 69 9." + arcz_missing = "" + else: + lines = [1,2,3,4,5,6,8,9] + missing = "8" + arcz = ".1 12 23 34 45 56 69 89 9." + arcz_missing = "89" self.check_coverage("""\ a = 0 try: @@ -1112,9 +1148,10 @@ def test_try_except(self): a = 123 assert a == 99 """, - [1,2,3,4,5,6,8,9], "8", - arcz=".1 12 23 34 45 56 69 89 9.", - arcz_missing="89", + lines=lines, + missing=missing, + arcz=arcz, + arcz_missing=arcz_missing, ) def test_try_finally(self): @@ -1380,12 +1417,11 @@ def test_excluding_for_else(self): for i in range(5): a += i+1 break - a = 99 else: #pragma: NO COVER a = 123 assert a == 1 """, - [1,2,3,4,5,8], "5", excludes=['#pragma: NO COVER']) + [1,2,3,4,7], "", excludes=['#pragma: NO COVER']) def test_excluding_while(self): self.check_coverage("""\ @@ -1393,10 +1429,9 @@ def test_excluding_while(self): while a*b: #pragma: NO COVER b += 1 break - b = 99 assert a == 3 and b == 0 """, - [1,6], "", excludes=['#pragma: NO COVER']) + [1,5], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 3; b = 0 while ( @@ -1404,10 +1439,9 @@ def test_excluding_while(self): ): #pragma: NO COVER b += 1 break - b = 99 assert a == 3 and b == 0 """, - [1,8], "", excludes=['#pragma: NO COVER']) + [1,7], "", excludes=['#pragma: NO COVER']) def test_excluding_while_else(self): self.check_coverage("""\ @@ -1415,12 +1449,11 @@ def test_excluding_while_else(self): while a: b += 1 break - b = 99 else: #pragma: NO COVER b = 123 assert a == 3 and b == 1 """, - [1,2,3,4,5,8], "5", excludes=['#pragma: NO COVER']) + [1,2,3,4,7], "", excludes=['#pragma: NO COVER']) def test_excluding_try_except(self): self.check_coverage("""\ @@ -1468,58 +1501,15 @@ def test_excluding_try_except(self): arcz=".1 12 23 37 45 58 78 8.", arcz_missing="45 58", ) - self.check_coverage("""\ - a = 0 - try: - a = 1 - raise Exception("foo") - except: - a = 99 - else: #pragma: NO COVER - a = 123 - assert a == 99 - """, - [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER'], - arcz=".1 12 23 34 45 56 69 89 9.", - arcz_missing="89", - ) - def test_excluding_try_except_pass(self): - self.check_coverage("""\ - a = 0 - try: - a = 1 - except: #pragma: NO COVER - x = 2 - assert a == 1 - """, - [1,2,3,6], "", excludes=['#pragma: NO COVER']) - self.check_coverage("""\ - a = 0 - try: - a = 1 - raise Exception("foo") - except ImportError: #pragma: NO COVER - x = 2 - except: - a = 123 - assert a == 123 - """, - [1,2,3,4,7,8,9], "", excludes=['#pragma: NO COVER']) - self.check_coverage("""\ - a = 0 - try: - a = 1 - except: #pragma: NO COVER - x = 2 - else: - a = 123 - assert a == 123 - """, - [1,2,3,7,8], "", excludes=['#pragma: NO COVER'], - arcz=".1 12 23 37 45 58 78 8.", - arcz_missing="45 58", - ) + def test_excluding_try_except_stranded_else(self): + if env.PYBEHAVIOR.omit_after_jump: + # The else can't be reached because the try ends with a raise. + arcz = ".1 12 23 34 45 56 69 9." + arcz_missing = "" + else: + arcz = ".1 12 23 34 45 56 69 89 9." + arcz_missing = "89" self.check_coverage("""\ a = 0 try: @@ -1532,8 +1522,8 @@ def test_excluding_try_except_pass(self): assert a == 99 """, [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER'], - arcz=".1 12 23 34 45 56 69 89 9.", - arcz_missing="89", + arcz=arcz, + arcz_missing=arcz_missing, ) def test_excluding_if_pass(self): @@ -1800,6 +1790,19 @@ def test_try_except_finally(self): arcz=".1 12 23 37 45 59 79 9A A.", arcz_missing="45 59", ) + + def test_try_except_finally_stranded_else(self): + if env.PYBEHAVIOR.omit_after_jump: + # The else can't be reached because the try ends with a raise. + lines = [1,2,3,4,5,6,10,11] + missing = "" + arcz = ".1 12 23 34 45 56 6A AB B." + arcz_missing = "" + else: + lines = [1,2,3,4,5,6,8,10,11] + missing = "8" + arcz = ".1 12 23 34 45 56 6A 8A AB B." + arcz_missing = "8A" self.check_coverage("""\ a = 0; b = 0 try: @@ -1813,9 +1816,10 @@ def test_try_except_finally(self): b = 2 assert a == 99 and b == 2 """, - [1,2,3,4,5,6,8,10,11], "8", - arcz=".1 12 23 34 45 56 6A 8A AB B.", - arcz_missing="8A", + lines=lines, + missing=missing, + arcz=arcz, + arcz_missing=arcz_missing, ) From 13f15b0e4316865287d4b5ae1d4f856dac9f9ecb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 23 Dec 2020 17:34:09 -0500 Subject: [PATCH 19/41] Py 3.10 doesn't jump back from finally any more --- coverage/env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage/env.py b/coverage/env.py index 5c612413a..7b845f669 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -72,7 +72,7 @@ class PYBEHAVIOR(object): # 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)) + finally_jumps_back = ((3, 8) <= PYVERSION < (3, 10)) # 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 From bd90b202587dd9f1e2e1319c1c542545c7263dd3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 Dec 2020 18:26:00 -0500 Subject: [PATCH 20/41] In 3.10, modules always have firstlineno==1 --- coverage/env.py | 4 ++++ coverage/parser.py | 10 +++++++++- tests/coveragetest.py | 5 +++++ tests/test_arcs.py | 4 +++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/coverage/env.py b/coverage/env.py index 7b845f669..f2881e324 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -92,6 +92,10 @@ class PYBEHAVIOR(object): # PyPy has always omitted statements after return. omit_after_return = omit_after_jump or PYPY + # Modules used to have firstlineno equal to the line number of the first + # real line of code. Now they always start at 1. + module_firstline_1 = pep626 + # Coverage.py specifics. # Are we using the C-implemented trace function? diff --git a/coverage/parser.py b/coverage/parser.py index 007f7599a..6a3ca2fc4 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -205,6 +205,12 @@ def _raw_parse(self): if not empty: self.raw_statements.update(self.byte_parser._find_statements()) + # The first line of modules can lie and say 1 always, even if the first + # line of code is later. If so, map 1 to the actual first line of the + # module. + if env.PYBEHAVIOR.module_firstline_1 and self._multiline: + self._multiline[1] = min(self.raw_statements) + def first_line(self, line): """Return the first line number of the statement including `line`.""" if line < 0: @@ -620,7 +626,9 @@ def _line__List(self, node): return node.lineno def _line__Module(self, node): - if node.body: + if env.PYBEHAVIOR.module_firstline_1: + return 1 + elif node.body: return self.line_for_node(node.body[0]) else: # Empty modules have no line number, they always start at 1. diff --git a/tests/coveragetest.py b/tests/coveragetest.py index f4961ed9b..b2763b042 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -219,6 +219,11 @@ def check_coverage( self.fail("None of the missing choices matched %r" % missing_formatted) if arcs is not None: + # print("Possible arcs:") + # print(" expected:", arcs) + # print(" actual:", analysis.arc_possibilities()) + # print("Executed:") + # print(" actual:", sorted(set(analysis.arcs_executed()))) with self.delayed_assertions(): self.assert_equal_arcs( arcs, analysis.arc_possibilities(), diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 849dbc4a0..b927526be 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -25,6 +25,7 @@ def test_simple_sequence(self): b = 3 """, arcz=".1 13 3.") + line1 = 1 if env.PYBEHAVIOR.module_firstline_1 else 2 self.check_coverage("""\ a = 2 @@ -32,7 +33,8 @@ def test_simple_sequence(self): c = 5 """, - arcz="-22 23 35 5-2") + arcz="-{0}2 23 35 5-{0}".format(line1) + ) def test_function_def(self): self.check_coverage("""\ From c7649842853de89c9a67c3f361f967349b17cb5f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 31 Dec 2020 18:34:37 -0500 Subject: [PATCH 21/41] A simple tool to see branch tracing arcs --- lab/branch_trace.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 lab/branch_trace.py diff --git a/lab/branch_trace.py b/lab/branch_trace.py new file mode 100644 index 000000000..7e8e88f9a --- /dev/null +++ b/lab/branch_trace.py @@ -0,0 +1,17 @@ +import sys + +pairs = set() +last = -1 + +def trace(frame, event, arg): + global last + if event == "line": + this = frame.f_lineno + pairs.add((last, this)) + last = this + return trace + +code = open(sys.argv[1]).read() +sys.settrace(trace) +exec(code) +print(sorted(pairs)) From 0ac78b400e6d45debc626748c1c94b9e1868c645 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 10 Jan 2021 08:33:07 -0500 Subject: [PATCH 22/41] Clean up the platform constants in env.py --- coverage/env.py | 13 +++++++------ tests/test_process.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/coverage/env.py b/coverage/env.py index f2881e324..b91af463d 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -11,24 +11,25 @@ WINDOWS = sys.platform == "win32" LINUX = sys.platform.startswith("linux") +# Python implementations. +CPYTHON = (platform.python_implementation() == "CPython") +PYPY = (platform.python_implementation() == "PyPy") +JYTHON = (platform.python_implementation() == "Jython") +IRONPYTHON = (platform.python_implementation() == "IronPython") + # Python versions. We amend version_info with one more value, a zero if an # official version, or 1 if built from source beyond an official version. PYVERSION = sys.version_info + (int(platform.python_version()[-1] == "+"),) PY2 = PYVERSION < (3, 0) PY3 = PYVERSION >= (3, 0) -# Python implementations. -PYPY = (platform.python_implementation() == 'PyPy') if PYPY: PYPYVERSION = sys.pypy_version_info PYPY2 = PYPY and PY2 PYPY3 = PYPY and PY3 -JYTHON = (platform.python_implementation() == 'Jython') -IRONPYTHON = (platform.python_implementation() == 'IronPython') - -# Python behavior +# Python behavior. class PYBEHAVIOR(object): """Flags indicating this Python's behavior.""" diff --git a/tests/test_process.py b/tests/test_process.py index 9fb930dee..e48861568 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1099,7 +1099,7 @@ def excepthook(*args): self.assertEqual(line_counts(data)['excepthook.py'], 7) def test_excepthook_exit(self): - if env.PYPY or env.JYTHON: + if not env.CPYTHON: self.skipTest("non-CPython handles excepthook exits differently, punt for now.") self.make_file("excepthook_exit.py", """\ import sys From 3adae9b5cdf67f7364607e3ca7307fa6ebbe1b08 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 10 Jan 2021 08:33:34 -0500 Subject: [PATCH 23/41] COVERAGE_ONE_TRACER runs just one tracer It chooses the appropriate tracer based on the implementation. --- igor.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/igor.py b/igor.py index 1df63eca2..b2dc05cfe 100644 --- a/igor.py +++ b/igor.py @@ -22,6 +22,12 @@ import pytest +# Contants derived the same as in coverage/env.py. We can't import +# that file here, it would be evaluated too early and not get the +# settings we make in this file. + +CPYTHON = (platform.python_implementation() == "CPython") +PYPY = (platform.python_implementation() == "PyPy") @contextlib.contextmanager def ignore_warnings(): @@ -73,7 +79,18 @@ def label_for_tracer(tracer): def should_skip(tracer): """Is there a reason to skip these tests?""" - if tracer == "py": + skipper = "" + + # $set_env.py: COVERAGE_ONE_TRACER - Only run tests for one tracer. + only_one = os.environ.get("COVERAGE_ONE_TRACER") + if only_one: + if CPYTHON: + if tracer == "py": + skipper = "Only one tracer: no Python tracer for CPython" + else: + if tracer == "c": + skipper = "No C tracer for {}".format(platform.python_implementation()) + elif 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: @@ -94,7 +111,7 @@ 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: + if PYPY: version += "_%s%s" % sys.pypy_version_info[:2] env_id = "%s%s_%s" % (impl, version, tracer) return env_id @@ -108,6 +125,7 @@ def run_tests(tracer, *runner_args): if 'COVERAGE_ENV_ID' in os.environ: os.environ['COVERAGE_ENV_ID'] = make_env_id(tracer) print_banner(label_for_tracer(tracer)) + return pytest.main(list(runner_args)) @@ -325,7 +343,7 @@ def print_banner(label): version = platform.python_version() - if '__pypy__' in sys.builtin_module_names: + if PYPY: version += " (pypy %s)" % ".".join(str(v) for v in sys.pypy_version_info) rev = platform.python_revision() From e3f8053d805f8930ffc8424ac098576457c5f506 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 2 Jan 2021 10:38:56 -0500 Subject: [PATCH 24/41] PEP 626: constant tests are kept as no-ops The conditionals are now getting unwieldy, perhaps we can simplify them in the future? --- coverage/env.py | 20 +++++- coverage/parser.py | 7 +- setup.cfg | 2 + tests/coveragetest.py | 2 +- tests/test_arcs.py | 141 +++++++++++++++++++++++++++++++++-------- tests/test_coverage.py | 19 ++++-- 6 files changed, 155 insertions(+), 36 deletions(-) diff --git a/coverage/env.py b/coverage/env.py index b91af463d..e9c65bb11 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -33,14 +33,27 @@ class PYBEHAVIOR(object): """Flags indicating this Python's behavior.""" + pep626 = CPYTHON and (PYVERSION > (3, 10, 0, 'alpha', 4)) + # Is "if __debug__" optimized away? - optimize_if_debug = (not PYPY) + if PYPY3: + optimize_if_debug = True + elif PYPY2: + optimize_if_debug = False + else: + optimize_if_debug = not pep626 # Is "if not __debug__" optimized away? optimize_if_not_debug = (not PYPY) and (PYVERSION >= (3, 7, 0, 'alpha', 4)) + if pep626: + optimize_if_not_debug = False + if PYPY3: + optimize_if_not_debug = True # Is "if not __debug__" optimized away even better? optimize_if_not_debug2 = (not PYPY) and (PYVERSION >= (3, 8, 0, 'beta', 1)) + if pep626: + optimize_if_not_debug2 = False # Do we have yield-from? yield_from = (PYVERSION >= (3, 3)) @@ -67,7 +80,7 @@ class PYBEHAVIOR(object): # 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. - actual_syspath0_dash_m = (not PYPY) and (PYVERSION >= (3, 7, 0, 'beta', 3)) + actual_syspath0_dash_m = CPYTHON and (PYVERSION >= (3, 7, 0, 'beta', 3)) # 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 @@ -97,6 +110,9 @@ class PYBEHAVIOR(object): # real line of code. Now they always start at 1. module_firstline_1 = pep626 + # Are "if 0:" lines (and similar) kept in the compiled code? + keep_constant_test = pep626 + # Coverage.py specifics. # Are we using the C-implemented trace function? diff --git a/coverage/parser.py b/coverage/parser.py index 6a3ca2fc4..1e307c41d 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -1118,9 +1118,14 @@ def _handle__TryFinally(self, node): @contract(returns='ArcStarts') def _handle__While(self, node): - constant_test = self.is_constant_expr(node.test) start = to_top = self.line_for_node(node.test) + constant_test = self.is_constant_expr(node.test) + top_is_body0 = False if constant_test and (env.PY3 or constant_test == "Num"): + top_is_body0 = True + if env.PYBEHAVIOR.keep_constant_test: + top_is_body0 = False + if top_is_body0: to_top = self.line_for_node(node.body[0]) self.block_stack.append(LoopBlock(start=to_top)) from_start = ArcStart(start, cause="the condition on line {lineno} was never true") diff --git a/setup.cfg b/setup.cfg index 16e2bc6cc..2d015d954 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,6 +6,8 @@ markers = filterwarnings = ignore:dns.hash module will be removed:DeprecationWarning ignore:Using or importing the ABCs:DeprecationWarning +# xfail tests that pass should fail the test suite +xfail_strict=true [pep8] # E265 block comment should start with '# ' diff --git a/tests/coveragetest.py b/tests/coveragetest.py index b2763b042..dbadd226b 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -509,5 +509,5 @@ def command_line(args): def xfail(condition, reason): - """A decorator to mark as test as expected to fail.""" + """A decorator to mark a test as expected to fail.""" return pytest.mark.xfail(condition, reason=reason, strict=True) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index b927526be..fb958a66a 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -3,7 +3,9 @@ """Tests for coverage.py's arc measurement.""" -from tests.coveragetest import CoverageTest +import pytest + +from tests.coveragetest import CoverageTest, xfail import coverage from coverage import env @@ -264,12 +266,17 @@ def test_nested_breaks(self): """, arcz=".1 12 23 34 45 25 56 51 67 17 7.", arcz_missing="17 25") - def test_while_true(self): + def test_while_1(self): # With "while 1", the loop knows it's constant. - if env.PYBEHAVIOR.nix_while_true: + if env.PYBEHAVIOR.keep_constant_test: + arcz = ".1 12 23 34 45 36 62 57 7." + arcz_missing = "" + elif env.PYBEHAVIOR.nix_while_true: arcz = ".1 13 34 45 36 63 57 7." + arcz_missing = "" else: arcz = ".1 12 23 34 45 36 63 57 7." + arcz_missing = "" self.check_coverage("""\ a, i = 1, 0 while 1: @@ -280,10 +287,15 @@ def test_while_true(self): assert a == 4 and i == 3 """, arcz=arcz, + arcz_missing=arcz_missing, ) + + def test_while_true(self): # With "while True", 2.x thinks it's computation, # 3.x thinks it's constant. - if env.PYBEHAVIOR.nix_while_true: + if env.PYBEHAVIOR.keep_constant_test: + arcz = ".1 12 23 34 45 36 62 57 7." + elif 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." @@ -311,7 +323,9 @@ def method(self): """) out = self.run_command("coverage run --branch --source=. main.py") self.assertEqual(out, 'done\n') - if env.PYBEHAVIOR.nix_while_true: + if env.PYBEHAVIOR.keep_constant_test: + num_stmts = 3 + elif env.PYBEHAVIOR.nix_while_true: num_stmts = 2 else: num_stmts = 3 @@ -323,7 +337,9 @@ def method(self): def test_bug_496_continue_in_constant_while(self): # https://github.com/nedbat/coveragepy/issues/496 # A continue in a while-true needs to jump to the right place. - if env.PYBEHAVIOR.nix_while_true: + if env.PYBEHAVIOR.keep_constant_test: + arcz = ".1 12 23 34 45 52 46 67 7." + elif 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." @@ -1111,38 +1127,75 @@ class OptimizedIfTest(CoverageTest): """Tests of if statements being optimized away.""" def test_optimized_away_if_0(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 3, 4, 8, 9] + arcz = ".1 12 23 24 34 48 49 89 9." + arcz_missing = "24" + # 49 isn't missing because line 4 is matched by the default partial + # exclusion regex, and no branches are considered missing if they + # start from an excluded line. + else: + lines = [1, 2, 3, 8, 9] + arcz = ".1 12 23 28 38 89 9." + arcz_missing = "28" + self.check_coverage("""\ a = 1 if len([2]): c = 3 - if 0: # this line isn't in the compiled code. + if 0: if len([5]): d = 6 else: e = 8 f = 9 """, - lines=[1, 2, 3, 8, 9], - arcz=".1 12 23 28 38 89 9.", - arcz_missing="28", + lines=lines, + arcz=arcz, + arcz_missing=arcz_missing, ) def test_optimized_away_if_1(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 3, 4, 5, 6, 9] + arcz = ".1 12 23 24 34 45 49 56 69 59 9." + arcz_missing = "24 59" + # 49 isn't missing because line 4 is matched by the default partial + # exclusion regex, and no branches are considered missing if they + # start from an excluded line. + else: + lines = [1, 2, 3, 5, 6, 9] + arcz = ".1 12 23 25 35 56 69 59 9." + arcz_missing = "25 59" + self.check_coverage("""\ a = 1 if len([2]): c = 3 - if 1: # this line isn't in the compiled code, - if len([5]): # but these are. + if 1: + if len([5]): d = 6 else: e = 8 f = 9 """, - lines=[1, 2, 3, 5, 6, 9], - arcz=".1 12 23 25 35 56 69 59 9.", - arcz_missing="25 59", + lines=lines, + arcz=arcz, + arcz_missing=arcz_missing, ) + + def test_optimized_away_if_1_no_else(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 3, 4, 5] + arcz = ".1 12 23 25 34 45 5." + arcz_missing = "" + # 25 isn't missing because line 2 is matched by the default partial + # exclusion regex, and no branches are considered missing if they + # start from an excluded line. + else: + lines = [1, 3, 4, 5] + arcz = ".1 13 34 45 5." + arcz_missing = "" self.check_coverage("""\ a = 1 if 1: @@ -1150,11 +1203,24 @@ def test_optimized_away_if_1(self): c = 4 d = 5 """, - lines=[1, 3, 4, 5], - arcz=".1 13 34 45 5.", + lines=lines, + arcz=arcz, + arcz_missing=arcz_missing, ) - def test_optimized_nested(self): + def test_optimized_if_nested(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 8, 11, 12, 13, 14, 15] + arcz = ".1 12 28 2F 8B 8F BC CD DE EF F." + arcz_missing = "" + # 2F and 8F aren't missing because they're matched by the default + # partial exclusion regex, and no branches are considered missing + # if they start from an excluded line. + else: + lines = [1, 12, 14, 15] + arcz = ".1 1C CE EF F." + arcz_missing = "" + self.check_coverage("""\ a = 1 if 0: @@ -1172,14 +1238,34 @@ def test_optimized_nested(self): h = 14 i = 15 """, - lines=[1, 12, 14, 15], - arcz=".1 1C CE EF F.", + lines=lines, + arcz=arcz, + arcz_missing=arcz_missing, ) + def test_dunder_debug(self): + # Since some of our tests use __debug__, let's make sure it is true as + # we expect + assert __debug__ + # Check that executed code has __debug__ + self.check_coverage("""\ + assert __debug__, "assert __debug__" + """ + ) + # Check that if it didn't have debug, it would let us know. + with pytest.raises(AssertionError): + self.check_coverage("""\ + assert not __debug__, "assert not __debug__" + """ + ) + def test_if_debug(self): - if not env.PYBEHAVIOR.optimize_if_debug: - self.skipTest("PyPy doesn't optimize away 'if __debug__:'") - # CPython optimizes away "if __debug__:" + if env.PYBEHAVIOR.optimize_if_debug: + arcz = ".1 12 24 41 26 61 1." + arcz_missing = "" + else: + arcz = ".1 12 23 31 34 41 26 61 1." + arcz_missing = "31" self.check_coverage("""\ for value in [True, False]: if value: @@ -1188,14 +1274,13 @@ def test_if_debug(self): else: x = 6 """, - arcz=".1 12 24 41 26 61 1.", + arcz=arcz, + arcz_missing=arcz_missing, ) + @xfail(env.PYBEHAVIOR.pep626, reason="https://bugs.python.org/issue42803") def test_if_not_debug(self): - # Before 3.7, no Python optimized away "if not __debug__:" - if not env.PYBEHAVIOR.optimize_if_debug: - self.skipTest("PyPy doesn't optimize away 'if __debug__:'") - elif env.PYBEHAVIOR.optimize_if_not_debug2: + if env.PYBEHAVIOR.optimize_if_not_debug2: arcz = ".1 12 24 41 26 61 1." arcz_missing = "" elif env.PYBEHAVIOR.optimize_if_not_debug: diff --git a/tests/test_coverage.py b/tests/test_coverage.py index b56d3b59e..a6c8f4924 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -8,7 +8,7 @@ from coverage import env from coverage.misc import CoverageException -from tests.coveragetest import CoverageTest +from tests.coveragetest import CoverageTest, xfail class TestCoverageTest(CoverageTest): @@ -622,7 +622,9 @@ def test_extra_doc_string(self): b = 3 assert (a,b) == (1,3) """, - [1,3,4], "") + ([1,3,4], [1,2,3,4]), + "", + ) self.check_coverage("""\ a = 1 "An extra docstring, should be a comment." @@ -632,7 +634,9 @@ def test_extra_doc_string(self): c = 6 assert (a,b,c) == (1,3,6) """, - ([1,3,6,7], [1,3,5,6,7], [1,3,4,5,6,7]), "") + ([1,3,6,7], [1,3,5,6,7], [1,3,4,5,6,7], [1,2,3,4,5,6,7]), + "", + ) def test_nonascii(self): self.check_coverage("""\ @@ -675,6 +679,7 @@ def test_statement_list(self): """, [1,2,3,5], "") + @xfail(env.PYBEHAVIOR.pep626, reason="pep626: https://bugs.python.org/issue42810") def test_if(self): self.check_coverage("""\ a = 1 @@ -926,12 +931,18 @@ def test_absurd_split_if(self): [1,2,4,5,7,9,10], "4, 7") def test_constant_if(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 3] + else: + lines = [2, 3] self.check_coverage("""\ if 1: a = 2 assert a == 2 """, - [2,3], "") + lines, + "", + ) def test_while(self): self.check_coverage("""\ From 5c3d0946821118ad11f5b10753c638a446f24bfe Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 5 Jan 2021 07:15:16 -0500 Subject: [PATCH 25/41] PEP 626: Docstring-only functions changed again --- coverage/env.py | 3 +++ tests/test_parser.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/coverage/env.py b/coverage/env.py index e9c65bb11..ea78a5be8 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -82,6 +82,9 @@ class PYBEHAVIOR(object): # affect the outcome. actual_syspath0_dash_m = CPYTHON and (PYVERSION >= (3, 7, 0, 'beta', 3)) + # 3.7 changed how functions with only docstrings are numbered. + docstring_only_function = (not PYPY) and ((3, 7, 0, 'beta', 5) <= PYVERSION <= (3, 10)) + # 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 diff --git a/tests/test_parser.py b/tests/test_parser.py index 5fb3bc1f6..98308df97 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -214,7 +214,7 @@ def bar(self): expected_arcs = set(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 (not env.PYPY) and (env.PYVERSION >= (3, 7, 0, 'beta', 5)): + if env.PYBEHAVIOR.docstring_only_function: # 3.7 changed how functions with only docstrings are numbered. expected_arcs.update(set(arcz_to_arcs("-46 6-4"))) expected_exits.update({6: 1}) @@ -394,6 +394,7 @@ def function(): "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 7e5e28f1aba87c10b96d0ae1244352f4c520aedc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2021 10:13:54 -0500 Subject: [PATCH 26/41] Use the supported way to get a C frame's lineno See https://bugs.python.org/issue42823 for discussion. --- coverage/ctracer/tracer.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 045523524..00e4218d8 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -305,7 +305,7 @@ CTracer_check_missing_return(CTracer *self, PyFrameObject *frame) goto error; } } - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "missedreturn"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "missedreturn"); self->pdata_stack->depth--; self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; } @@ -529,13 +529,13 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) self->pcur_entry->file_data = file_data; self->pcur_entry->file_tracer = file_tracer; - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, filename, "traced"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), filename, "traced"); } else { Py_XDECREF(self->pcur_entry->file_data); self->pcur_entry->file_data = NULL; self->pcur_entry->file_tracer = Py_None; - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, filename, "skipped"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), filename, "skipped"); } self->pcur_entry->disposition = disposition; @@ -552,7 +552,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) self->pcur_entry->last_line = -frame->f_code->co_firstlineno; } else { - self->pcur_entry->last_line = frame->f_lineno; + self->pcur_entry->last_line = PyFrame_GetLineNumber(frame); } ok: @@ -633,7 +633,7 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) STATS( self->stats.lines++; ) if (self->pdata_stack->depth >= 0) { - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "line"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "line"); if (self->pcur_entry->file_data) { int lineno_from = -1; int lineno_to = -1; @@ -655,7 +655,7 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) } } else { - lineno_from = lineno_to = frame->f_lineno; + lineno_from = lineno_to = PyFrame_GetLineNumber(frame); } if (lineno_from != -1) { @@ -744,7 +744,7 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame) } /* Pop the stack. */ - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "return"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "return"); self->pdata_stack->depth--; self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; } @@ -807,14 +807,14 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse #if WHAT_LOG if (what <= (int)(sizeof(what_sym)/sizeof(const char *))) { ascii = MyText_AS_BYTES(frame->f_code->co_filename); - printf("trace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), frame->f_lineno); + printf("trace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame)); Py_DECREF(ascii); } #endif #if TRACE_LOG ascii = MyText_AS_BYTES(frame->f_code->co_filename); - if (strstr(MyBytes_AS_STRING(ascii), start_file) && frame->f_lineno == start_line) { + if (strstr(MyBytes_AS_STRING(ascii), start_file) && PyFrame_GetLineNumber(frame) == start_line) { logging = TRUE; } Py_DECREF(ascii); @@ -931,7 +931,7 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) #if WHAT_LOG ascii = MyText_AS_BYTES(frame->f_code->co_filename); - printf("pytrace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), frame->f_lineno); + printf("pytrace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame)); Py_DECREF(ascii); #endif From 9a6248090ba51ab8aedaa0ed69363f58d4100cfb Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Wed, 6 Jan 2021 19:56:12 -0500 Subject: [PATCH 27/41] A better test for 'if not __debug__' --- coverage/parser.py | 4 +--- tests/test_arcs.py | 24 +++++++++++++----------- tests/test_coverage.py | 3 +-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/coverage/parser.py b/coverage/parser.py index 1e307c41d..9c7a8d1e4 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -338,9 +338,7 @@ def missing_arc_description(self, start, end, executed_arcs=None): fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)]) msgs = [] - for fragment_pair in fragment_pairs: - smsg, emsg = fragment_pair - + for smsg, emsg in fragment_pairs: if emsg is None: if end < 0: # Hmm, maybe we have a one-line callable, let's check. diff --git a/tests/test_arcs.py b/tests/test_arcs.py index fb958a66a..f3aa8ebb9 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -5,7 +5,7 @@ import pytest -from tests.coveragetest import CoverageTest, xfail +from tests.coveragetest import CoverageTest import coverage from coverage import env @@ -1278,24 +1278,26 @@ def test_if_debug(self): arcz_missing=arcz_missing, ) - @xfail(env.PYBEHAVIOR.pep626, reason="https://bugs.python.org/issue42803") def test_if_not_debug(self): - if env.PYBEHAVIOR.optimize_if_not_debug2: - arcz = ".1 12 24 41 26 61 1." - arcz_missing = "" + arcz_missing = "" + if env.PYBEHAVIOR.pep626: + arcz = ".1 12 23 34 42 37 72 28 8." + elif env.PYBEHAVIOR.optimize_if_not_debug2: + arcz = ".1 12 23 35 52 37 72 28 8." elif env.PYBEHAVIOR.optimize_if_not_debug: - arcz = ".1 12 23 31 26 61 1." - arcz_missing = "" + arcz = ".1 12 23 34 42 37 72 28 8." else: - arcz = ".1 12 23 31 34 41 26 61 1." - arcz_missing = "34 41" + arcz = ".1 12 23 34 45 42 52 37 72 28 8." + arcz_missing = "45 52" self.check_coverage("""\ + lines = set() for value in [True, False]: if value: if not __debug__: - x = 4 + lines.add(5) else: - x = 6 + lines.add(7) + assert lines == set([7]) """, arcz=arcz, arcz_missing=arcz_missing, diff --git a/tests/test_coverage.py b/tests/test_coverage.py index a6c8f4924..68eea1150 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -8,7 +8,7 @@ from coverage import env from coverage.misc import CoverageException -from tests.coveragetest import CoverageTest, xfail +from tests.coveragetest import CoverageTest class TestCoverageTest(CoverageTest): @@ -679,7 +679,6 @@ def test_statement_list(self): """, [1,2,3,5], "") - @xfail(env.PYBEHAVIOR.pep626, reason="pep626: https://bugs.python.org/issue42810") def test_if(self): self.check_coverage("""\ a = 1 From f7126b2efe318275a2ed675a8ca636976fd5e79a Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 7 Jan 2021 09:34:25 -0500 Subject: [PATCH 28/41] Update the support files for HTML gold files --- tests/gold/html/support/coverage_html.js | 11 +- tests/gold/html/support/style.css | 269 ++++++++++++++++++----- 2 files changed, 226 insertions(+), 54 deletions(-) diff --git a/tests/gold/html/support/coverage_html.js b/tests/gold/html/support/coverage_html.js index 22152333e..6bc9fdf59 100644 --- a/tests/gold/html/support/coverage_html.js +++ b/tests/gold/html/support/coverage_html.js @@ -172,7 +172,10 @@ 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); + var stored_list = undefined; + try { + stored_list = localStorage.getItem(storage_name); + } catch(err) {} if (stored_list) { sort_list = JSON.parse('[[' + stored_list + ']]'); @@ -221,8 +224,10 @@ coverage.index_ready = function ($) { 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()) + $(window).on("unload", function () { + try { + localStorage.setItem(storage_name, sort_list.toString()) + } catch(err) {} }); }; diff --git a/tests/gold/html/support/style.css b/tests/gold/html/support/style.css index e8ff57657..3e7f9b66b 100644 --- a/tests/gold/html/support/style.css +++ b/tests/gold/html/support/style.css @@ -4,11 +4,17 @@ /* 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; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } -body { font-family: georgia, serif; font-size: 1em; } +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } html > body { font-size: 16px; } -p { font-size: .75em; line-height: 1.33333333em; } +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } table { border-collapse: collapse; } @@ -19,106 +25,267 @@ 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; } -.indexfile #footer { margin: 1em 3em; } +@media (prefers-color-scheme: dark) { #header { background: black; } } -.pyfile #footer { margin: 1em 1em; } +@media (prefers-color-scheme: dark) { #header { border-color: #333; } } -#footer .content { padding: 0; font-size: 85%; font-family: verdana, sans-serif; color: #666666; font-style: italic; } +.indexfile #footer { margin: 1rem 3rem; } -#index { margin: 1em 0 0 3em; } +.pyfile #footer { margin: 1rem 1rem; } -#header .content { padding: 1em 3rem; } +#footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3rem; } + +#header .content { padding: 1rem 3rem; } 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; } +#filter_container { float: right; margin: 0 2em 0 0; } + +#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } + +#filter_container input:focus { border-color: #007acc; } h2.stats { margin-top: .5em; font-size: 1em; } -.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; } +.stats button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { .stats button { border-color: #444; } } + +.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } + +.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } + +.stats button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { .stats button.run { background: #373d29; } } + +.stats button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.run.show_run { background: #373d29; } } + +.stats button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { .stats button.mis { background: #4b1818; } } + +.stats button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.mis.show_mis { background: #4b1818; } } + +.stats button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { .stats button.exc { background: #333; } } + +.stats button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.exc.show_exc { background: #333; } } + +.stats button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { .stats button.par { background: #650; } } -#source p .annotate.long, .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; } +.stats button.par.show_par { background: #ffa; border: 2px solid #dddd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.par.show_par { background: #650; } } + +.help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } #source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } #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; } + +.indexfile .help_panel { width: 20em; min-height: 4em; } + +.pyfile .help_panel { width: 16em; min-height: 8em; } #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 { padding: 1em 0 1em 3rem; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } +.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; } + +#source { padding: 1em 0 1em 3rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + #source p { position: relative; white-space: pre; } + #source p * { box-sizing: border-box; } -#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999999; font-family: verdana, sans-serif; } -#source p .n a { text-decoration: none; color: #999999; font-size: .8333em; line-height: 1em; } -#source p .n a:hover { text-decoration: underline; color: #999999; } + +#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n a { text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + #source p.highlight .n { background: #ffdd00; } -#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid white; } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + #source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + #source p .t:hover ~ .r .annotate.long { display: block; } -#source p .t .com { color: green; font-style: italic; line-height: 1px; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6A9955; } } + #source p .t .key { font-weight: bold; line-height: 1px; } -#source p .t .str { color: #000080; } + +#source p .t .str { color: #0451A5; } + +@media (prefers-color-scheme: dark) { #source p .t .str { color: #9CDCFE; } } + #source p.mis .t { border-left: 0.2em solid #ff0000; } -#source p.mis.show_mis .t { background: #ffdddd; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + #source p.mis.show_mis .t:hover { background: #f2d2d2; } -#source p.run .t { border-left: 0.2em solid #00ff00; } -#source p.run.show_run .t { background: #ddffdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + #source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + #source p.exc .t { border-left: 0.2em solid #808080; } -#source p.exc.show_exc .t { background: #eeeeee; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + #source p.exc.show_exc .t:hover { background: #e2e2e2; } -#source p.par .t { border-left: 0.2em solid #eeee99; } -#source p.par.show_par .t { background: #ffffaa; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #dddd00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + #source p.par.show_par .t:hover { background: #f2f2a2; } -#source p .r { position: absolute; top: 0; right: 2.5em; font-family: verdana, sans-serif; } -#source p .annotate { font-family: georgia; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + #source p .annotate.short:hover ~ .long { display: block; } + #source p .annotate.long { width: 30em; right: 2.5em; } + #source p input { display: none; } + #source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + #source p input ~ .r label.ctx::before { content: "▶ "; } + #source p input ~ .r label.ctx:hover { background: #d5f7ff; color: #666; } -#source p input:checked ~ .r label.ctx { background: #aaeeff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #aef; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + #source p input:checked ~ .r label.ctx::before { content: "▼ "; } + #source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + #source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } -#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: verdana, sans-serif; white-space: nowrap; background: #aaeeff; border-radius: .25em; margin-right: 1.75em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #aef; border-radius: .25em; margin-right: 1.75em; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + #source p .ctxs span { display: block; text-align: right; } +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + #index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } -#index td.left, #index th.left { padding-left: 0; } -#index td.right, #index th.right { padding-right: 0; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + #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 a { text-decoration: none; color: #000; } + +#index th { font-style: italic; color: #333; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th.headerSortDown, #index th.headerSortUp { white-space: nowrap; background: #eee; } + +@media (prefers-color-scheme: dark) { #index th.headerSortDown, #index th.headerSortUp { background: #333; } } + +#index th.headerSortDown:after { content: " ↑"; } + +#index th.headerSortUp:after { content: " ↓"; } + +#index td.name a { text-decoration: none; color: inherit; } + #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: #ddd; position: absolute; min-height: 3px; width: 100%; } +#index tr.file:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } + +#index tr.file:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } From 212489d1c3fdc3445e8733bf8974339929e58df7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Thu, 7 Jan 2021 09:50:28 -0500 Subject: [PATCH 29/41] Need new gold files for pep626 partial branch HTML report --- tests/gold/html/partial_626/index.html | 93 +++++++++++++++++++++ tests/gold/html/partial_626/partial_py.html | 83 ++++++++++++++++++ tests/test_html.py | 50 +++++++---- 3 files changed, 210 insertions(+), 16 deletions(-) create mode 100644 tests/gold/html/partial_626/index.html create mode 100644 tests/gold/html/partial_626/partial_py.html diff --git a/tests/gold/html/partial_626/index.html b/tests/gold/html/partial_626/index.html new file mode 100644 index 000000000..f1b1465ed --- /dev/null +++ b/tests/gold/html/partial_626/index.html @@ -0,0 +1,93 @@ + + + + + Coverage report + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

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

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedbranchespartialcoverage
Total9016287%
partial.py9016287%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/tests/gold/html/partial_626/partial_py.html b/tests/gold/html/partial_626/partial_py.html new file mode 100644 index 000000000..adb0aaf0f --- /dev/null +++ b/tests/gold/html/partial_626/partial_py.html @@ -0,0 +1,83 @@ + + + + + + Coverage for partial.py: 87% + + + + + + + + + + +
+ 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# partial branches and excluded lines 

+

2a = 2 

+

3 

+

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

+

5 break 

+

6 

+

7while a: # pragma: no branch 

+

8 break 

+

9 

+

10if 0: 

+

11 never_happen() 

+

12 

+

13if 13: 13 ↛ 16line 13 didn't jump to line 16, because the condition on line 13 was never false

+

14 a = 14 

+

15 

+

16if a == 16: 

+

17 raise ZeroDivisionError("17") 

+
+ + + diff --git a/tests/test_html.py b/tests/test_html.py index 85f082040..3eeb607a1 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -938,23 +938,41 @@ def test_partial(self): cov = coverage.Coverage(config_file="partial.ini") partial = self.start_import_stop(cov, "partial") - cov.html_report(partial, directory="out/partial") - compare_html(gold_path("html/partial"), "out/partial") - contains( - "out/partial/partial_py.html", - '

', - '

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

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

', - ) - contains( - "out/partial/index.html", - 'partial.py', - '91%' - ) + if env.PYBEHAVIOR.pep626: + cov.html_report(partial, directory="out/partial_626") + compare_html(gold_path("html/partial_626"), "out/partial_626") + contains( + "out/partial_626/partial_py.html", + '

', + '

', + # The "if 0" and "if 1" statements are marked as run. + '

', + # The "raise ZeroDivisionError" is excluded by regex in the .ini. + '

', + ) + contains( + "out/partial_626/index.html", + 'partial.py', + '87%' + ) + else: + cov.html_report(partial, directory="out/partial") + compare_html(gold_path("html/partial"), "out/partial") + contains( + "out/partial/partial_py.html", + '

', + '

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

', + # The "raise ZeroDivisionError" is excluded by regex in the .ini. + '

', + ) + contains( + "out/partial/index.html", + 'partial.py', + '91%' + ) def test_styled(self): self.make_file("a.py", """\ From f08d8a8a1d6f852a9bc04a950ec9f9facaf92f5e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 9 Jan 2021 07:01:35 -0500 Subject: [PATCH 30/41] Fix a test to be usable with PEP626 In the old code, the return and raise were unreachable, so Python 3.10 compiled them away. This meant the return and raise messages weren't in the missing arc fragments. The new code has a path to the return and raise. --- tests/test_parser.py | 68 +++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/tests/test_parser.py b/tests/test_parser.py index 98308df97..9d3f9f678 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -330,69 +330,71 @@ def function(): try: if something(4): break + elif something(6): + x = 7 else: - if something(7): + if something(9): continue else: continue - if also_this(11): - return 12 + if also_this(13): + return 14 else: - raise Exception(14) + raise Exception(16) finally: - this_thing(16) - that_thing(17) + this_thing(18) + that_thing(19) """) 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" + parser.missing_arc_description(18, 5), + "line 18 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" + parser.missing_arc_description(5, 19), + "line 5 didn't jump to line 19, 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" + parser.missing_arc_description(18, 10), + "line 18 didn't jump to line 10, because the continue on line 10 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" + parser.missing_arc_description(10, 2), + "line 10 didn't jump to line 2, because the continue on line 10 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" + parser.missing_arc_description(18, 14), + "line 18 didn't jump to line 14, because the return on line 14 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" + parser.missing_arc_description(14, -1), + "line 14 didn't return from function 'function', " + "because the return on line 14 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" + parser.missing_arc_description(18, -1), + "line 18 didn't except from function 'function', " + "because the raise on line 16 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" + parser.missing_arc_description(18, 19), + "line 18 didn't jump to line 19, 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" + parser.missing_arc_description(18, 2), + "line 18 didn't jump to line 2, " + "because the continue on line 10 wasn't executed" " or " - "the continue on line 10 wasn't executed" + "the continue 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" + parser.missing_arc_description(18, -1), + "line 18 didn't except from function 'function', " + "because the raise on line 16 wasn't executed" " or " - "line 16 didn't return from function 'function', " - "because the return on line 12 wasn't executed" + "line 18 didn't return from function 'function', " + "because the return on line 14 wasn't executed" ) def test_missing_arc_descriptions_bug460(self): From 474847081a11b0f643df5950f4763ac29a1524b0 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 9 Jan 2021 08:27:11 -0500 Subject: [PATCH 31/41] Use the modern way to load modules by file name. Python 3.10 finally got super-noisy about load_module, which has been deprecated since 3.4! https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.load_module --- coverage/backward.py | 12 +++++++----- setup.cfg | 4 +++- tests/conftest.py | 19 +++++++++++++++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/coverage/backward.py b/coverage/backward.py index 9d1d78e5b..8af3452b2 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -245,15 +245,17 @@ def import_local_file(modname, modfile=None): """ try: - from importlib.machinery import SourceFileLoader + import importlib.util as importlib_util except ImportError: - SourceFileLoader = None + importlib_util = None if modfile is None: modfile = modname + '.py' - if SourceFileLoader: - # pylint: disable=no-value-for-parameter, deprecated-method - mod = SourceFileLoader(modname, modfile).load_module() + if importlib_util: + spec = importlib_util.spec_from_file_location(modname, modfile) + mod = importlib_util.module_from_spec(spec) + sys.modules[modname] = mod + spec.loader.exec_module(mod) else: for suff in imp.get_suffixes(): # pragma: part covered if suff[0] == '.py': diff --git a/setup.cfg b/setup.cfg index 2d015d954..7ba8525a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,10 +2,12 @@ addopts = -q -n3 --strict --force-flaky --no-flaky-report -rfe --failed-first markers = expensive: too slow to run during "make smoke" -# How come this warning is suppressed successfully here, but not in conftest.py?? + +# How come these warnings are suppressed successfully here, but not in conftest.py?? filterwarnings = ignore:dns.hash module will be removed:DeprecationWarning ignore:Using or importing the ABCs:DeprecationWarning + # xfail tests that pass should fail the test suite xfail_strict=true diff --git a/tests/conftest.py b/tests/conftest.py index 82a6b0f2f..10761cddf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,21 +25,32 @@ def set_warnings(): warnings.simplefilter("default") warnings.simplefilter("once", DeprecationWarning) - # A warning to suppress: + # Warnings to suppress: + # How come these warnings are successfully suppressed here, but not in setup.cfg?? + # 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) - # How come this warning is successfully suppressed here, but not in setup.cfg?? warnings.filterwarnings( "ignore", category=DeprecationWarning, - message="The value of convert_charrefs will become True in 3.5.", + message=r"The value of convert_charrefs will become True in 3.5.", ) + warnings.filterwarnings( "ignore", category=DeprecationWarning, - message=".* instead of inspect.getfullargspec", + message=r".* instead of inspect.getfullargspec", ) + + # :681: + # ImportWarning: VendorImporter.exec_module() not found; falling back to load_module() + warnings.filterwarnings( + "ignore", + category=ImportWarning, + message=r".*exec_module\(\) not found; falling back to load_module\(\)", + ) + if env.PYPY3: # pypy3 warns about unclosed files a lot. warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning) From 5a76fa9a8500d7a8b16e2c65c4d8372000abf6e3 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 9 Jan 2021 10:10:06 -0500 Subject: [PATCH 32/41] Better control over setup.py warnings --- setup.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 86a054ab2..d1bfe6608 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,20 @@ 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 - +import distutils.log # pylint: disable=wrong-import-order + +# $set_env.py: COVERAGE_QUIETER - Set to remove some noise from test output. +if bool(int(os.getenv("COVERAGE_QUIETER", "0"))): + # Distutils has its own mini-logging code, and it sets the level too high. + # When I ask for --quiet when running tessts, I don't want to see warnings. + old_set_verbosity = distutils.log.set_verbosity + def better_set_verbosity(v): + """--quiet means no warnings!""" + if v <= 0: + distutils.log.set_threshold(distutils.log.ERROR) + else: + old_set_verbosity(v) + distutils.log.set_verbosity = better_set_verbosity # Get or massage our metadata. We exec coverage/version.py so we can avoid # importing the product code into setup.py. From 07ac031c24f8133b917a3946349e468182730ad6 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 9 Jan 2021 15:14:25 -0500 Subject: [PATCH 33/41] Mention PEP 626 in the changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index ae10409c1..188b804c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,10 +29,15 @@ Unreleased of the output easier. Thanks, Judson Neer. This had been requested a number of times (`issue 1086`_, `issue 922`_, `issue 732`_). +- Update to support Python 3.10 alphas in progress, including `PEP 626: Precise + line numbers for debugging and other tools `_. + .. _issue 1086: https://github.com/nedbat/coveragepy/issues/1086 .. _issue 732: https://github.com/nedbat/coveragepy/issues/732 .. _issue 922: https://github.com/nedbat/coveragepy/issues/922 +.. _pep626: https://www.python.org/dev/peps/pep-0626/ + .. _changes_531: From 69573662dab1203009075bca655eadf088aeda78 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 10 Jan 2021 12:39:43 -0500 Subject: [PATCH 34/41] I don't understand the codecov comments, so turn them off --- .github/codecov.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/codecov.yml b/.github/codecov.yml index 167a6c11a..dc6cc4cbe 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -12,6 +12,4 @@ coverage: default: informational: true -comment: - layout: diff, files - behavior: new +comment: false From b0710b1fc868db5c385b3d30a2fab49a2aeb2e81 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 10 Jan 2021 21:00:09 -0500 Subject: [PATCH 35/41] skip_covered and skip_empty for HTML. #1090 --- CHANGES.rst | 9 +++++++- coverage/config.py | 4 ++++ coverage/control.py | 4 ++-- coverage/html.py | 12 +++++++++-- doc/config.rst | 32 +++++++++++++++------------- tests/test_html.py | 52 ++++++++++++++++++++++++++++----------------- 6 files changed, 74 insertions(+), 39 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 188b804c7..31515c2d1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,12 +29,19 @@ Unreleased of the output easier. Thanks, Judson Neer. This had been requested a number of times (`issue 1086`_, `issue 922`_, `issue 732`_). +- The ``skip_covered`` and ``skip_empty`` settings in the configuration file + can now be specified in the ``[html]`` section, so that text reports and HTML + reports can use separate settings. The HTML report will still use the + ``[report]`` settings if there isn't a value in the ``[html]`` section. + Closes `issue 1090`_. + - Update to support Python 3.10 alphas in progress, including `PEP 626: Precise line numbers for debugging and other tools `_. -.. _issue 1086: https://github.com/nedbat/coveragepy/issues/1086 .. _issue 732: https://github.com/nedbat/coveragepy/issues/732 .. _issue 922: https://github.com/nedbat/coveragepy/issues/922 +.. _issue 1086: https://github.com/nedbat/coveragepy/issues/1086 +.. _issue 1090: https://github.com/nedbat/coveragepy/issues/1090 .. _pep626: https://www.python.org/dev/peps/pep-0626/ diff --git a/coverage/config.py b/coverage/config.py index 2af4a1cc8..803dcd5d7 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -217,6 +217,8 @@ def __init__(self): # Defaults for [html] self.extra_css = None self.html_dir = "htmlcov" + self.html_skip_covered = None + self.html_skip_empty = None self.html_title = "Coverage report" self.show_contexts = False @@ -384,6 +386,8 @@ def copy(self): # [html] ('extra_css', 'html:extra_css'), ('html_dir', 'html:directory'), + ('html_skip_covered', 'html:skip_covered', 'boolean'), + ('html_skip_empty', 'html:skip_empty', 'boolean'), ('html_title', 'html:title'), ('show_contexts', 'html:show_contexts', 'boolean'), diff --git a/coverage/control.py b/coverage/control.py index 086490730..8d129bcb5 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -955,8 +955,8 @@ def html_report( 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, - skip_empty=skip_empty, precision=precision, + html_skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, + html_skip_empty=skip_empty, precision=precision, ): reporter = HtmlReporter(self) return reporter.report(morfs) diff --git a/coverage/html.py b/coverage/html.py index ef50b56b3..0dfee7ca8 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -173,6 +173,14 @@ def __init__(self, cov): self.coverage = cov self.config = self.coverage.config self.directory = self.config.html_dir + + self.skip_covered = self.config.html_skip_covered + if self.skip_covered is None: + self.skip_covered = self.config.skip_covered + self.skip_empty = self.config.html_skip_empty + if self.skip_empty is None: + self.skip_empty= self.config.skip_empty + title = self.config.html_title if env.PY2: title = title.decode("utf8") @@ -271,7 +279,7 @@ def html_file(self, fr, analysis): nums = analysis.numbers self.all_files_nums.append(nums) - if self.config.skip_covered: + if self.skip_covered: # Don't report on 100% files. no_missing_lines = (nums.n_missing == 0) no_missing_branches = (nums.n_partial_branches == 0) @@ -280,7 +288,7 @@ def html_file(self, fr, analysis): file_be_gone(html_path) return - if self.config.skip_empty: + if self.skip_empty: # Don't report on empty files. if nums.n_statements == 0: file_be_gone(html_path) diff --git a/doc/config.rst b/doc/config.rst index c6cb94dd5..3a8b0784d 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -315,16 +315,6 @@ setting also affects the interpretation of the ``fail_under`` setting. ``show_missing`` (boolean, default False): when running a summary report, show missing lines. See :ref:`cmd_report` 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_report` for more information. - -.. _config_report_skip_empty: - -``skip_empty`` (boolean, default False): Don't include empty files (those that -have 0 statements) in the report. See :ref:`cmd_report` for more information. - .. _config_report_sort: ``sort`` (string, default "Name"): Sort the text report by the named column. @@ -344,18 +334,30 @@ also apply to HTML output, where appropriate. ``directory`` (string, default "htmlcov"): where to write the HTML report files. +.. _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_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: +.. _config_html_skip_covered: -``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. +``skip_covered`` (boolean, defaulted from ``[report] skip_covered``): Don't +include files in the report that are 100% covered files. See :ref:`cmd_report` +for more information. + +.. _config_html_skip_empty: + +``skip_empty`` (boolean, defaulted from ``[report] skip_empty``): Don't include +empty files (those that have 0 statements) in the report. See :ref:`cmd_report` +for more information. .. _config_html_title: diff --git a/tests/test_html.py b/tests/test_html.py index 3eeb607a1..825b0afbe 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -495,7 +495,8 @@ def test_reporting_on_unmeasured_file(self): self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/other_py.html") - def test_report_skip_covered_no_branches(self): + def make_main_and_not_covered(self): + """Helper to create files for skip_covered scenarios.""" self.make_file("main_file.py", """ import not_covered @@ -507,39 +508,41 @@ def normal(): def not_covered(): print("n") """) + + def test_report_skip_covered(self): + self.make_main_and_not_covered() self.run_coverage(htmlargs=dict(skip_covered=True)) self.assert_exists("htmlcov/index.html") self.assert_doesnt_exist("htmlcov/main_file_py.html") self.assert_exists("htmlcov/not_covered_py.html") - def test_report_skip_covered_100(self): - self.make_file("main_file.py", """ - def normal(): - print("z") - normal() - """) - res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) - self.assertEqual(res, 100.0) + def test_html_skip_covered(self): + self.make_main_and_not_covered() + self.make_file(".coveragerc", "[html]\nskip_covered = True") + self.run_coverage() + self.assert_exists("htmlcov/index.html") self.assert_doesnt_exist("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/not_covered_py.html") def test_report_skip_covered_branches(self): - self.make_file("main_file.py", """ - import not_covered + self.make_main_and_not_covered() + self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True)) + self.assert_exists("htmlcov/index.html") + self.assert_doesnt_exist("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/not_covered_py.html") + def test_report_skip_covered_100(self): + self.make_file("main_file.py", """ def normal(): print("z") normal() """) - self.make_file("not_covered.py", """ - def not_covered(): - print("n") - """) - self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True)) - self.assert_exists("htmlcov/index.html") + res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) + self.assertEqual(res, 100.0) self.assert_doesnt_exist("htmlcov/main_file_py.html") - self.assert_exists("htmlcov/not_covered_py.html") - def test_report_skip_empty_files(self): + def make_init_and_main(self): + """Helper to create files for skip_empty scenarios.""" self.make_file("submodule/__init__.py", "") self.make_file("main_file.py", """ import submodule @@ -548,11 +551,22 @@ def normal(): print("z") normal() """) + + def test_report_skip_empty(self): + self.make_init_and_main() self.run_coverage(htmlargs=dict(skip_empty=True)) self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/main_file_py.html") self.assert_doesnt_exist("htmlcov/submodule___init___py.html") + def test_html_skip_empty(self): + self.make_init_and_main() + self.make_file(".coveragerc", "[html]\nskip_empty = True") + self.run_coverage() + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/main_file_py.html") + self.assert_doesnt_exist("htmlcov/submodule___init___py.html") + class HtmlStaticFileTest(CoverageTest): """Tests of the static file copying for the HTML report.""" From dc0e80657257def9203b5e0ca7b6141d3dcd1d57 Mon Sep 17 00:00:00 2001 From: Valentin Lab Date: Mon, 14 Dec 2020 16:19:29 +0100 Subject: [PATCH 36/41] fix: combine aliases on windows base dirs (ie: ``X:\``) (fixes: #577) Signed-off-by: Valentin Lab --- coverage/files.py | 6 ++++-- tests/test_files.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/coverage/files.py b/coverage/files.py index 5c2ff1ace..59b2bd61d 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -359,17 +359,19 @@ def add(self, pattern, result): match an entire tree, and not just its root. """ + pattern_sep = sep(pattern) + if len(pattern) > 1: pattern = pattern.rstrip(r"\/") # The pattern can't end with a wildcard component. if pattern.endswith("*"): raise CoverageException("Pattern must not end with wildcards.") - pattern_sep = sep(pattern) # The pattern is meant to match a filepath. Let's make it absolute # unless it already is, or is meant to match any prefix. - if not pattern.startswith('*') and not isabs_anywhere(pattern): + if not pattern.startswith('*') and not isabs_anywhere(pattern + + pattern_sep): pattern = abs_file(pattern) if not pattern.endswith(pattern_sep): pattern += pattern_sep diff --git a/tests/test_files.py b/tests/test_files.py index 9df4e5d0d..84e25f107 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -349,6 +349,20 @@ def test_multiple_wildcard(self): './django/foo/bar.py' ) + def test_windows_root_paths(self): + aliases = PathAliases() + aliases.add('X:\\', '/tmp/src') + self.assert_mapped( + aliases, + "X:\\a\\file.py", + "/tmp/src/a/file.py" + ) + self.assert_mapped( + aliases, + "X:\\file.py", + "/tmp/src/file.py" + ) + def test_leading_wildcard(self): aliases = PathAliases() aliases.add('*/d1', './mysrc1') From 94239ad30e56f8f4bf01dcaf8700cdecca86e7f1 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 17 Jan 2021 17:16:53 -0500 Subject: [PATCH 37/41] Add changelog for #1080 #577 --- CHANGES.rst | 6 +++++- CONTRIBUTORS.txt | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 31515c2d1..12468ea56 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -35,14 +35,18 @@ Unreleased ``[report]`` settings if there isn't a value in the ``[html]`` section. Closes `issue 1090`_. +- Combining files on Windows across drives how works properly, fixing `issue + 577`_. Thanks, `Valentine Lab `_. + - Update to support Python 3.10 alphas in progress, including `PEP 626: Precise line numbers for debugging and other tools `_. +.. _issue 577: https://github.com/nedbat/coveragepy/issues/577 .. _issue 732: https://github.com/nedbat/coveragepy/issues/732 .. _issue 922: https://github.com/nedbat/coveragepy/issues/922 .. _issue 1086: https://github.com/nedbat/coveragepy/issues/1086 .. _issue 1090: https://github.com/nedbat/coveragepy/issues/1090 - +.. _pr1080: https://github.com/nedbat/coveragepy/pull/1080 .. _pep626: https://www.python.org/dev/peps/pep-0626/ diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 44b4f557d..455c40967 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -136,6 +136,7 @@ Ted Wexler Thijs Triemstra Thomas Grainger Titus Brown +Valentine Lab Vince Salvino Ville Skyttä Xie Yanbo From a09b1714c26cde1542044f44295600679d4368fc Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Mon, 18 Jan 2021 18:08:56 -0500 Subject: [PATCH 38/41] Simplify the testing of the toml extra, fixing #1084 --- CHANGES.rst | 4 +++ coverage/optional.py | 76 ------------------------------------------ coverage/tomlconfig.py | 8 +++-- tests/helpers.py | 19 +++++++++++ tests/test_config.py | 10 +++--- tests/test_testing.py | 19 ++++++----- 6 files changed, 44 insertions(+), 92 deletions(-) delete mode 100644 coverage/optional.py diff --git a/CHANGES.rst b/CHANGES.rst index 12468ea56..b517c0ce2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -38,12 +38,16 @@ Unreleased - Combining files on Windows across drives how works properly, fixing `issue 577`_. Thanks, `Valentine Lab `_. +- Fix an obscure warning from deep in the _decimal module, as reported in + `issue 1084`_. + - Update to support Python 3.10 alphas in progress, including `PEP 626: Precise line numbers for debugging and other tools `_. .. _issue 577: https://github.com/nedbat/coveragepy/issues/577 .. _issue 732: https://github.com/nedbat/coveragepy/issues/732 .. _issue 922: https://github.com/nedbat/coveragepy/issues/922 +.. _issue 1084: https://github.com/nedbat/coveragepy/issues/1084 .. _issue 1086: https://github.com/nedbat/coveragepy/issues/1086 .. _issue 1090: https://github.com/nedbat/coveragepy/issues/1090 .. _pr1080: https://github.com/nedbat/coveragepy/pull/1080 diff --git a/coverage/optional.py b/coverage/optional.py deleted file mode 100644 index 507a1ada7..000000000 --- a/coverage/optional.py +++ /dev/null @@ -1,76 +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 - -""" -Imports that we need at runtime, but might not be present. - -When importing one of these modules, always do it in the function where you -need the module. Some tests will need to remove the module. If you import -it at the top level of your module, then the test won't be able to simulate -the module being unimportable. - -The import will always succeed, but the value will be None if the module is -unavailable. - -Bad:: - - # MyModule.py - import unsure - - def use_unsure(): - unsure.something() - -Also bad:: - - # MyModule.py - from coverage.optional import unsure - - def use_unsure(): - unsure.something() - -Good:: - - # MyModule.py - - def use_unsure(): - from coverage.optional import unsure - if unsure is None: - raise Exception("Module unsure isn't available!") - - unsure.something() - -""" - -import contextlib - -# This file's purpose is to provide modules to be imported from here. -# pylint: disable=unused-import - -# TOML support is an install-time extra option. -try: - import toml -except ImportError: # pragma: not covered - toml = None - - -@contextlib.contextmanager -def without(modname): - """Hide a module for testing. - - Use this in a test function to make an optional module unavailable during - the test:: - - with coverage.optional.without('toml'): - use_toml_somehow() - - Arguments: - modname (str): the name of a module importable from - `coverage.optional`. - - """ - real_module = globals()[modname] - try: - globals()[modname] = None - yield - finally: - globals()[modname] = real_module diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py index 25542f99e..3ad581571 100644 --- a/coverage/tomlconfig.py +++ b/coverage/tomlconfig.py @@ -11,6 +11,12 @@ from coverage.backward import configparser, path_types from coverage.misc import CoverageException, substitute_variables +# TOML support is an install-time extra option. +try: + import toml +except ImportError: # pragma: not covered + toml = None + class TomlDecodeError(Exception): """An exception class that exists even when toml isn't installed.""" @@ -29,8 +35,6 @@ def __init__(self, our_file): self.data = None def read(self, filenames): - from coverage.optional import toml - # RawConfigParser takes a filename or list of filenames, but we only # ever call this with a single filename. assert isinstance(filenames, path_types) diff --git a/tests/helpers.py b/tests/helpers.py index 9c6a0ad8e..0621d7a94 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -10,6 +10,7 @@ import subprocess import sys +import mock from unittest_mixins import ModuleCleaner from coverage import env @@ -203,3 +204,21 @@ def arcs_to_arcz_repr(arcs): line += _arcs_to_arcz_repr_one(b) repr_list.append(line) return "\n".join(repr_list) + "\n" + + +def without_module(using_module, missing_module_name): + """ + Hide a module for testing. + + Use this in a test function to make an optional module unavailable during + the test:: + + with without_module(product.something, 'toml'): + use_toml_somehow() + + Arguments: + using_module: a module in which to hide `missing_module_name`. + missing_module_name (str): the name of the module to hide. + + """ + return mock.patch.object(using_module, missing_module_name, None) diff --git a/tests/test_config.py b/tests/test_config.py index dd86303f2..4225540c0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,9 +10,9 @@ import coverage from coverage.misc import CoverageException -import coverage.optional from tests.coveragetest import CoverageTest, UsingModulesMixin +from tests.helpers import without_module class ConfigTest(CoverageTest): @@ -712,7 +712,7 @@ def test_nocoveragerc_file_when_specified(self): def test_no_toml_installed_no_toml(self): # Can't read a toml file that doesn't exist. - with coverage.optional.without('toml'): + with without_module(coverage.tomlconfig, 'toml'): msg = "Couldn't read 'cov.toml' as a config file" with self.assertRaisesRegex(CoverageException, msg): coverage.Coverage(config_file="cov.toml") @@ -720,7 +720,7 @@ def test_no_toml_installed_no_toml(self): def test_no_toml_installed_explicit_toml(self): # Can't specify a toml config file if toml isn't installed. self.make_file("cov.toml", "# A toml file!") - with coverage.optional.without('toml'): + with without_module(coverage.tomlconfig, 'toml'): msg = "Can't read 'cov.toml' without TOML support" with self.assertRaisesRegex(CoverageException, msg): coverage.Coverage(config_file="cov.toml") @@ -732,7 +732,7 @@ def test_no_toml_installed_pyproject_toml(self): [tool.coverage.run] xyzzy = 17 """) - with coverage.optional.without('toml'): + with without_module(coverage.tomlconfig, 'toml'): msg = "Can't read 'pyproject.toml' without TOML support" with self.assertRaisesRegex(CoverageException, msg): coverage.Coverage() @@ -744,7 +744,7 @@ def test_no_toml_installed_pyproject_no_coverage(self): [tool.something] xyzzy = 17 """) - with coverage.optional.without('toml'): + with without_module(coverage.tomlconfig, 'toml'): cov = coverage.Coverage() # We get default settings: self.assertFalse(cov.config.timid) diff --git a/tests/test_testing.py b/tests/test_testing.py index c5d464309..34ea32635 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -12,14 +12,16 @@ import pytest import coverage +from coverage import tomlconfig from coverage.backunittest import TestCase, unittest from coverage.files import actual_path from coverage.misc import StopEverything -import coverage.optional from tests.coveragetest import CoverageTest, convert_skip_exceptions -from tests.helpers import arcs_to_arcz_repr, arcz_to_arcs -from tests.helpers import CheckUniqueFilenames, re_lines, re_line +from tests.helpers import ( + arcs_to_arcz_repr, arcz_to_arcs, + CheckUniqueFilenames, re_lines, re_line, without_module, +) def test_xdist_sys_path_nuttiness_is_fixed(): @@ -323,12 +325,11 @@ def _same_python_executable(e1, e2): return False # pragma: only failure -def test_optional_without(): - # pylint: disable=reimported - from coverage.optional import toml as toml1 - with coverage.optional.without('toml'): - from coverage.optional import toml as toml2 - from coverage.optional import toml as toml3 +def test_without_module(): + toml1 = tomlconfig.toml + with without_module(tomlconfig, 'toml'): + toml2 = tomlconfig.toml + toml3 = tomlconfig.toml assert toml1 is toml3 is not None assert toml2 is None From 6ce7b4e3fe716f4cf14922a518b64279110a3ff9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jan 2021 17:48:51 -0500 Subject: [PATCH 39/41] Latest sample HTML --- doc/sample_html/cogapp___init___py.html | 4 ++-- doc/sample_html/cogapp___main___py.html | 4 ++-- doc/sample_html/cogapp_backward_py.html | 4 ++-- doc/sample_html/cogapp_cogapp_py.html | 4 ++-- doc/sample_html/cogapp_makefiles_py.html | 4 ++-- doc/sample_html/cogapp_test_cogapp_py.html | 4 ++-- doc/sample_html/cogapp_test_makefiles_py.html | 4 ++-- doc/sample_html/cogapp_test_whiteutils_py.html | 4 ++-- doc/sample_html/cogapp_whiteutils_py.html | 4 ++-- doc/sample_html/index.html | 4 ++-- doc/sample_html/status.json | 2 +- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/doc/sample_html/cogapp___init___py.html b/doc/sample_html/cogapp___init___py.html index d4d31a79e..be126eb48 100644 --- a/doc/sample_html/cogapp___init___py.html +++ b/doc/sample_html/cogapp___init___py.html @@ -66,8 +66,8 @@

diff --git a/doc/sample_html/cogapp___main___py.html b/doc/sample_html/cogapp___main___py.html index 97140bde7..52a0236e1 100644 --- a/doc/sample_html/cogapp___main___py.html +++ b/doc/sample_html/cogapp___main___py.html @@ -62,8 +62,8 @@

diff --git a/doc/sample_html/cogapp_backward_py.html b/doc/sample_html/cogapp_backward_py.html index 17149a30a..b79f30a03 100644 --- a/doc/sample_html/cogapp_backward_py.html +++ b/doc/sample_html/cogapp_backward_py.html @@ -99,8 +99,8 @@

diff --git a/doc/sample_html/cogapp_cogapp_py.html b/doc/sample_html/cogapp_cogapp_py.html index 3f0e5a567..6fa600fa2 100644 --- a/doc/sample_html/cogapp_cogapp_py.html +++ b/doc/sample_html/cogapp_cogapp_py.html @@ -865,8 +865,8 @@

diff --git a/doc/sample_html/cogapp_makefiles_py.html b/doc/sample_html/cogapp_makefiles_py.html index df5fd59e5..699bf9879 100644 --- a/doc/sample_html/cogapp_makefiles_py.html +++ b/doc/sample_html/cogapp_makefiles_py.html @@ -103,8 +103,8 @@

diff --git a/doc/sample_html/cogapp_test_cogapp_py.html b/doc/sample_html/cogapp_test_cogapp_py.html index 4894b141d..b7f9c9fdb 100644 --- a/doc/sample_html/cogapp_test_cogapp_py.html +++ b/doc/sample_html/cogapp_test_cogapp_py.html @@ -2535,8 +2535,8 @@

diff --git a/doc/sample_html/cogapp_test_makefiles_py.html b/doc/sample_html/cogapp_test_makefiles_py.html index 26028b2f8..b928d6005 100644 --- a/doc/sample_html/cogapp_test_makefiles_py.html +++ b/doc/sample_html/cogapp_test_makefiles_py.html @@ -179,8 +179,8 @@

diff --git a/doc/sample_html/cogapp_test_whiteutils_py.html b/doc/sample_html/cogapp_test_whiteutils_py.html index 88c631b2e..54c01da27 100644 --- a/doc/sample_html/cogapp_test_whiteutils_py.html +++ b/doc/sample_html/cogapp_test_whiteutils_py.html @@ -158,8 +158,8 @@

diff --git a/doc/sample_html/cogapp_whiteutils_py.html b/doc/sample_html/cogapp_whiteutils_py.html index b7f2d7d50..388fa0afb 100644 --- a/doc/sample_html/cogapp_whiteutils_py.html +++ b/doc/sample_html/cogapp_whiteutils_py.html @@ -130,8 +130,8 @@

diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html index be4b02c74..6ce866387 100644 --- a/doc/sample_html/index.html +++ b/doc/sample_html/index.html @@ -156,8 +156,8 @@

Coverage report: diff --git a/doc/sample_html/status.json b/doc/sample_html/status.json index fe49a4d49..83336a370 100644 --- a/doc/sample_html/status.json +++ b/doc/sample_html/status.json @@ -1 +1 @@ -{"format":2,"version":"5.3.1","globals":"28441ac12ca4ad5182670460eb380fe5","files":{"cogapp___init___py":{"hash":"6010eef3af87123028eb691d70094593","index":{"nums":[1,2,0,0,0,0,0],"html_filename":"cogapp___init___py.html","relative_filename":"cogapp/__init__.py"}},"cogapp___main___py":{"hash":"2cec3551dfd9a5818a6550318658ccd4","index":{"nums":[1,3,0,3,0,0,0],"html_filename":"cogapp___main___py.html","relative_filename":"cogapp/__main__.py"}},"cogapp_backward_py":{"hash":"f95e44a818c73b2187e6fadc6257f8ce","index":{"nums":[1,22,0,6,4,2,2],"html_filename":"cogapp_backward_py.html","relative_filename":"cogapp/backward.py"}},"cogapp_cogapp_py":{"hash":"f85acbdbacefaccb9c499ef6cbe2ffc4","index":{"nums":[1,485,1,215,200,28,132],"html_filename":"cogapp_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"cogapp_makefiles_py":{"hash":"4fd2add44238312a5567022fe28737de","index":{"nums":[1,27,0,20,14,0,14],"html_filename":"cogapp_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"cogapp_test_cogapp_py":{"hash":"ee9b3c832eaa47b9e3940133c58827af","index":{"nums":[1,790,6,549,20,0,18],"html_filename":"cogapp_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"cogapp_test_makefiles_py":{"hash":"66093f767a400ce1720b94a7371de48b","index":{"nums":[1,71,0,53,6,0,6],"html_filename":"cogapp_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"cogapp_test_whiteutils_py":{"hash":"068beefb2872fe6739fad2471c36a4f1","index":{"nums":[1,69,0,50,0,0,0],"html_filename":"cogapp_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"cogapp_whiteutils_py":{"hash":"b16b0e7f940175106b11230fea9e8c8c","index":{"nums":[1,45,0,5,34,4,4],"html_filename":"cogapp_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file +{"format":2,"version":"5.4","globals":"a486ca194f909abc39de9a093fa5a484","files":{"cogapp___init___py":{"hash":"6010eef3af87123028eb691d70094593","index":{"nums":[1,2,0,0,0,0,0],"html_filename":"cogapp___init___py.html","relative_filename":"cogapp/__init__.py"}},"cogapp___main___py":{"hash":"2cec3551dfd9a5818a6550318658ccd4","index":{"nums":[1,3,0,3,0,0,0],"html_filename":"cogapp___main___py.html","relative_filename":"cogapp/__main__.py"}},"cogapp_backward_py":{"hash":"f95e44a818c73b2187e6fadc6257f8ce","index":{"nums":[1,22,0,6,4,2,2],"html_filename":"cogapp_backward_py.html","relative_filename":"cogapp/backward.py"}},"cogapp_cogapp_py":{"hash":"f85acbdbacefaccb9c499ef6cbe2ffc4","index":{"nums":[1,485,1,215,200,28,132],"html_filename":"cogapp_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"cogapp_makefiles_py":{"hash":"4fd2add44238312a5567022fe28737de","index":{"nums":[1,27,0,20,14,0,14],"html_filename":"cogapp_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"cogapp_test_cogapp_py":{"hash":"ee9b3c832eaa47b9e3940133c58827af","index":{"nums":[1,790,6,549,20,0,18],"html_filename":"cogapp_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"cogapp_test_makefiles_py":{"hash":"66093f767a400ce1720b94a7371de48b","index":{"nums":[1,71,0,53,6,0,6],"html_filename":"cogapp_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"cogapp_test_whiteutils_py":{"hash":"068beefb2872fe6739fad2471c36a4f1","index":{"nums":[1,69,0,50,0,0,0],"html_filename":"cogapp_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"cogapp_whiteutils_py":{"hash":"b16b0e7f940175106b11230fea9e8c8c","index":{"nums":[1,45,0,5,34,4,4],"html_filename":"cogapp_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file From 96ae2000a951267db71f23ead7aa63fab70af0e7 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 24 Jan 2021 17:49:10 -0500 Subject: [PATCH 40/41] Prep for 5.4 --- CHANGES.rst | 6 ++++-- NOTICE.txt | 2 +- README.rst | 2 +- coverage/version.py | 2 +- doc/conf.py | 8 ++++---- doc/index.rst | 4 ++-- doc/python-coverage.1.txt | 8 ++++---- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index b517c0ce2..98f632842 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,8 +21,10 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. .. Version 9.8.1 --- 2027-07-27 .. ---------------------------- -Unreleased ----------- +.. _changes_54: + +Version 5.4 --- 2021-01-24 +-------------------------- - The text report produced by ``coverage report`` now always outputs a TOTAL line, even if only one Python file is reported. This makes regex parsing diff --git a/NOTICE.txt b/NOTICE.txt index 2e7671024..37ded535b 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Copyright 2001 Gareth Rees. All rights reserved. -Copyright 2004-2020 Ned Batchelder. All rights reserved. +Copyright 2004-2021 Ned Batchelder. All rights reserved. Except where noted otherwise, this software is licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in diff --git a/README.rst b/README.rst index 7708e9c6c..66cd938a8 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ Coverage.py runs on many versions of Python: * CPython 2.7. * CPython 3.5 through 3.10 alpha. -* PyPy2 7.3.1 and PyPy3 7.3.1. +* PyPy2 7.3.3 and PyPy3 7.3.3. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. diff --git a/coverage/version.py b/coverage/version.py index f6340f423..8cc58dfb1 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, 3, 2, "alpha", 0) +version_info = (5, 4, 0, "final", 0) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/doc/conf.py b/doc/conf.py index 2fbf6c1e9..b76c0a235 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -59,18 +59,18 @@ # General information about the project. project = u'Coverage.py' -copyright = u'2009\N{EN DASH}2020, Ned Batchelder.' # CHANGEME # pylint: disable=redefined-builtin +copyright = u'2009\N{EN DASH}2021, 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 # built documents. # # The short X.Y version. -version = "5.3.1" # CHANGEME +version = "5.4" # CHANGEME # The full version, including alpha/beta/rc tags. -release = "5.3.1" # CHANGEME +release = "5.4" # CHANGEME # The date of release, in "monthname day, year" format. -release_date = "December 19, 2020" # CHANGEME +release_date = "January 24, 2021" # CHANGEME rst_epilog = """ .. |release_date| replace:: {release_date} diff --git a/doc/index.rst b/doc/index.rst index 0e7eb22ee..6f408b897 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -18,12 +18,12 @@ supported on: * Python versions 2.7, 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 alpha. -* PyPy2 7.3.1 and PyPy3 7.3.1. +* PyPy2 7.3.3 and PyPy3 7.3.3. .. ifconfig:: prerelease **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 5.3.1, `described here`_. + apply.** The latest stable version is coverage.py 5.4, `described here`_. .. _described here: http://coverage.readthedocs.io/ diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt index a98954823..0bbd44d0a 100644 --- a/doc/python-coverage.1.txt +++ b/doc/python-coverage.1.txt @@ -2,13 +2,13 @@ python-coverage =============== -------------------------------------------------- -measure code coverage of Python program execution -------------------------------------------------- +---------------------------- +Measure Python code coverage +---------------------------- :Author: Ned Batchelder :Author: |author| -:Date: 2019-11-11 +:Date: 2021-01-24 :Copyright: Apache 2.0 license, attribution and disclaimer required. :Manual section: 1 :Manual group: Coverage.py From 4c8c34cb642db78ba808d09cb46100a8131f93fe Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 26 Jan 2021 06:04:02 -0500 Subject: [PATCH 41/41] Remove unneeded slash --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ec1a5aa81..ff5d3c999 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ DOCBIN = .tox/doc/bin SPHINXOPTS = -aE SPHINXBUILD = $(DOCBIN)/sphinx-build $(SPHINXOPTS) SPHINXAUTOBUILD = $(DOCBIN)/sphinx-autobuild -p 9876 --ignore '.git/**' --open-browser -WEBHOME = ~/web/stellated/ +WEBHOME = ~/web/stellated WEBSAMPLE = $(WEBHOME)/files/sample_coverage_html WEBSAMPLEBETA = $(WEBHOME)/files/sample_coverage_html_beta