Repository navigation
Upgrading from 0.3.0: make your execution-evidence recordings again. The
store format is now 6 (0.3.0 wrote 1): --evidence auto skips an older
recording, naming one is an error, and the run action plans from the
code until record makes a new one. Expect plans to select more tests
than 0.3.0 did: most of the changes below close cases where diffcone
missed an affected test. The JSON report keeps schema version 3 and gains
fields: always_run and analysis.counts.always_run;
environment_checked and preloaded_modules in analysis.evidence;
rootdir, configfile, junit_prefix and run_arguments in pytest's
discovery[].config; and evidence_not_used in the plan run -o writes.
check --format json gains mapping.rootdir and mapping.junit_prefix.
New discovery notes: unparsable_config (discovery incomplete),
unreadable_plugins and plugin_outside_roots.
Planning
diffcone.tomltakes[[always_run]]entries: the tests (or
benchmarks) whose id matches a pattern are selected in every plan, with
or without a recording, and the report gives the entry as the reason and
counts the targets each entry matched; the text report flags an entry
that matches nothing. A malformed entry, or one naming an unknown
runner, is an analysis error, so the plan selects everything rather than
quietly leaving those tests out.diffcone reportcomputes a job's
selected share without them.- A test is selected when the fixtures, hooks or plugins around it change
though its own code did not (rulelifecycle_changed), for example when
another test module adds a plugin topytest_plugins; with a recording,
compared with what ran around it when it was recorded. pytest plugin
modules are dependencies of every test, since their import-time code
runs for the whole session. - diffcone now assumes a test does not depend on what an earlier test left
behind. A change to module state made by code that runs only inside one
test counts for that test only, not for the other tests and benchmarks
reading the same state: the test itself, a function-scoped fixture,
unittest'ssetUp/tearDown, and a helper in a test module or
conftest that only such code calls. A patch undone when the test ends
counts the same way, also for lookups on a library module it patches:
monkeypatchandmockermethods on pytest's fixtures, awith mock.patch(...)block, a@mock.patch(...)decorator,
MonkeyPatch.context(), and a test or fixture that saves a module
attribute and puts it back. Still counted for every reader: a test
module's top-level code, wider-scoped fixtures, hooks, all project code
(a cache a test fills through it), a doctest's function, a manifest
entry on project code, a test orsetUpthat module-level code, a
wider-scoped fixture or a hook calls, and a store through a
monkeypatchthat a wider-scoped fixture requests or the test module
overrides (one overridden in a parent conftest is still taken as
undone). If a test relies on an earlier test's state,
declare the dependency indiffcone.toml. - A fixture or function that only empties or fills a module-level dict,
list or set (REG.clear()), or resets another module's variable
(mod.X = None), does not count as reading it: an autouse reset fixture
no longer selects every test when the code filling the registry
changes. - An attribute read on an object of unknown type is matched to
module-level functions, classes and variables only in modules such an
object can be: modules passed around or bound to another name, obtained
by name or named in a string, and test modules and conftests, which
pytest hands out. A name bound only to modules (m = ops, then
m.value()) is followed like the import itself. Methods still match
whatever the receiver. - A text a test writes or runs (
pytester.makepyfile,exec,-ccode)
whose template field the test's own parameter fills, when
@pytest.mark.parametrizeon the test gives that parameter literal
values, is read once per value instead of counting as text built at run
time, which any change reached. A value that is not a literal, an
indirectparameter, another decorator on the test, or a parametrize
on its class or inpytestmarkkeeps the old rule. - Indexing a very long function is no longer quadratic in its calls.
- Editing the docstring of a method under
@classmethod,@staticmethod
or@propertyno longer counts as code run at import (which selected
every test importing the module): these builtins only copy the
docstring. Code that reads the docstring is still selected, and so is a
project decorator with one of those names.
Fixed
Static analysis
- A change to a dependency, build or CI file outside the source roots
selected nothing when the roots don't hold the repository root. These
now select every target: lock files (uv.lock,poetry.lock, any
*.lock,pylock.toml,conda-lock.yml),requirements*files (also
dev-requirements.txt,Requirements.txt,requirements.pip),
environment*files, a.txt/.yml/.yamlfile in a directory named
depsordependencies, a workspace member'spyproject.toml,
uv.toml,hatch.toml,poetry.toml,pdm.toml,pytest.toml,
.envand.pthfiles,sitecustomize.py,noxfile.py,
.github/workflows/, checker configurations that pytest plugins run as
tests (mypy.ini,ruff.toml), compiled sources, anything under a
*.dist-info,*.egg-infoor*.eggdirectory, a build script
anywhere under a root (a nestedsetup.py), and a lock-file change
betweenINDEXandWORKTREE. Evidence mode treats them the same way. - A name taken from a dict, list or set written in the code
(getattr(handlers, NAMES[key]), a lazy-export table) is bounded by it
only while every use of the table is a read: a table changed through an
alias, a helper it is passed to, its module (core.TABLE[k] = v),
globals(),vars(),sys.modules,monkeypatchormock.patch(an
object or a dotted string) is no longer read as its literal. A
parameter or class-body name that shares its name with such a table is
no longer bounded by it. - A lookup on a standard-library or third-party module
(getattr(logging, name)) counts writes that reach the module
indirectly: an alias, a helper it is passed to, a loop,__setattr__,
vars(), a module found at run time, a submodule handed on
(register(os.path)). Handing a library object such as
datetime.timezone.utcto a call does not make every lookup on that
library depend on the caller. - Modules whose name is known only at run time: once code can reach one,
no literal table is bounded. A name that starts with fixed text
(import_module(f"plugins.{name}")) stands for the modules with that
prefix; a newtypes.ModuleTypeis no project module unless it is
installed insys.modulesunder one's name;exec(code, namespace)
does not reach the calling module;m.__dict__["X"] = vchanges only
X.runpy.run_module(name),runpy.run_path(path)andpython -m <name>with a name nothing bounds count as an import of any module, as
importlib.import_module(name)does. - An object put in
sys.modules(assignment,setdefault,update,
monkeypatch.setitem,mock.patch.dict) was ignored. It is now the
module of that name: lookups on that module see it, and while it stays
installed, the module's importers depend on the code that installed it.
Without a recording, code that installs an object under a name computed
at run time makes every test importing a project module depend on it. - A change to a module-level variable made through a module object the
analysis did not resolve to the variable's module is a dependency of
the variable's readers:importlib.import_module("pkg.reg").TABLE["x"] = f,sys.modules[...], a function-local import, a star import, or a
module passed to a helper that writes into it. A function's
__globals__, a frame'sf_globals,pickle.loads,
pkgutil.resolve_name,pydoc.locate,gc.get_objects()and
importlib.util.module_from_speccopies are recognised as ways to
change a module's tables, and a lookup by a computed name on a module
sees objects other code puts on that module. Star-imported names
(from sys import *, thenmodules[...]) are seen too. - State changed through an argument or a receiver (
register(REGISTRY),
registry.add(x)) did not reach its readers, in either mode; nor did a
change to code that may call such a writer differently, or a write
through a module attribute and an item (store._CACHE["k"].append(x),
del store._CACHE[k]). A method called on an instance attribute
(self._client.get()) counts as that class's method only when every
assignment of the attribute creates an instance of one class (or of a
third-party class), and a module-level object made by a third-party
call is changed only by methods of classes deriving from a third-party
class; calls on such attributes are still matched by name. A function
a module-level variable's initialiser calls runs at import, so a change
to it reaches the module's importers. - Changing the arguments of a call made at import time (
X = set_mode("slow"), a class attribute, a decorator's or default's
argument) did not select the tests that read the state the callee
changes. An import added to or removed from a module that starts or
stops running another module's import-time code reaches the readers of
what that code writes (a registry filled at import), in both modes;
added imports stay non-structural otherwise. - Moving a module's or a class's statements around (a constant moved
below the decorator that reads it, two@registerfunctions swapped, a
defmoved below atry/exceptimport of the same name) was no
change at all: the order of statements both sides have is now part of
the definition, so a reorder reaches importers. Only swapping
neighbouring plaindefs (no decorator that does anything, no
annotation evaluated at import) still selects nothing. - A decorator diffcone can't show leaves the function alone (one from an
installed package, or a project decorator that calls or registers it)
makes the decorated function count as code that runs on import, and its
parameters are no longer bounded by its call sites. A name imported in
atry/exceptorif/else, rebound by a star import, or redefined
in the module is no longer taken fortyping.final, a quiet decorator
or a structural base; attribute uses of a name bound several ways
depend on every binding. - A local name, parameter or function-local import was taken for a
module-level import or builtin of the same name, and a call of an
unresolved function namedrebinds,registersormutated_byfor one
of diffcone's own kinds of dependency. - A dynamic import, script path or
getattrreceiver bounded by its call
sites is unbounded when a caller may be unseen (a call through an object
of unknown type, an unseen construction). - Reading members by name or wholesale is a dependency: an attribute a
class does not define (C.__doc__,C.__type_params__, one a decorator
sets),C.__dict__[name]andobj.__dict__[name],mod.__dict__[name]
andinspect.getmembers(mod)(a test parametrized over a module's
functions),matchclass patterns,operator.attrgetter/
methodcallerandstr.formatfields ('{0.attr}'); a non-literal
name is a dynamic reference.C.__subclasses__(),C.__mro__and
C.__bases__reach the classes they return. - Code that passes a module to other code (
read(ops, name)) depends on
that module's functions and on what its imports bind (api.core.TABLE);
a module from a literalimport_module("pkg.sub")or__import__that
is handed on counts as handed on. An instance handed to code outside the
project (json.dump(x, w),print(file=w),dict(obj),
csv.writer(w)) depends on every method of its class, not only its
dunder methods. - A class obtained by a name nothing resolves (
m.Alt()on an object of
unknown type, a lazy__getattr__export) reaches its__init__and
__new__. A class with anenum.Enum-family base or an in-scope
metaclass depends on its__init__/__new__, which run when the class
is created (Color.RED.codereachesColor.__init__);dir(mod)
depends onmod.__dir__, anddir,hasattrandvarsof a library
module your code writes to are lookup sites. - A docstring-only change selects the tests that read that docstring, in
both modes, also through a parameter (inspect.getdoc(fn)in a
helper),getattr(obj, "__doc__")and a module's__doc__. - A change to PEP 695/696 type parameters (bounds, defaults, their number)
was no change at all; a bound or default naming project code is now a
dependency. A subscript in an evaluated annotation (X: Registry["k"] = 1) counts as code that runs on import. - Code a test runs in a new process: a test running a script path,
-m moduleor-ccode named in the code did not depend on it. A module's
file is an import of it; a.pyfile no module name maps to
(scripts/gen-data.py) is read as-ccode is (what it imports counts,
and a change to the file reaches the test; if it imports by a name it
computes, any change does); a Python command whose program is
built at run time is affected by any change. A child whose interpreter
is not the first word (env X=1 python -m ...,uv run python ...,
timeout 30 python ...,asyncio.create_subprocess_exec(sys.executable, ...),os.execl) is followed.python -ccode that changes
sys.path,sys.meta_pathorsys.modulesby any assignment, deletion
or augmented assignment, or callsos.chdir, can import anything. - Text a test writes out and runs (
exec,eval,compile,timeit,
pytester.makepyfile) is read as the code it is: its imports and calls
count. Text built at run time from pieces that are not written out can
import anything. - A test file given to a test runner (
python -m pytest path,
pytest.main([...]),python -m unittest module,unittest.main (module=...),loadTestsFromName) counted as an import; it runs whole,
conftests and fixtures included.unittestdiscovery,
doctest.testfile,DocFileSuiteandDocFileTest, a directory or a
non-literal argument can run anything, so any change selects the test. - Functions obtained from
pickle.load/loads(and cloudpickle, dill,
joblib) or from a module loaded by a computed file path were invisible
without a recording; such code is now affected by any change. A round
trip (loads(dumps(x)), a temporary file), pickle bytes written in the
code and a path in the module's own directory are bounded by what they
load. - Patches: one that may outlast its test (
patch(...).start(), a patcher
kept for later, aMonkeyPatch()of your own, awith mock.patch(...)
around a fixture'syield,setattron any other receiver) is a store
on the module it patches, so lookups on that module follow it, and set
insys.modulesthat way it counts as installed for good. Awith mock.patch.dict(os.environ)orwarnings.catch_warnings()block whose
body yields does not put the process state back.mock.patch.multiple ("pkg.mod", NAME=...)depends on those names and the module; any dotted
patch target counts as an import of its module, also through a patcher
called on a value (pytest.MonkeyPatch().setattr("pkg.mod.NAME", ...)). - A write to process-global state (
os.environ, warning filters,
sys.path) is recognised whichever import binds the name:from os import *, an import intrywith another inexcept, or a
function-local one. Awithstatement counts as putting state back only
when every binding of its name is the restoring one. - Snapshots:
WORKTREEdid not see edits to files flagged
assume-unchanged or skip-worktree. It now reads git-ignored.pyfiles
in directories git does not ignore as a whole (a generated
_version.py,local_settings.py); ignored directories are still
skipped. File mode changes (chmod +x) under the source roots are
changes, in commits and the working tree. A submodule under the roots
no longer makes everyWORKTREEplan select everything (one checked out
at another commit, or with uncommitted edits, still counts as a
change); a path containing a line break, or a tracked file replaced by
a directory, no longer fails the plan. - A file nested too deeply to analyse is an analysis error that selects
everything (a test, conftest or benchmark module: a file that did not
parse, which makes discovery incomplete), instead of a crash. A
diffcone.tomlthat is present but can't be read (left unmerged by a
conflicted merge) is an analysis error too; it was read as no file. A
manifest that is not valid JSON says so.
Discovery
- A fixture defined with a decorator whose name ends in
fixture
(pytest-twisted'sasync_fixtureandasync_yield_fixture,
pytest-trio'strio_fixture) was not seen; when its name was also a
well-known plugin's (pytest-django'sclient), it was taken for that
plugin's fixture and what it requested reached nothing. Such decorators
now make fixtures, and a plugin's fixture name is no longer assumed when
the test's class, module, conftests or plugins define it in a way
diffcone can't read: the name is an unknown fixture, selected on every
change. - An autouse fixture imported into a conftest, a test module or a plugin
(by name, through star imports at any depth, or as a module attribute)
was not a dependency of the tests it applies to; nor was a fixture with
name=imported under its function name. - Conftests pytest loads at startup (in the
test*directories of a path
it starts from) count even when ignored or undernorecursedirs;
--ignore,--ignore-globandnorecursedirsapply only below the
paths pytest starts from, and not under a conftest whose
pytest_ignore_collectcan return False. pytest_pluginsis read however it is built (+,+=,.append,
.extend, a comma-separated string, a variable) or imported from
another module; one that can't be read keeps every test selected
(unreadable_plugins), as does a plugin module in the repository
outside the source roots (plugin_outside_roots). Thepytest_*
methods of an object registered as a plugin
(config.pluginmanager.register(...)), and plugins registered with
import_plugin("name"), apply to every test.pytestmarklists built with+,+=,.appendor.extend, or
imported, apply theirusefixtures;indirect=given as a variable, as
the third positional argument or through**kwargsno longer drops the
fixture from the test's dependencies.- Tests defined, aliased or marked
__test__ = Trueinsideif/try
blocks, and tests imported from such modules, are found; an imported
test discovery can't follow is reported, not dropped. - Code pytest runs while collecting that changes the process (a conftest,
a test module or a module they import that sets an environment variable
or extendssys.pathat import, apytest_generate_teststhat does,
also through a patch nothing undoes:mock.patch.dict(os.environ, ...).start(),pytest.MonkeyPatch().setenv(...),syspath_prepend)
is a dependency of every test. - A collection hook imported into a conftest (by a star import, or from
outside the source roots), or a star import from outside the roots in a
conftest or plugin, makes discovery possibly incomplete. - A
pytester/testdirtest depends on the project's registered plugins
(pytest11entry points,pytest_plugins,-p) and on what its inline
files import. - ASV:
setUpandTearDownin any case, and the benchmark class's
__init__, are dependencies of its benchmarks; atimeraw_benchmark
bound under another name, or renamed withbenchmark_name, depends on
what its code imports;benchmark_nameapplies wherever the function is
bound, inherited methods included;benchmark_dir: "."finds
benchmarks.
Execution evidence
- A module stored into pytest's
doctest_namespace(pandas' autouse
add_doctest_importsdoesdoctest_namespace["pd"] = pd) no longer
counts as handed to unknown code. When that module could reachgc,
every test that ran the fixture counted as able to look up anything, so
a docstring edit selected every test. - A module variable filled once by one function and then only read (a
lazily computed constant such as pandas'_pkg_dirin
find_stack_level) no longer counts as state that function writes for
later tests: the value is the same whoever computes it first, so a
change that only makes the function run earlier or later reaches nothing
through it. A change to the function itself, or a reader outside it,
still counts. - Code that ran while pytest collected a test file (building the ids of
parametrized tests, anids=function,pytest_generate_testsand what
it calls) now selects that file's tests when it changes, and the tests
reading what it stored, instead of every test; so does a change to a
file read then. It still selects every test when what ran it can set
something for the whole process (an environment variable) or reaches
pytest's configuration or session (.config,.session,.stash,
.pluginmanager, another hook). Code run by session hooks
(pytest_configure,pytest_collection_modifyitems) selects every test
as before. - A test that failed or errored while it was recorded is always selected:
its recording stops where it failed, so where it passes (a flaky failure,
a different environment) it can run code the recording never saw.
VIRTUAL_ENVis now part of the environment a recording is checked
against: a tool run from its own environment passes itsVIRTUAL_ENVto
the suite, and IPython warns on one that is not the interpreter's. - A test module listing the machine's locales while pytest collected it
(locale -a, as pandas' locale tests do) made every change select every
test. Listing locales runs no Python, and no longer counts as a process
the recording can't follow. Nor doesninja, which an editable
meson-python install (pandas) runs on import to rebuild compiled code. - Before Python 3.14, a test that had a library evaluate string
annotations (from __future__ import annotations, forward references)
throughtyping.get_type_hints, as pydantic and FastAPI do, recorded
nothing of them, so a change to those annotations, or to a class they
name, did not select it. The recorder now credits the test with the
annotations' owner and what their names resolve to, throughtyping,
annotationlibandinspectalike;inspect's evaluation no longer
counts as running text code, which selected the test on every change. - A recording no longer disables pytest's cache plugin: a configured
--failed-firstmade recording a usage error. The plugin starts from
an empty cache of its own, so--lf/--ffneither narrow nor reorder
the recorded run, and the repository's.pytest_cacheis left alone. - Editing one test selected every test of its file: pytest looks up each
test's location as the test starts, which read the file inside the
test's record. That lookup is no longer recorded; a test that reads its
own file still is. - On Python 3.14, editing the body of a function whose annotations were
read outside the tests selected every test, or every test importing its
module. Python 3.14 evaluates annotations in code of their own, which
pytest runs while collecting (reading a signature) and frameworks run at
import (FastAPI, dataclasses, pydantic). That code no longer counts as
the function running there, nor as reading a change its annotations
don't name; a change to the annotations still counts. - Editing a test file selected every test when the suite was recorded
with a virtual environment inside the checkout (uv run pytestruns
.venv/bin/pytest): pytest reading files during collection looked like
the project reading them outside any test. An environment in the
checkout, or inside a source directory (where it hid that directory's
code or gave a false "installed copy" error), is now installed code. - A session hook that lists installed distributions
(importlib.metadata.distributions()) made adding any file anywhere
select every test; those scans are no longer recorded. - The recorder starts before pytest loads any
-pplugin (addopts,
PYTEST_ADDOPTS, the command) and before a projectsitecustomize.py,
so what they import is recorded. Modules imported before it could start
are listed (preloaded_modules),collectwarns, and any change in
them is planned from the code. - Files the recording missed are recorded: files a library reads while it
is imported (matplotlib'smatplotlibrc), files reached through a
directory descriptor (dir_fd=,os.listdir(fd),os.fstat), entries
of anos.scandirlisting,os.readlink,sqlite3file:URIs,
symlinked paths, and on macOS and Windows a file opened under another
capitalisation. A descriptor the recorder cannot trace flags the test. - A process started while a
conftest.pyis imported or in a hook was not
credited to anything; what it runs now counts as part of that import or
hook, and one the recording cannot follow makes every change plan from
the code (pytest-xdist workers are not affected). A test using a
multiprocessingpool, forkserver or forked process that an earlier
test started is always selected while that process runs. - Tests that run code compiled from text no file holds (doctests,
--doctest-modules,timeit, a notebook kernel) are always selected
(ruletext_code). A stringskipifcondition naming nothing of the
project andautospec=Truemocks do not count as such text. - A change to code that runs at import, or that ran during an import,
reaches the tests reading what it writes into another module, Cython
code included. Code that can obtain a test module without importing it
by name sees changes to it: library code importing one by a computed
name, an unpickler, a loader of a computed path. A lazy-export
__getattr__whose table only test-time code can change does not count
as such code, nor dogc.get_stats(),gc.collect()and
gc.callbacks, andhasattr(obj, "name")notices onlyname.
pytest_pluginsin a test module selects every test when it changes. - A function added, deleted or redefined with an annotation that calls
code (def f(x: set_mode("fast"))), or a module-level variable whose
annotation does, reaches what that code writes: before Python 3.14
annotations run with the definition. A definition whose annotations call
nothing is not escalated. - Caches: a module-level cache filled by an earlier test hid the function
that filled it from later tests' records, in every order; a change to
that function now reaches the tests reading the cache. So does a cache
kept on an object, a class, a closure, a function attribute or in a
memoising decorator diffcone knows (functools.cached_property,
cachetools, Django's and Werkzeug'scached_property); a decorator
from an installed package it doesn't know is taken to keep nothing. A
store made by a test's own code counts for that test only. - A non-literal
getattron a standard or third-party module sees
changes anywhere in the project, not only in its own imports, and a
test module's import-time change reaches lookups in library code. - Code a hook ran that changed, or that reads a change, selects every
test. - Cython: edits to blank lines or
#lines inside a string in a function
body were ignored. - A recording stored under a path containing
#,?or%was skipped
as if there were none. - When
runcouldn't use a recording because only-OorPYTHONPATH
differed, it listed no differences; every difference is now named.
Running, check and validate
validateandcorpuscould not run the suites of several projects.
A checkout of a commit lacks a git-ignored, build-generated module such
as the_version.pysetuptools-scm or hatch-vcs writes, so the package
could not be imported; the repository's copy of each git-ignored module
inside a package is now copied into every checkout that lacks it
(not a conftest or test file;--setup-commandruns after it). pytest's cache plugin is no longer
disabled, which made a configured--failed-firstor--lfa usage
error; it starts from an empty cache of its own instead, so those options
neither narrow nor reorder the run, and the repository's.pytest_cache
is left alone (a run whose cache could not be moved is refused). With--coverage, a suite that did not run is reported
as such instead of as a missing coverage database; pytest-cov is loaded
with-pwhen plugin autoloading is disabled; the database's place is
set in diffcone's coverage config instead ofCOVERAGE_FILE, which the
tests' own processes inherited; and parallel parts beside it are read
too. Errors name the snapshot (base <commit>orhead) whose suite
failed.validateandcorpusreported tests outside the target list as
misses even when the plan said discovery was incomplete (it exits 3,
andrunrefuses it). They are now listed as "outside the target list"
and don't fail the check, and coverage recall is measured over the
targets. When discovery claims to be complete, such a test is still a
miss, labelledMISSED (not a target). The JSON gains
discovery_incomplete,outside_targetsand atargetflag on each
record;counts.targetsnow counts targets only.diffcone checkdecided "already failing at the baseline" per test
function, so a parametrized case that failed at the baseline excused a
new failure of another case: a miss reported as clean. Cases are now
compared one by one.checkandvalidatework when pytest's rootdir lies below the
repository (an ini file in a subdirectory): the plan records pytest's
rootdir, configuration file and--junit-prefix,check --rootdirand
--junit-prefixoverride them, and a directory that fails to collect
counts as a collection failure of every test under it. Before, every
failure was an unknown miss, which therecordaction confirmed.validate,corpusandcollectrun the whole suite whatever-xor
--maxfailis configured, andvalidateandcorpusstop with exit
code 2 when the suite reported fewer outcomes than it collected: a miss
can no longer hide behind an early stop.validateruns pytest without
-vand reports the command line it ran.runrefuses a working tree that differs from the planned snapshot in
any file the plan would have selected on (data and configuration files
under the roots,diffcone.toml, runner configuration, build inputs,
conftests, ignored.pyfiles, file modes, dirty submodules, files
flagged assume-unchanged or skip-worktree,.pyfiles with non-ASCII
names) and names them.run's "did not collect" message names--lf,--ff,--sw,-k,
-mor--deselectamong the effective pytest arguments and where
they are set.collectandrun --collectno longer refuse a clean checkout because
untracked bytecode or ASV output has a non-ASCII name; a renamed file
counts as both names.run --dry-run --evidenceprints a command you can run, without
diffcone's internal recorder plugin.discoverhonours--no-cacheand--cache-dir: it uses the same
cache asplan, and its output is unchanged.- On Windows, a relative
--commandresolved forvalidateandcorpus
was quoted for a POSIX shell, and one written with forward slashes
(.venv/Scripts/python.exe) was not resolved. - An unexpected error in any command exits 2;
check,report,
collectandpruneexited 1, whichcheckuses for a miss.
CI actions
- The
recordaction withcheck: truecould pass a push that had a
miss: a re-run of the job restored the newest recording instead of its
own commit's (so the kept verdict was lost and the push was re-checked
against a later baseline), and a check that crashed, or a re-run whose
JUnit could not be read, was recorded as checked with no miss. A
recording that is not from before the pushed commit is no longer
checked against. A re-run's other failures (when the job's pytest
arguments name a path after an option) no longer count as misses, and
the re-run of a missed test runs acommandwith quotes or spaces as
written. diffcone reportignores recording artifacts uploaded by pull-request
runs (a pull request from a fork runs its own code), counts an artifact
it cannot read as a problem instead of failing, reports pull-request
runs whose selected tests pytest did not collect, keeps a test id with
backticks inside its code span, and escapes|in "not checked"
reasons.- The
reportaction failed whenever a job of a reported run had been
re-run (two artifacts of the same name); it reads the latest attempt's.
Its window starts at the last report that posted, so a run with only a
job summary (a manual run with posting off) no longer hides the runs
before it. - The
recordaction's re-run of a new unselected failure stopped at the
first failure whenpytest-argsoraddoptshad-xor--maxfail,
and the misses it never reached were reported as flaky. It now re-runs
with--maxfail=0, and a missed test the re-run did not run still
counts as a miss.
Discovery
- pytest options in
PYTEST_ADDOPTS, plugins inPYTEST_PLUGINSand
pytest arguments written into the run's command are read:planand
discovertake--command, the CI actions pass it, and the report
lists what was read (run_arguments). A command whose pytest arguments
cannot be found is reported. - Understood now:
.as an argument, a.txtor.rstfile named as an
argument (always a doctest),--doctest-globpatterns with a directory,
option values written as separate arguments (--ignore tests/slow),
absolute paths inside the repository,-o name=valueoverrides and
paths inaddopts. - A pytest configuration file below the repository root that pytest
would use (one nearer the paths it starts from than the root's) is not
read by discovery, so it makes discovery incomplete
(unmodelled_runner_option, exit code 3;runrefuses without
--allow-incomplete-discovery). - A pytest configuration file (
pytest.toml,pytest.ini,
pyproject.toml,tox.ini,setup.cfg) or anasv.conf.jsonthat
can't be parsed made discovery quietly use the defaults; it now makes
discovery incomplete (unparsable_config, exit code 3), as does a
benchmark_diroutside the repository. asv.conf.jsoncis read likeasv.conf.json, and trailing commas in
either are accepted, as asv allows.
Execution evidence
- A Python process a test starts with
subprocess(directly, through
asyncio, or throughuv run) records itself on Linux and macOS, and
the test is credited with what it ran instead of being selected for
every change; so is every test that runs while the process is still
running (a server or a warm worker another test started). Apython -c
probe that runs project code is caught by its own recording, and a
project's ownsitecustomize.pydoes not stop subprocesses from
recording themselves. A process started another way (os.system, a
shell,multiprocessing, anything on Windows), one that cannot record
itself (a replaced environment,python -S, Python before 3.12), one
whose recording was cut short (a full disk), and one running project
code from a-csnippet or a script outside the source roots still
select their test for every change. - A test run that changes its own environment (a test that installs or
upgrades a package with the test interpreter) is named:collectwarns
with the packages, the recording keeps the environment it started in,
andrunsays when it meets that environment. The recording is still
keyed by the environment at the end, so such a run plans from the code. plan --evidencesays the environment was not checked
(environment_checked: false): onlyruncan check it, and its-o
report saystruewhen the environment matched.run --collecton a recording made with--reverse-checkruns the
selected tests again in reverse order, so the advanced records are
order-checked too.- A method or attribute added to (or changed or deleted on) a fake class
in a test module selects only the tests that ran library code calling a
method of that name (x.read()) and also ran code able to hand that
code a fake: code naming the class or a subclass, or finding it without
naming it (a base's__subclasses__(),gc, a frame's globals). A
fake built at import, in a decorator or during collection, or one whose
class or bases sit outside test code, still selects every such test. - A lookup on a standard or third-party module that tests patch
(dir(builtins)orhasattr(os, name)in library code, with
monkeypatch.setattr(builtins, ...)in a test) no longer makes every
change anywhere select every test that ran the lookup: the tests that
patch the module are selected through their own code. A module written
at import or in a hook is treated as before, and a change to code that
runs at import still reaches the lookup when it can run a writer. - Editing an ordinary module named
build.py(not at the repository root
or besidepyproject.toml/setup.cfg) selects the tests that ran it,
as static planning does, instead of every test. - Recordings no longer select every test for every change when pytest
plugins or test helpers ask the machine or the repository something
outside the tests:gitreporting the commit, branch, remote or
configuration (pytest-benchmark does as it starts) andps
(shellingham's shell detection) joinunameandlocale. Agit
command that prints a file or writes still counts. - Running the suite under coverage (
--covinaddopts) no longer makes
every source file look read by the module importing it: coverage
resolving a file it is about to measure is nobody's read. - Tests that build URLs with werkzeug (every Flask app) no longer count as
running code compiled from text, which selected them for every change. - A change to a module no test imports (a Sphinx
docs/conf.py) no
longer reaches code that looks names up on whatever it is handed (a
frame's globals, a function's__code__,sys.modules), which selected
every test importing that code. A module imported by any test, even
inside one, or by an import added in the same change, still does.
CI
recorddescribes every recording fordiffcone report, also without
check(a nightly recording) and when the recording failed: its
resultsoutput always holdscontext.json.reporttakesrepository(report on another repository's runs; a
public one needs no extra access),issues-repositoryand
issues-token(post the report and miss issues elsewhere, each source
repository with its own report issue), andcomment: on-problem
(comment only on a miss, a failed check or an incomplete report). Miss
issues outside diffcone's tracker link to a pre-filled diffcone issue.run -oaddsevidence_not_used(why the recording was not used, and
what differed) to the plan it writes, anddiffcone reportshows those
differences per job.