test(dtypes): cover the protection and boolean paths, and pin three crashes - #500
Merged
Merged
Conversation
…rashes
Two of four mutation lanes on steps/dtypes.py. 22 tests.
Every candidate mutant was first confirmed against the FULL 7370-test suite
rather than the targeted subset, because a mutant that survives a subset may
still be covered elsewhere. That filter removed 6 of 18 candidates -- a third
of what a subset-only run would have reported as gaps. Only full-suite
survivors got tests.
The real gaps were concentrated in refine_numeric_after_semantic, which was
reachable from no existing test at all: only cleaner.py calls it, and only
after an applied numeric semantic repair. Two mutants live there, including
one where the preserve_leading_zeros=False opt-out silently stops working.
The rest cover the boolean vocabulary subset test, the NA-fill on match
masks, the literal (non-regex) decimal replacement, and the dateish screen
answering "no" when it has nothing to inspect.
Three mutants are proven equivalent by execution rather than argument: the
int64 fit guard is unreachable because the integrality check rejects every
non-finite value first (line trace over 22 adversarial series, zero hits on
the guard), and two mask-normalisation comparisons are no-ops on pandas 2.3.3
(exhaustive enumeration of every mask pattern over {True, False, NA} for
lengths 0-5 against 6 target series, 2562 configurations, zero divergences).
Both are scoped to this pandas version and say so.
Also pins three crashes found while building those adversarial inputs, all
reachable from fd.clean on default settings and none of them mutants:
duplicate index labels break the formatted-number rescue, and a complex value
beside text crashes the numeric finalizer. Pinned as characterization tests
so a fix has to come past them; neither is fixed here.
The complex-crash test asserts the finalizer directly, not through fd.clean:
the end-to-end route is order-dependent and an fd.clean-level pytest.raises
was genuinely flaky under randomised ordering. A companion test records that
order dependence and asserts only what holds either way -- the frame comes
back unchanged, or it raises, never a silently coerced column.
Tests only; no src change.
Contributor
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…nd 2.x CI's py3.9 lane (pandas 1.5.3) failed on three assertions, none of which tested freshdata: two illustrate what pandas would do without the dateish screen, and pandas 1.x answers differently -- a bare int coerces to NaT rather than Timestamp(42), and format="mixed" did not exist before 2.0. Both are now gated on PANDAS_MAJOR >= 2, keeping their documentation value on modern pandas without asserting third-party behaviour that varies. The third was a third spelling of the complex-modulo crash: pandas 1.x raises "can't mod complex numbers" where 2.x raises the numpy ufunc error or Python's unsupported-operand error. The assertion now accepts any of the three, because the point is that it raises at all, not how it phrases it. Verified on both lanes locally this time: py3.9/pandas 1.5.3 and py3.12/pandas 2.3.3, 22 passed on each.
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.
What
Two of four mutation lanes on
steps/dtypes.py— the module that decides when a column's type is silently rewritten. 22 tests.The filter that matters
Every candidate mutant was confirmed against the full 7370-test suite before a test was written, not just the 130-test targeted subset. That removed 6 of 18 candidates — a third of what a subset-only run would have reported as gaps.
This corrects how earlier numbers in this series were framed: a surviving mutant is a candidate, not a finding, until it survives the whole suite. The headline "
dtypes.pyis at 26.5%" was subset-relative and overstated the gap.Where the real gaps were
refine_numeric_after_semanticwas reachable from no existing test at all — onlycleaner.pycalls it, and only after an applied numeric semantic repair. Two gaps live there:StringDtypecolumn is never an object dtype, so flippingortoandmakes the function skip every such column;ormakes the retry convert nothing, ever, so thepreserve_leading_zeros=Falseopt-out silently stops working.The rest: the boolean-vocabulary subset test (
<=→<stops a column whose values are exactly the 8-word vocabulary from being detected), the NA-fill on match masks, the literal (non-regex) decimal replacement — where a regex-metacharacter decimal separator turns"1|5"into".1.|.5."— and the dateish screen answering no when it has nothing to inspect (a yes makesto_datetimeturn a bare42into1970-01-01T00:00:00.000000042).Three equivalent mutants, proven by execution
_finalize_numericconsults_fits_int64only after(nonnull % 1 == 0).all()holds, and that is false for every non-finite value (inf % 1isnan).sys.settraceover 22 adversarial series records zero hits on the guard's return line while recording many on the function; the test assertsreached >= 10so the proof cannot go vacuous. A counterexample was hunted first — maskedFloat64would break it ifinf % 1producedpd.NA— and pandas keepsNaN.{True, False, NA}for lengths 0–5 against 6 target series = 2,562 configurations, 1,806 carrying NA, zero divergences, plus a dual-evaluation census over 81,439 executions.Both are scoped to pandas 2.3.3 / py3.12 and say so in their docstrings. On pandas < 2.0 a nullable mask containing NA raised on indexing, so the normalisation these mutants skip is what keeps the code version-proof.
Three crashes pinned (not fixed)
Found while building adversarial inputs — none is a mutant. All reproduce from
fd.cleanon default settings; all verified independently.1. Duplicate index labels break the formatted-number rescue (
dtypes.py:213,.loc[labels]expands each repeated label to every matching row). A duplicate index alone is harmless; it needs a duplicate index plus a formatted-number rescue plus ~20+ rows. Both natural routes fail at realistic scale:2. A complex value beside text crashes the numeric finalizer (
nonnull % 1 == 0). All-complex is fine —infer_dtypereports"complex"and the column is left alone; one complex beside text makes it"mixed"and it reaches the finalizer.3. Outside this module, the same duplicate-index frame with a coerced casualty crashes
engine/missing.py:110. Not pinned here (different module), but it means the index-uniqueness assumption wants auditing across the engine rather than patching twice.One test deliberately does not go through
fd.cleanThe complex-crash test asserts
_finalize_numericdirectly. The end-to-end route is order-dependent: the samefd.cleancall raises in a fresh process but not after certain other work in the same process, so anfd.clean-levelpytest.raiseswas genuinely flaky underpytest-randomly(seeds 5 and 6 failed; 1–4 and 7–8 passed).Hypotheses tested and rejected: unrestored monkeypatch or tracer (both restored, verified); global RNG (same input raised on 40/40 seeds); the
_finalize_numericcalls themselves; importing the test module. The trigger is constructing the adversarial pool, whose helper is pure.infer_dtypeis stable ("mixed"throughout) and_finalize_numericis stable (always raises), so the divergence is upstream inclean's routing. Mechanism not identified — likely a pandas/numpy caching detail, but that is a conjecture and is labelled as one.A companion test records the order dependence and asserts only what holds in either state: the frame comes back unchanged, or it raises — never a silently coerced column that has lost the complex value. Verified across 12 orderings including both formerly-failing seeds.
Scope
Tests only — no
src/change. No behaviour change, no changelog entry, no compatibility impact.Verification
ruff check .clean