Skip to content

test(metamorphic): assert what cleaning must and must not change - #475

Merged
kevincostner17 merged 1 commit into
mainfrom
test/metamorphic
Sep 16, 2026
Merged

kevincostner17 merged 1 commit into
mainfrom
test/metamorphic

Conversation

@kevincostner17

Copy link
Copy Markdown
Contributor

Why

The suite had no invariance tests on the public API. Worse, tests/test_execution/test_action_parity.py::_normalize sorts rows and columns before comparing engines, so an order-sensitivity bug in fd.clean would pass that gate silently.

Invariances

Reordering rows (4 seeds) or columns, adding an unrelated column, renaming a column whose meaning is pinned by semantic_context, and cleaning twice must change no surviving cell.

Row order is checked against imputation specifically — impute reads the whole column and is the most plausible place for a positional accident to leak in. Both mean and missforest are covered.

Required changes

Without these, a cleaner that returned its input untouched would satisfy every invariance above and look perfect:

Property
valid amount → "apple" changes the result
valid country → "N/A" changes the result
outliers="flag" flags and leaves the value at 9999.0

The outlier case asserts both halves deliberately: silence would be a missed detection, a changed value would be an unrequested repair.

Honest framing

Every property already holds on main, so these are guards against regression rather than bug reports. The engine-parity case is the exception in spirit — it covers ground test_action_parity cannot, because it compares the frames as returned instead of sorting them first. Both polars and duckdb run it; neither is skipped.

Two things that looked like defects and were not

Recorded in the test docstrings so the next reader doesn't re-derive them:

  • assert_frame_equal failed with an empty message while every column, dtype, index and shape matched. fd.clean returns CleanResult, whose _constructor is pd.DataFrame by design (result.py:22), so .reindex() yields a plain frame and the comparison failed on frame type. That design is right — a derived or filtered frame should not carry a .report() that no longer describes it — so the helper normalises both sides instead.
  • A 2-vs-2 categorical variant tie ('USA' vs 'usa') is not normalised in either input order, so there is no order-dependent tie-break to guard against.

Verification

  • 20 tests, all passing; polars and duckdb parity cases confirmed running (not skipped).
  • Full suite py3.12: 6656 passed, 22 skipped, 0 failed, coverage 93.91%.
  • ruff check . clean.
  • No library code changed.

@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: b4e738a7-07e2-495b-839d-2c4ac67fe0d9


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The suite had no invariance tests on the public API. Worse,
tests/test_execution/test_action_parity.py::_normalize sorts rows *and* columns
before comparing engines, so an order-sensitivity bug in fd.clean would pass
that gate silently. These properties are asserted directly, on unsorted output.

Invariances -- reordering rows (4 seeds) or columns, adding an unrelated
column, renaming a column whose meaning is pinned by semantic_context, and
cleaning twice must change no surviving cell. Row order is also checked against
imputation specifically, since impute reads the whole column and is the most
plausible place for a positional accident to leak in; mean and missforest are
both covered.

Required changes -- replacing a valid amount with "apple", replacing a country
with "N/A", and flagging an outlier must each change the result. Without these
a cleaner that returned its input untouched would satisfy every invariance
above and look perfect. The outlier case asserts both halves: the value stays
9999.0 and the report still records the detection.

Every property already holds on main, so these are guards against regression
rather than bug reports. The engine-parity case is the exception in spirit: it
covers ground test_action_parity cannot, because it compares the frames as
returned instead of sorting them first. Both polars and duckdb run it; neither
is skipped in CI.

Two things that looked like defects and were not, recorded here so the next
reader does not re-derive them:

- assert_frame_equal failed with an EMPTY message while every column, dtype,
  index and shape matched. fd.clean returns CleanResult, whose _constructor is
  pd.DataFrame by design (result.py:22), so .reindex() yields a plain frame and
  the comparison failed on frame *type*. That design is right -- a derived or
  filtered frame should not carry a .report() that no longer describes it -- so
  the helper normalises both sides instead.
- A 2-vs-2 categorical variant tie ('USA' vs 'usa') is not normalised in either
  input order, so there is no order-dependent tie-break to guard against.

Full suite 6656 passed / 0 failed, coverage 93.91%; ruff clean.
@kevincostner17
kevincostner17 merged commit 815a9ec into main Sep 16, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant