From 150f8a921c224c9702c6ec94f3f76e1177aab9c7 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Tue, 11 Feb 2025 09:49:34 -0500
Subject: [PATCH 01/29] build: bump version to 7.6.13
---
CHANGES.rst | 6 ++++++
coverage/version.py | 4 ++--
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index fcd12b863..b0b100a44 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,6 +20,12 @@ upgrading your version of coverage.py.
.. Version 9.8.1 — 2027-07-27
.. --------------------------
+Unreleased
+----------
+
+Nothing yet.
+
+
.. start-releases
.. _changes_7-6-12:
diff --git a/coverage/version.py b/coverage/version.py
index e81549634..b7f776e5c 100644
--- a/coverage/version.py
+++ b/coverage/version.py
@@ -8,8 +8,8 @@
# version_info: same semantics as sys.version_info.
# _dev: the .devN suffix if any.
-version_info = (7, 6, 12, "final", 0)
-_dev = 0
+version_info = (7, 6, 13, "alpha", 0)
+_dev = 1
def _make_version(
From 830d2eb48dbcc3050dc8d5398fbaf31e1b1b0344 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Thu, 13 Feb 2025 19:11:03 -0500
Subject: [PATCH 02/29] refactor: no need for ast nodes that don't exist
anymore
---
coverage/parser.py | 14 +++++++-------
tests/test_parser.py | 2 ++
2 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/coverage/parser.py b/coverage/parser.py
index a8bcbf1a6..de7e6f714 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -655,13 +655,13 @@ def __init__(self, body: Sequence[ast.AST]) -> None:
def is_constant_test_expr(node: ast.AST) -> bool:
- """Is this a compile-time constant test expression?"""
- node_name = node.__class__.__name__
- if node_name in [ # in PYVERSIONS:
- "Constant", # all
- "NameConstant", # 9 10 11, gone in 12
- "Num", # 9 10 11, gone in 12
- ]:
+ """Is this a compile-time constant test expression?
+
+ We don't try to mimic all of CPython's optimizations. We just have to
+ handle the kinds of constant expressions people might actually use.
+
+ """
+ if isinstance(node, ast.Constant):
return True
elif isinstance(node, ast.Name):
if node.id in ["True", "False", "None", "__debug__"]:
diff --git a/tests/test_parser.py b/tests/test_parser.py
index eb228bf85..f566b2603 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1200,6 +1200,8 @@ def test_os_error(self) -> None:
[
("True", True),
("False", True),
+ ("1", True),
+ ("0", True),
("__debug__", True),
("not __debug__", True),
("not(__debug__)", True),
From 1bf0db200d84ed980bb9434ccdaad7383e4cb188 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 15 Feb 2025 18:42:54 -0500
Subject: [PATCH 03/29] test: more convenience for when gold files need to
change
---
tests/gold/README.rst | 25 ++++++++++++-------------
tests/gold/html/Makefile | 2 +-
2 files changed, 13 insertions(+), 14 deletions(-)
diff --git a/tests/gold/README.rst b/tests/gold/README.rst
index 00b43ff28..b80bc0f77 100644
--- a/tests/gold/README.rst
+++ b/tests/gold/README.rst
@@ -14,19 +14,18 @@ output is in the tests/actual directory. Those files are ignored by git.
There's a Makefile in the html directory for working with gold files and their
associated support files.
-To view the tests/actual files, you need to tentatively copy them to the gold
-directories, and then add the supporting files so they can be viewed as
-complete output. For example::
+The gold files and the actual output files are not viewable as-is: they are
+missing the support files (css etc) they need. You can copy those support
+files with::
- cp tests/actual/html/contexts/* tests/gold/html/contexts
cd tests/gold/html
make complete
-If the new actual output is correct, you can use "make update-gold" to copy the
-actual output as the new gold files.
+If the new actual output is correct, you can use ``make update-gold`` to copy
+the actual output as the new gold files.
-If you have changed some of the supporting files (.css or .js), then "make
-update-support" will copy the updated files to the tests/gold/html/support
+If you have changed some of the supporting files (.css or .js), then ``make
+update-support`` will copy the updated files to the tests/gold/html/support
directory for checking test output.
If you have added a gold test, you'll need to manually copy the tests/actual
@@ -39,11 +38,11 @@ again, you can run just the failed tests again with::
The saved HTML files in the html directories can't be viewed properly without
the supporting CSS and Javascript files. But we don't want to save copies of
-those files in every subdirectory. The make target "make complete" in
+those files in every subdirectory. The make target ``make complete`` in
tests/gold/html will copy the support files so you can open the HTML files to
-see how they look. When you are done checking the output, you can use "make
-clean" to remove the support files from the gold directories.
+see how they look. When you are done checking the output, you can use ``make
+clean`` to remove the support files from the gold directories.
-If the output files are correct, you can update the gold files with "make
-update-gold". If there are version-specific gold files (for example,
+If the output files are correct, you can update the gold files with ``make
+update-gold``. If there are version-specific gold files (for example,
bom/2/\*), you'll need to update them manually.
diff --git a/tests/gold/html/Makefile b/tests/gold/html/Makefile
index 5ae08b44e..d9668ce68 100644
--- a/tests/gold/html/Makefile
+++ b/tests/gold/html/Makefile
@@ -6,7 +6,7 @@ help:
@grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf " %-26s%s\n", $$1, $$2}'
complete: ## Copy support files into directories so the HTML can be viewed properly.
- @for sub in *; do \
+ @for sub in * ../../actual/html/*; do \
if [ -f "$$sub/index.html" ]; then \
echo Copying into $$sub ; \
cp -n support/* $$sub ; \
From 3e87fab41c9532e2bf8e5a5ccf2b9c3570a55cf4 Mon Sep 17 00:00:00 2001
From: Ned Batchelder
Date: Sat, 15 Feb 2025 18:48:44 -0500
Subject: [PATCH 04/29] fix: better understanding of constant conditions
---
CHANGES.rst | 5 +-
coverage/parser.py | 31 +++++---
tests/gold/html/partial/class_index.html | 12 +--
tests/gold/html/partial/function_index.html | 12 +--
tests/gold/html/partial/index.html | 12 +--
tests/gold/html/partial/partial_py.html | 8 +-
tests/gold/html/partial_626/class_index.html | 22 +++---
.../gold/html/partial_626/function_index.html | 22 +++---
tests/gold/html/partial_626/index.html | 22 +++---
tests/gold/html/partial_626/partial_py.html | 16 ++--
tests/test_arcs.py | 74 +++++++++++--------
tests/test_html.py | 2 +-
tests/test_parser.py | 30 ++++----
13 files changed, 147 insertions(+), 121 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index b0b100a44..bddce7bb3 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -23,7 +23,10 @@ upgrading your version of coverage.py.
Unreleased
----------
-Nothing yet.
+- Many constant tests in if statements are now recognized as being optimized
+ away. For example, previously ``if 13:`` would have been considered a branch
+ with one path not taken. Now it is understood as always true and no coverage
+ is missing.
.. start-releases
diff --git a/coverage/parser.py b/coverage/parser.py
index de7e6f714..37bedf341 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -654,7 +654,7 @@ def __init__(self, body: Sequence[ast.AST]) -> None:
# TODO: Shouldn't the cause messages join with "and" instead of "or"?
-def is_constant_test_expr(node: ast.AST) -> bool:
+def is_constant_test_expr(node: ast.AST) -> tuple[bool, bool]:
"""Is this a compile-time constant test expression?
We don't try to mimic all of CPython's optimizations. We just have to
@@ -662,15 +662,20 @@ def is_constant_test_expr(node: ast.AST) -> bool:
"""
if isinstance(node, ast.Constant):
- return True
+ return True, bool(node.value)
elif isinstance(node, ast.Name):
if node.id in ["True", "False", "None", "__debug__"]:
- return True
+ return True, eval(node.id) # pylint: disable=eval-used
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
- return is_constant_test_expr(node.operand)
+ is_constant, val = is_constant_test_expr(node.operand)
+ return is_constant, not val
elif isinstance(node, ast.BoolOp):
- return all(is_constant_test_expr(v) for v in node.values)
- return False
+ rets = [is_constant_test_expr(v) for v in node.values]
+ is_constant = all(is_const for is_const, _ in rets)
+ if is_constant:
+ op = any if isinstance(node.op, ast.Or) else all
+ return True, op(v for _, v in rets)
+ return False, False
class AstArcAnalyzer:
@@ -1156,10 +1161,14 @@ def _handle__For(self, node: ast.For) -> set[ArcStart]:
def _handle__If(self, node: ast.If) -> set[ArcStart]:
start = self.line_for_node(node.test)
- from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
- exits = self.process_body(node.body, from_start=from_start)
- from_start = ArcStart(start, cause="the condition on line {lineno} was always true")
- exits |= self.process_body(node.orelse, from_start=from_start)
+ constant_test, val = is_constant_test_expr(node.test)
+ exits = set()
+ if not constant_test or val:
+ from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
+ exits |= self.process_body(node.body, from_start=from_start)
+ if not constant_test or not val:
+ from_start = ArcStart(start, cause="the condition on line {lineno} was always true")
+ exits |= self.process_body(node.orelse, from_start=from_start)
return exits
if sys.version_info >= (3, 10):
@@ -1271,7 +1280,7 @@ def _handle__Try(self, node: ast.Try) -> set[ArcStart]:
def _handle__While(self, node: ast.While) -> set[ArcStart]:
start = to_top = self.line_for_node(node.test)
- constant_test = is_constant_test_expr(node.test)
+ constant_test, _ = is_constant_test_expr(node.test)
top_is_body0 = False
if constant_test:
top_is_body0 = True
diff --git a/tests/gold/html/partial/class_index.html b/tests/gold/html/partial/class_index.html
index e455ca6ba..14307fd8a 100644
--- a/tests/gold/html/partial/class_index.html
+++ b/tests/gold/html/partial/class_index.html
@@ -4,8 +4,8 @@
Coverage report
-
-
+
+
@@ -56,8 +56,8 @@