Skip to content

Commit f3d6b4a

Browse files
committedFeb 10, 2025
refactor: check for more kinds of constant tests
1 parent 67899ea commit f3d6b4a

File tree

3 files changed

+57
-14
lines changed

3 files changed

+57
-14
lines changed
 

‎coverage/parser.py

+20-11
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,25 @@ def __init__(self, body: Sequence[ast.AST]) -> None:
654654
# TODO: Shouldn't the cause messages join with "and" instead of "or"?
655655

656656

657+
def is_constant_test_expr(node: ast.AST) -> bool:
658+
"""Is this a compile-time constant test expression?"""
659+
node_name = node.__class__.__name__
660+
if node_name in [ # in PYVERSIONS:
661+
"Constant", # all
662+
"NameConstant", # 9 10 11, gone in 12
663+
"Num", # 9 10 11, gone in 12
664+
]:
665+
return True
666+
elif isinstance(node, ast.Name):
667+
if node.id in ["True", "False", "None", "__debug__"]:
668+
return True
669+
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
670+
return is_constant_test_expr(node.operand)
671+
elif isinstance(node, ast.BoolOp):
672+
return all(is_constant_test_expr(v) for v in node.values)
673+
return False
674+
675+
657676
class AstArcAnalyzer:
658677
"""Analyze source text with an AST to find executable code paths.
659678
@@ -1022,16 +1041,6 @@ def _missing__While(self, node: ast.While) -> ast.AST | None:
10221041
new_while.orelse = []
10231042
return new_while
10241043

1025-
def is_constant_expr(self, node: ast.AST) -> bool:
1026-
"""Is this a compile-time constant?"""
1027-
node_name = node.__class__.__name__
1028-
if node_name in ["Constant", "NameConstant", "Num"]:
1029-
return True
1030-
elif isinstance(node, ast.Name):
1031-
if node.id in ["True", "False", "None", "__debug__"]:
1032-
return True
1033-
return False
1034-
10351044
# In the fullness of time, these might be good tests to write:
10361045
# while EXPR:
10371046
# while False:
@@ -1262,7 +1271,7 @@ def _handle__Try(self, node: ast.Try) -> set[ArcStart]:
12621271

12631272
def _handle__While(self, node: ast.While) -> set[ArcStart]:
12641273
start = to_top = self.line_for_node(node.test)
1265-
constant_test = self.is_constant_expr(node.test)
1274+
constant_test = is_constant_test_expr(node.test)
12661275
top_is_body0 = False
12671276
if constant_test:
12681277
top_is_body0 = True

‎tests/test_arcs.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,6 @@ def test_while_1(self) -> None:
485485
)
486486

487487
def test_while_true(self) -> None:
488-
# With "while True", 2.x thinks it's computation,
489-
# 3.x thinks it's constant.
490488
self.check_coverage("""\
491489
a, i = 1, 0
492490
while True:
@@ -500,6 +498,20 @@ def test_while_true(self) -> None:
500498
branchz_missing="",
501499
)
502500

501+
def test_while_not_false(self) -> None:
502+
self.check_coverage("""\
503+
a, i = 1, 0
504+
while not False:
505+
if i >= 3:
506+
a = 4
507+
break
508+
i += 1
509+
assert a == 4 and i == 3
510+
""",
511+
branchz="34 36",
512+
branchz_missing="",
513+
)
514+
503515
def test_zero_coverage_while_loop(self) -> None:
504516
# https://github.com/nedbat/coveragepy/issues/502
505517
self.make_file("main.py", "print('done')")

‎tests/test_parser.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from __future__ import annotations
77

8+
import ast
89
import re
910
import textwrap
1011
from unittest import mock
@@ -13,7 +14,7 @@
1314

1415
from coverage import env
1516
from coverage.exceptions import NoSource, NotPython
16-
from coverage.parser import PythonParser
17+
from coverage.parser import PythonParser, is_constant_test_expr
1718

1819
from tests.coveragetest import CoverageTest
1920
from tests.helpers import arcz_to_arcs
@@ -1192,3 +1193,24 @@ def test_os_error(self) -> None:
11921193
with pytest.raises(NoSource, match=re.escape(msg)):
11931194
with mock.patch("coverage.python.read_python_source", side_effect=OSError("Fake!")):
11941195
PythonParser(filename="cant-read.py")
1196+
1197+
1198+
@pytest.mark.parametrize(
1199+
["expr", "is_constant"],
1200+
[
1201+
("True", True),
1202+
("False", True),
1203+
("__debug__", True),
1204+
("not __debug__", True),
1205+
("not(__debug__)", True),
1206+
("-__debug__", False),
1207+
("__debug__ or True", True),
1208+
("__debug__ + True", False),
1209+
("x", False),
1210+
("__debug__ or debug", False),
1211+
]
1212+
)
1213+
def test_is_constant_test_expr(expr: str, is_constant: bool) -> None:
1214+
node = ast.parse(expr, mode="eval").body
1215+
print(ast.dump(node, indent=4))
1216+
assert is_constant_test_expr(node) == is_constant

0 commit comments

Comments
 (0)