Skip to content

--quality-delta gates a clean tree on Python: 58 dead-code false positives for Django test hooks dispatched via cls./self. #228

Description

@hnipps

What is wrong

On a Django monolith (~7,100 tracked files, Python), ripwire . --quality-delta on a clean working tree (identical to git HEAD, only untracked directories present) reports 58 gating regressions, all kind="dead-code", all preexisting-worse, exit 2:

<quality-delta baseline="git-HEAD" regressions="58" minor="0" acked="0" stale="0"
  preexisting-worse="58" new-symbol="0" gating="58" register-macro-excluded="0"
  api-new-surface="0" at="a2c192068+dirty+shallow">
<r kind="dead-code" sym="api_v2/tests/test_boundary_meter_status_logs.py::BoundaryMeterStatusLogTests::create_test_objects" p="api_v2/tests/test_boundary_meter_status_logs.py:82" gating="1" .../>
<r kind="dead-code" sym="api_v2/tests/test_boundary_meter_status_logs.py::BoundaryMeterStatusLogTests::detail_url_kwargs" p="api_v2/tests/test_boundary_meter_status_logs.py:99" gating="1" .../>
...

Two problems stacked:

  1. A no-op diff should never gate. Working tree == HEAD, yet the baseline snapshot and the working-tree snapshot disagree on whether these symbols are dead. Whatever differs between the two parses (ordering, resolver tie-breaks, shallow-clone history) makes the exit code unreliable as a pre-commit hook on this repo. --quality-delta=HEAD~1..HEAD on a one-file test commit also reports 28 preexisting-worse dead-code rows in unrelated files.

  2. The symbols are not dead. They are Django test-class hooks dispatched dynamically. A base class in api_v2/tests/utils.py does, inside setUpTestData:

    cls.create_test_objects()

    and ~30 test classes each define their own create_test_objects. Likewise detail_url_kwargs is called as self.detail_url_kwargs(...) from base helpers and overridden per subclass. The name-based resolver sees one call site with ~30 same-language definitions in other files, declines the bind, and every override reads as unreachable. This is the Python equivalent of the C++ self-registering test macro case that register_macros already exempts, but there is no Python-side hook for it.

The --callers=create_test_objects verb reflects the same blind spot (counts_floor="1", callers found only via super().create_test_objects() chains).

Environment

  • ripwire 0.6.0 (Release, AppleClang 16.0.0.16000026, built_from=2d2f10e62), macOS arm64
  • Repo is a shallow clone (--doctor reports shallow="1"); private, so I cannot share it, but the shape is any Django project using TestCase subclasses with @classmethod hooks overridden per class.
  • --doctor: 8 checks, 7 passed (the one failure is an unrelated stale tracked PNG).

Reproduction shape

# tests/base.py
class BaseAPITest(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.create_test_objects()          # dynamic dispatch on cls
    @classmethod
    def create_test_objects(cls): ...

# tests/test_a.py .. tests/test_z.py  (many files)
class ATests(BaseAPITest):
    @classmethod
    def create_test_objects(cls): ...      # reported dead

Run ripwire . --quality-delta on a clean tree. Expected: gating="0". Observed: one gating dead-code row per override.

Workaround that works

ripwire . --quality-delta --quality-ack="Django test hooks dispatched via cls/super" --ack-only=dead-code

After that, gating="0" acked="58". But acking 58 findings to make a clean tree pass is a ratchet built on a false floor.

Suggestions

  • Treat a cls.NAME() / self.NAME() call site whose name has N same-language method definitions across subclasses as reaching all of them for dead-code purposes (receiver is provably an instance/class of the hierarchy). Or, cheaper: extend .ripwire_config with a Python analogue of register_macros naming method names to exempt from dead-code.
  • Whatever the resolver decides, --quality-delta against an identical tree should be deterministic and report zero. That part looks like a bug independent of the dispatch question.

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