summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2018-11-09 07:03:25 -0500
committerNed Batchelder <ned@nedbatchelder.com>2018-11-11 16:45:33 -0500
commite5dcb933ab791206040a849eacd726ffe40c348a (patch)
tree81ea6018218db5436766f1d46ff999351e33b82c
parentcc12f4b7c40347b7297f7f6d938150bfde8c9ed5 (diff)
downloadpython-coveragepy-git-e5dcb933ab791206040a849eacd726ffe40c348a.tar.gz
Python 3.8 will optimize away "while True:"
-rw-r--r--coverage/env.py2
-rw-r--r--coverage/parser.py21
-rw-r--r--tests/test_arcs.py37
-rw-r--r--tests/test_concurrency.py2
4 files changed, 52 insertions, 10 deletions
diff --git a/coverage/env.py b/coverage/env.py
index fd98fa2b..d97b193c 100644
--- a/coverage/env.py
+++ b/coverage/env.py
@@ -39,6 +39,8 @@ class PYBEHAVIOR(object):
# (old behavior)?
trace_decorated_def = (PYVERSION >= (3, 8))
+ # Are while-true loops optimized into absolute jumps with no loop setup?
+ nix_while_true = (PYVERSION >= (3, 8))
# Coverage.py specifics.
diff --git a/coverage/parser.py b/coverage/parser.py
index 6ae81c19..1c19f69e 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -711,6 +711,13 @@ class AstArcAnalyzer(object):
node = None
return node
+ # Missing nodes: _missing__*
+ #
+ # Entire statements can be optimized away by Python. They will appear in
+ # the AST, but not the bytecode. These functions are called (by
+ # find_non_missing_node) to find a node to use instead of the missing
+ # node. They can return None if the node should truly be gone.
+
def _missing__If(self, node):
# If the if-node is missing, then one of its children might still be
# here, but not both. So return the first of the two that isn't missing.
@@ -738,6 +745,20 @@ class AstArcAnalyzer(object):
return non_missing_children[0]
return NodeList(non_missing_children)
+ def _missing__While(self, node):
+ body_nodes = self.find_non_missing_node(NodeList(node.body))
+ if not body_nodes:
+ return None
+ # Make a synthetic While-true node.
+ new_while = ast.While()
+ new_while.lineno = body_nodes.lineno
+ new_while.test = ast.Name()
+ new_while.test.lineno = body_nodes.lineno
+ new_while.test.id = "True"
+ new_while.body = body_nodes.body
+ new_while.orelse = None
+ return new_while
+
def is_constant_expr(self, node):
"""Is this a compile-time constant?"""
node_name = node.__class__.__name__
diff --git a/tests/test_arcs.py b/tests/test_arcs.py
index 0cf4dd3b..324bd53f 100644
--- a/tests/test_arcs.py
+++ b/tests/test_arcs.py
@@ -248,6 +248,10 @@ class LoopArcTest(CoverageTest):
def test_while_true(self):
# With "while 1", the loop knows it's constant.
+ if env.PYBEHAVIOR.nix_while_true:
+ arcz = ".1 13 34 45 36 63 57 7."
+ else:
+ arcz = ".1 12 23 34 45 36 63 57 7."
self.check_coverage("""\
a, i = 1, 0
while 1:
@@ -257,11 +261,13 @@ class LoopArcTest(CoverageTest):
i += 1
assert a == 4 and i == 3
""",
- arcz=".1 12 23 34 45 36 63 57 7.",
+ arcz=arcz,
)
# With "while True", 2.x thinks it's computation,
# 3.x thinks it's constant.
- if env.PY3:
+ if env.PYBEHAVIOR.nix_while_true:
+ arcz = ".1 13 34 45 36 63 57 7."
+ elif env.PY3:
arcz = ".1 12 23 34 45 36 63 57 7."
else:
arcz = ".1 12 23 34 45 36 62 57 7."
@@ -287,22 +293,31 @@ class LoopArcTest(CoverageTest):
""")
out = self.run_command("coverage run --branch --source=. main.py")
self.assertEqual(out, 'done\n')
+ if env.PYBEHAVIOR.nix_while_true:
+ num_stmts = 2
+ else:
+ num_stmts = 3
+ expected = "zero.py {n} {n} 0 0 0% 1-3".format(n=num_stmts)
report = self.report_from_command("coverage report -m")
squeezed = self.squeezed_lines(report)
- self.assertIn("zero.py 3 3 0 0 0% 1-3", squeezed[3])
+ self.assertIn(expected, squeezed[3])
def test_bug_496_continue_in_constant_while(self):
# https://bitbucket.org/ned/coveragepy/issue/496
- if env.PY3:
- arcz = ".1 12 23 34 45 53 46 6."
+ # A continue in a while-true needs to jump to the right place.
+ if env.PYBEHAVIOR.nix_while_true:
+ arcz = ".1 13 34 45 53 46 67 7."
+ elif env.PY3:
+ arcz = ".1 12 23 34 45 53 46 67 7."
else:
- arcz = ".1 12 23 34 45 52 46 6."
+ arcz = ".1 12 23 34 45 52 46 67 7."
self.check_coverage("""\
up = iter('ta')
while True:
char = next(up)
if char == 't':
continue
+ i = "line 6"
break
""",
arcz=arcz
@@ -685,10 +700,12 @@ class ExceptionArcTest(CoverageTest):
def test_bug_212(self):
# "except Exception as e" is crucial here.
+ # Bug 212 said that the "if exc" line was incorrectly marked as only
+ # partially covered.
self.check_coverage("""\
def b(exc):
try:
- while 1:
+ while "no peephole".upper():
raise Exception(exc) # 4
except Exception as e:
if exc != 'expected':
@@ -701,8 +718,10 @@ class ExceptionArcTest(CoverageTest):
except:
pass
""",
- arcz=".1 .2 1A 23 34 45 56 67 68 7. 8. AB BC C. DE E.",
- arcz_missing="C.", arcz_unpredicted="CD")
+ arcz=".1 .2 1A 23 34 3. 45 56 67 68 7. 8. AB BC C. DE E.",
+ arcz_missing="3. C.",
+ arcz_unpredicted="CD",
+ )
def test_except_finally(self):
self.check_coverage("""\
diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py
index 16d1af81..2877b7c2 100644
--- a/tests/test_concurrency.py
+++ b/tests/test_concurrency.py
@@ -115,7 +115,7 @@ SUM_RANGE_Q = """
def run(self):
sum = 0
- while True:
+ while "no peephole".upper():
i = self.q.get()
if i is None:
break