Files
codeql/python/ql/test/library-tests/ControlFlow/evaluation-order/timer.py
Taus 3402d0eaeb Python: Add self-validating CFG tests
These tests consist of various Python constructions (hopefully a
somewhat comprehensive set) with specific timestamp annotations
scattered throughout. When the tests are run using the Python 3
interpreter, these annotations are checked and compared to the "current
timestamp" to see that they are in agreement. This is what makes the
tests "self-validating".

There are a few different kinds of annotations: the basic `t[4]` style
(meaning this is executed at timestamp 4), the `t.dead[4]` variant
(meaning this _would_ happen at timestamp 4, but it is in a dead
branch), and `t.never` (meaning this is never executed at all).

In addition to this, there is a query, MissingAnnotations, which checks
whether we have applied these annotations maximally. Many expression
nodes are not actually annotatable, so there is a sizeable list of
excluded nodes for that query.
2026-05-05 15:21:39 +00:00

186 lines
4.8 KiB
Python

"""Abstract timer for self-validating CFG evaluation-order tests.
Provides a Timer context manager and a @test decorator for writing tests
that verify the order in which Python evaluates expressions.
Usage with @test decorator (preferred):
from timer import test
@test
def test_sequential(t):
x = 1 @ t[0]
y = 2 @ t[1]
z = (x + y) @ t[2]
Usage with context manager (manual):
from timer import Timer
with Timer("my_test") as t:
x = 1 @ t[0]
Timer API:
t[n] - assert current timestamp is n, return marker
t[n, m, ...] - assert current timestamp is one of {n, m, ...}
t["label"] - record current timestamp under label (development aid)
t(value, n) - equivalent to: value @ t[n]
Run a test file directly to self-validate: python test_file.py
"""
import atexit
import sys
_results = []
class _Check:
"""Marker returned by t[n] — asserts the current timestamp."""
__slots__ = ("_timer", "_expected")
def __init__(self, timer, expected):
self._timer = timer
self._expected = expected
def __rmatmul__(self, value):
ts = self._timer._tick()
if ts not in self._expected:
self._timer._error(
f"expected {sorted(self._expected)}, got {ts}"
)
return value
class _Label:
"""Marker returned by t["name"] — records the timestamp under a label."""
__slots__ = ("_timer", "_name")
def __init__(self, timer, name):
self._timer = timer
self._name = name
def __rmatmul__(self, value):
ts = self._timer._tick()
self._timer._labels.setdefault(self._name, []).append(ts)
return value
class _NeverCheck:
"""Marker returned by t.never — fails if the expression is ever evaluated."""
def __init__(self, timer):
self._timer = timer
def __rmatmul__(self, value):
self._timer._error("expression annotated with t.never was evaluated")
return value
class _DeadCheck:
"""Marker returned by t.dead[n] — fails if the expression is ever evaluated."""
def __init__(self, timer):
self._timer = timer
def __rmatmul__(self, value):
self._timer._error("expression annotated with t.dead was evaluated")
return value
class _DeadSubscript:
"""Subscriptable returned by t.dead — produces _DeadCheck markers."""
def __init__(self, timer):
self._timer = timer
def __getitem__(self, key):
return _DeadCheck(self._timer)
class Timer:
"""Context manager tracking abstract evaluation timestamps.
Each Timer instance maintains a counter starting at 0. Every time an
annotation (@ t[n] or t(value, n)) is encountered, the counter is
compared against the expected value and then incremented.
"""
def __init__(self, name="<unnamed>"):
self._name = name
self._counter = 0
self._errors = []
self._labels = {}
self.dead = _DeadSubscript(self)
self.never = _NeverCheck(self)
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self._labels:
for name, timestamps in sorted(self._labels.items()):
print(f" {name}: {', '.join(map(str, timestamps))}")
_results.append((self._name, list(self._errors)))
if self._errors:
print(f"{self._name}: FAIL")
for err in self._errors:
print(f" {err}")
else:
print(f"{self._name}: ok")
return False
def _tick(self):
ts = self._counter
self._counter += 1
return ts
def _error(self, msg):
self._errors.append(msg)
def __getitem__(self, key):
if isinstance(key, str):
return _Label(self, key)
elif isinstance(key, tuple):
return _Check(self, list(key))
else:
return _Check(self, [key])
def __call__(self, value, key):
"""Alternative to @ operator: t(value, 4) or t(value, [1, 2, 3])."""
if isinstance(key, list):
key = tuple(key)
marker = self[key]
return marker.__rmatmul__(value)
def test(fn):
"""Decorator that creates a Timer and runs the test function immediately.
The function receives a fresh Timer as its sole argument. Errors are
collected (not raised) and reported after the function completes.
"""
with Timer(fn.__name__) as t:
try:
fn(t)
except Exception as e:
t._error(f"exception: {type(e).__name__}: {e}")
return fn
def _report():
"""Print summary at interpreter exit."""
if not _results:
return
total = len(_results)
passed = sum(1 for _, errors in _results if not errors)
print("---")
print(f"{passed}/{total} tests passed")
if passed < total:
sys.exit(1)
atexit.register(_report)