Conversation
Simulation._run_formula no longer sets trace, tracer and branch_name on the shared parameter tree when a simulation traces. Its formulas get a per-call TracingParameterNode instead, so the tax-benefit system, its clones and every other simulation on it stay untraced, and each traced simulation and branch records its own parameter reads under its own branch name. TracingParameterNodeAtInstant (and the new wrapper) answer special names and lookups on unfilled instances with AttributeError, so copy, deepcopy and pickle no longer recurse (Reform.modify_parameters deep-copies the tree). The dev extra and the smoke job require pytest-rerunfailures>=16.2,<17: 14.0 empties pytest's setup stack before a rerun without running its finalizers, so a module-scoped fixture of the rerun module was handed to every later module (pytest-dev/pytest-rerunfailures#278 restores them from 15.0). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On #560's Windows CI (runs 36967463690, 36968947271, 36969160069, 36972147369) a flaky test was rerun, and afterwards
tests/core/test_parameters.py::test_get_at_instantgot aTracingParameterNodeAtInstantandtests/core/test_reforms.py::test_modify_parametersoverflowed the stack, both on the same system object. Three defects chained together. This PR fixes each one, so breaking any single link would already have prevented the failure.1. A rerun leaked the module-scoped
tax_benefit_systeminto later modules (test tooling)make testrunspytest --reruns 2. The installed plugin is pytest-rerunfailures 14.0, the newest the old<15pin allows, and the lock pairs it with pytest 9.1.1. Before a rerun it emptiesitem.session._setupstate.stack(_remove_failed_setup_state_from_session) without running the finalizers on it. pytest registers a fixture's finalizer with its scope node only when it creates the value (FixtureDef.executereturnscached_resultearly otherwise). So the rerun module'stax_benefit_systemis never torn down, and every later module gets the same cached object.pytest-rerunfailures restores those finalizers from 15.0 (pytest-dev/pytest-rerunfailures#278, "fix compatibility with pytest 8.2 by restoring deleted finalizers"). 16.2 is the first release that declares pytest 9 support. Fix: the
devextra and the smoke job now requirepytest-rerunfailures>=16.2,<17, anduv.lockmoves to 16.7. The lock also syncs its stalepolicyengine-coreversion line (3.32.11 → 3.32.12, matchingpyproject.toml).2. Tracing switched tracing on in the shared parameter tree (library)
When a traced simulation ran a formula,
Simulation._run_formulasettrace,tracerandbranch_nameontax_benefit_system.parameters(the "soft-recast") and never reset them. The consequences:TracingParameterNodeAtInstantholding the tracer and branch name of that moment. Every later simulation on the system, traced or not, read through it. A second traced simulation's reads went to the first simulation's tracer, and a branch's reads were filed under another branch's name.ParameterNode.clonecopiestraceandtracer, so clones traced too.Simulation._calculatereadsparameters(period)(the abolition check) before any formula runs, which caches an untraced node at that instant. So on master a tracedincome_taxfor 2017-01 records no parameter reads at all. The soft-recast only reached instants first read after it.policyengine-us's
SharedParameterPolicy(spm.py) lends one parameter tree to several systems and documents that writing to it "must not happen at all"; the soft-recast was such a write.Fix: a traced simulation's formulas now get a
TracingParameterNode, a per-call view of the tree. Calling it at an instant returns aTracingParameterNodeAtInstantwith this simulation's tracer and branch name. Other attributes are read from the wrapped node, which is never modified. Untraced simulations are unchanged: they still gettax_benefit_system.parametersitself.Behaviour change: traces now record the parameters a formula reads through its
parametersargument, at every instant. Reads made elsewhere aren't recorded: the abolition check, and code that reachessimulation.tax_benefit_system.parametersdirectly. On master those were recorded only at instants first read after the soft-recast. policyengine.py'sderivereads onlynode.children, so it is unaffected.3.
TracingParameterNodeAtInstantcould not be copied (library)copy.copy,copy.deepcopyandpickleall recursed. Each creates the instance with__new__and then probes it (__setstate__,__deepcopy__, ...), and__getattr__readself.parameter_node_at_instant, which isn't set yet, so it called itself without end.Reform.modify_parametersdeep-copies the baseline's parameter tree, so once a traced node sat in the root's at-instant cache, any reform that calledmodify_parameterson that system crashed. Fix: both wrappers raiseAttributeErrorfor special (__dunder__) names and for any lookup on an instance that hasn't been filled in. Copy, deepcopy and pickle now round-trip.Options considered
tax_benefit_systemtest_cycles,test_formulas,test_reforms,test_opt_out_cacheandtest_calculate_output) made function-scoped, and it fails Hypothesis'function_scoped_fixturehealth check intest_branch_shared_arrays_differential.py. The plugin bump fixes the leak for every shared fixture at every scope; function scope would cover this one fixture only.TracingParameterNoderather than restoring flags afterwards. Restoring would still leave traced nodes in the at-instant caches, and it races when threads share a system.TracingParameterNodeAtInstantcopy-safeReproduction
test_branch_shared_arrays.pyand ran it withtest_parameters.pyandtest_reforms.pyunder--reruns 2with 14.0. All four modules got the same system object.test_get_at_instantfailed (TracingParameterNodeAtInstant) andtest_modify_parametersraisedRecursionError, which is the CI failure.tests/core/test_rerun_fixture_isolation.pyruns a two-module suite in a subprocess with the installed plugin, once with a test that passes on its rerun and once with one that fails every attempt. With 14.0 both cases fail (assert 'test_a_rerun' == 'test_b_later'); with 15.0, 16.2 and 16.7 both pass.tests/core/test_tracing_parameter_isolation.py: 28 of its 30 tests fail against master's library code. The other two are non-regression checks.tests/core/test_tracing_parameter_isolation_properties.py: against master, Hypothesis shrinks the failure to a single traced calculation that leaves the system traced.Invariants
These hold for every input and are stated and executed in the new tests:
trace/tracer/branch_name, only plain cached at-instant nodes, and clones and deep copies plain too.Tests
tests/core/test_tracing_parameter_isolation.py(30),tests/core/test_tracing_parameter_isolation_properties.py(Hypothesis, 40 examples),tests/core/test_rerun_fixture_isolation.py(2), and helpers intests/fixtures/tracing.py.coverage run ... --reruns 2 --reruns-delay 5): running locally; this PR's CI is the authoritative run.ruff format --check .andruff checkpass.Follow-up
Iterating a traced parameter node (
for name in node,"x" in node) raisesKeyError: 0, because the wrapper has__getitem__but no__iter__. It's out of scope here and filed as a separate task.axiom: n/a: engine tracing and test tooling, no policy change
🤖 Generated with Claude Code