|
8 | 8 | import pytest |
9 | 9 |
|
10 | 10 | import freshdata as fd |
| 11 | +from freshdata.context.types import ColumnConstraint, ContextPolicy |
11 | 12 | from freshdata.guard import ( |
12 | 13 | ProtectedColumnError, |
| 14 | + _series_identical, |
13 | 15 | hard_protected_columns, |
14 | 16 | protected_column_set, |
15 | 17 | snapshot_protected, |
@@ -181,3 +183,47 @@ def test_guard_report_metadata_names_protected_columns(): |
181 | 183 | ) |
182 | 184 | guard_actions = [a for a in report if a.step == "guard"] |
183 | 185 | assert guard_actions and guard_actions[0].metadata["protected_columns"] == ["rev"] |
| 186 | + |
| 187 | + |
| 188 | +# ── #449: duplicate index labels ──────────────────────────────────────────────── |
| 189 | + |
| 190 | + |
| 191 | +def _dup_frame() -> pd.DataFrame: |
| 192 | + return pd.DataFrame( |
| 193 | + {"a": pd.array([None, None, None, 0], dtype="Int64"), |
| 194 | + "b": pd.array([None] * 4, dtype="Int64")}, |
| 195 | + index=[0, 0, 0, 0], |
| 196 | + ) |
| 197 | + |
| 198 | + |
| 199 | +def _never_modify_policy(column: str) -> ContextPolicy: |
| 200 | + return ContextPolicy(constraints=(ColumnConstraint( |
| 201 | + id="c1", column=column, resolved_from=column, resolution_confidence=1.0, |
| 202 | + rule="protected", action="never_modify", enforcement="hard"),)) |
| 203 | + |
| 204 | + |
| 205 | +def test_protected_column_survives_a_duplicate_index(): |
| 206 | + # Regression (#449): the guard aligned the snapshot with before.loc[after.index], |
| 207 | + # which multiplies rows when labels repeat, so dropping any row made the |
| 208 | + # comparison impossible and raised on an unmodified column. |
| 209 | + df = _dup_frame() |
| 210 | + expected = fd.clean(df.copy(), verbose=False)["a"].tolist() |
| 211 | + out = fd.clean(df.copy(), policy=_never_modify_policy("a"), verbose=False) |
| 212 | + assert out["a"].tolist() == expected |
| 213 | + |
| 214 | + |
| 215 | +def test_guard_still_detects_a_modified_cell_on_a_duplicate_index(): |
| 216 | + before = pd.Series([1.0, 2.0, 3.0, 4.0], index=[0, 0, 0, 0]) |
| 217 | + assert _series_identical(before, pd.Series([1.0, 4.0], index=[0, 0])) is None |
| 218 | + for broken in ( |
| 219 | + pd.Series([1.0, 9.0], index=[0, 0]), # value rewritten |
| 220 | + pd.Series([4.0, 1.0], index=[0, 0]), # rows reordered |
| 221 | + pd.Series([1.0, 2.0, 3.0, 4.0, 5.0], index=[0] * 5), # row gained |
| 222 | + ): |
| 223 | + assert _series_identical(before, broken) is not None |
| 224 | + |
| 225 | + |
| 226 | +def test_guard_treats_two_missing_values_as_equal_on_a_duplicate_index(): |
| 227 | + before = pd.Series([1.0, None, 3.0], index=[0, 0, 0]) |
| 228 | + assert _series_identical(before, pd.Series([None, 3.0], index=[0, 0])) is None |
| 229 | + assert _series_identical(before, pd.Series([7.0, 3.0], index=[0, 0])) is not None |
0 commit comments