Skip to content

A conftest can switch off a vacuity rule by rebinding the module-level name it reads #924

Description

@OffgridwithJD

test/pytest/pgc_vacuity.py keeps the values its guards read in module-level names, and a conftest.py can rebind them. Rebinding one turns the guard off for every test in that directory, silently, with no reason recorded and no refusal — which is the shape the layer's own false-positive budget section forbids: "each [hatch] costing more to type than the honest form".

Two lines of conftest.py is less to type than the honest form.

Measured on main at de8fca4b

The layer's own pgc_vacuity.py (md5 07e29f2bb84f), two arms differing in exactly one file:

$ diff -rq arm_plain arm_conftest
Only in arm_conftest: conftest.py

Both arms hold the same offending test — the order-collapse shape _order_killed_names exists to refuse:

def test_order_collapsed(expect):
    got = ["b", "a"]
    g = sorted(got)
    expect.ordered_rows(g, ["a", "b"], "rows in order")

arm_conftest's extra file is:

import pgc_vacuity
pgc_vacuity._ORDER_KILLERS = ()

Results:

---- arm_plain
    VACUITY: 1 collected test(s) never reported an outcome, so the run lost them
    silently: test_collapse.py::test_order_collapsed
    no tests ran in 0.01s          <- the guard fires

---- arm_conftest
    .                                                      [100%]
    1 passed in 0.02s              <- the guard is off

The premise is asserted rather than assumed: grep -c '^_ORDER_KILLERS' pgc_vacuity.py is 1 in the tree under test, and the two arms are byte-identical apart from that conftest.

Why a conftest reaches it

The scan runs from pytest_collection_modifyitems. A conftest.py is imported before the test modules in its directory are collected, so a module-level rebind lands before the scan reads the name. The blast radius is the directory, not the file — the same radius as autouse-fixture-supplies-the-assertion-count.

Scope

main's layer keeps two module-level names that guards read:

name read by effect of rebinding to an empty value
_ORDER_KILLERS _order_killed_names, and the ast walk at the second site the order-collapse refusal stops firing (measured above)
_RECORDERS expect, pytest_runtest_call, the session checks not measured here; it is the recorder registry rather than a rule's data

This is a class, not an instance. Any rule whose data lives in a module-level name is switchable from a conftest, so the count matters less than the shape: the next guard added the same way inherits the hole.

Not a live defect today

No conftest.py in the corpus does this — the repository has one conftest.py, and it does not touch either name. So this is a hole in the layer's design rather than a failing test today, which is why it is an issue and not a pull request.

Suggested shape of a fix

Make a rule's data unreachable from the code under test, or make reaching it cost what a hatch costs:

  • keep the values in a closure or behind a function the rule calls, so there is no name to rebind;
  • or read them through an accessor that refuses a value it did not itself produce;
  • or, if a conftest-level override is genuinely wanted, give it the layer's existing hatch grammar — a reason from a closed list, recorded as a counted assertion, and named in the refusal — so that switching a rule off appears in the run's output rather than in nobody's.

The first is cheapest and matches what the layer already does for the recorder's internals.

Provenance

Found while reviewing a branch that adds a third such name, and verified against main rather than against the branch, so it is a property of the shipped layer. The branch that surfaced it states the same hole in its own residual; this issue is the part that is not the branch's to fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions