From ab9614a70aa9c453e2a0887e2a351b682cbe5449 Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Wed, 25 Mar 2026 19:29:33 +0100 Subject: [PATCH 1/4] #1652 wildcard replacement with domain on grouping_variables --- cdisc_rules_engine/models/actions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cdisc_rules_engine/models/actions.py b/cdisc_rules_engine/models/actions.py index 2150d94c6..95c40211b 100644 --- a/cdisc_rules_engine/models/actions.py +++ b/cdisc_rules_engine/models/actions.py @@ -191,6 +191,10 @@ def generate_targeted_error_object( # noqa: C901 targets, ) + grouping_variables = [ + x.replace("--", self.dataset_metadata.domain_cleaned or "") + for x in grouping_variables + ] missing_grouping_vars = [ var for var in grouping_variables if var not in data.columns ] From f8a0a17e21ee4dab36a6dedc7e8c4fb2799a9acd Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Thu, 26 Mar 2026 11:56:24 +0100 Subject: [PATCH 2/4] #1652 renamed wildcard property to comply with new metadata structure --- cdisc_rules_engine/models/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdisc_rules_engine/models/actions.py b/cdisc_rules_engine/models/actions.py index 15897956b..63682ee4c 100644 --- a/cdisc_rules_engine/models/actions.py +++ b/cdisc_rules_engine/models/actions.py @@ -192,7 +192,7 @@ def generate_targeted_error_object( # noqa: C901 ) grouping_variables = [ - x.replace("--", self.dataset_metadata.domain_cleaned or "") + x.replace("--", self.dataset_metadata.wildcard_replacement or "") for x in grouping_variables ] missing_grouping_vars = [ From c72f285d893eb6ebd30f0567ba6a14b10a515fea Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Fri, 27 Mar 2026 17:39:42 +0100 Subject: [PATCH 3/4] extended test to check for wildcard replacement --- tests/unit/test_actions.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index 81c81ce28..fcd2f0972 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -105,11 +105,22 @@ def test_targeted_error_object_with_dataset_sensitivity(): assert error["value"] == {"TEST": 1, "MISSING": "Not in dataset"} -def test_targeted_error_object_with_group_sensitivity(): +@pytest.mark.parametrize( + "grouping_variables, wildcard_replacement, expected_column", + [ + # Original case: explicit column name, no wildcard + (["SETCD"], None, "SETCD"), + # Wildcard case: "--PARMCD" replaced with domain "TX" → "TXPARMCD" + (["--PARMCD", "SETCD"], "TX", "SETCD"), + ], +) +def test_targeted_error_object_with_group_sensitivity( + grouping_variables, wildcard_replacement, expected_column +): dummy_rule = { "core_id": "FB4607", "sensitivity": "Group", - "grouping_variables": ["SETCD"], + "grouping_variables": grouping_variables, "conditions": { "all": [ { @@ -171,18 +182,16 @@ def test_targeted_error_object_with_group_sensitivity(): # Should have exactly 3 errors (one per SETCD group) assert len(result.errors) == 3 - # Extract SETCD values from errors - setcd_values = [ - error.to_representation()["value"]["SETCD"] for error in result.errors + group_values = [ + error.to_representation()["value"][expected_column] for error in result.errors ] - assert sorted(setcd_values) == ["SET1", "SET2", "SET3"] + assert sorted(group_values) == ["SET1", "SET2", "SET3"] # Each error should have row information from the first record in its group for error in result.errors: error_repr = error.to_representation() assert "row" in error_repr - assert "SETCD" in error_repr["value"] - assert "TXPARMCD" in error_repr["value"] + assert expected_column in error_repr["value"] def test_group_sensitivity_missing_grouping_variables(): From 35051cb5edf96496d6376e5045efff5635e91464 Mon Sep 17 00:00:00 2001 From: alexfurmenkov Date: Thu, 2 Apr 2026 16:11:09 +0200 Subject: [PATCH 4/4] #1652 updated grouping test to cover one more cases --- tests/unit/test_actions.py | 187 +++++++++++++++++++++++++++++-------- 1 file changed, 147 insertions(+), 40 deletions(-) diff --git a/tests/unit/test_actions.py b/tests/unit/test_actions.py index fcd2f0972..f025849f7 100644 --- a/tests/unit/test_actions.py +++ b/tests/unit/test_actions.py @@ -106,16 +106,153 @@ def test_targeted_error_object_with_dataset_sensitivity(): @pytest.mark.parametrize( - "grouping_variables, wildcard_replacement, expected_column", + "grouping_variables, expected_column, df, expected_groups", [ - # Original case: explicit column name, no wildcard - (["SETCD"], None, "SETCD"), - # Wildcard case: "--PARMCD" replaced with domain "TX" → "TXPARMCD" - (["--PARMCD", "SETCD"], "TX", "SETCD"), + # Case 1: explicit column name, no wildcard + ( + ["SETCD"], + "SETCD", + pd.DataFrame( + { + "TXPARMCD": [ + "PLANSUB", + "PLANSUB", + "PLANSUB", + "OTHER", + "OTHER", + "OTHER", + "OTHER", + "VALUE", + "VALUE", + ], + "SETCD": [ + "SET1", + "SET1", + "SET1", + "SET2", + "SET2", + "SET2", + "SET2", + "SET3", + "SET3", + ], + "USUBJID": [ + "001", + "001", + "001", + "002", + "002", + "002", + "002", + "003", + "003", + ], + } + ), + ["SET1", "SET2", "SET3"], + ), + # Case 2: wildcard "--PARMCD" → "TXPARMCD", grouped only on TXPARMCD + # Each distinct TXPARMCD value should produce one value + ( + ["--PARMCD"], + "TXPARMCD", + pd.DataFrame( + { + "TXPARMCD": [ + "PLANSUB", + "PLANSUB", + "PLANSUB", + "OTHER", + "OTHER", + "OTHER", + "OTHER", + "VALUE", + "VALUE", + ], + "SETCD": [ + "SET1", + "SET1", + "SET1", + "SET2", + "SET2", + "SET2", + "SET2", + "SET3", + "SET3", + ], + "USUBJID": [ + "001", + "001", + "001", + "002", + "002", + "002", + "002", + "003", + "003", + ], + } + ), + ["PLANSUB", "OTHER", "VALUE"], + ), + # Case 3: grouped on both "--PARMCD" (→ "TXPARMCD") and "SETCD" + # TXPARMCD varies within SETCD groups to prove both columns contribute to grouping: + # SET1 has PLANSUB + OTHER → 2 groups + # SET2 has OTHER only → 1 group + # SET3 has VALUE + PLANSUB → 2 groups + # Total: 5 distinct (TXPARMCD, SETCD) combinations + ( + ["--PARMCD", "SETCD"], + "SETCD", + pd.DataFrame( + { + "TXPARMCD": [ + "PLANSUB", + "PLANSUB", + "OTHER", + "OTHER", + "OTHER", + "OTHER", + "OTHER", + "VALUE", + "VALUE", + "PLANSUB", + "PLANSUB", + ], + "SETCD": [ + "SET1", + "SET1", + "SET1", + "SET1", + "SET2", + "SET2", + "SET2", + "SET3", + "SET3", + "SET3", + "SET3", + ], + "USUBJID": [ + "001", + "001", + "001", + "001", + "002", + "002", + "002", + "003", + "003", + "003", + "003", + ], + } + ), + ["SET1", "SET1", "SET2", "SET3", "SET3"], # two SET1s and two SET3s + ), ], ) def test_targeted_error_object_with_group_sensitivity( - grouping_variables, wildcard_replacement, expected_column + grouping_variables, expected_column, df, expected_groups ): dummy_rule = { "core_id": "FB4607", @@ -139,35 +276,6 @@ def test_targeted_error_object_with_group_sensitivity( "output_variables": ["SETCD", "TXPARMCD"], } - # Create test data with 3 groups (SETCD), multiple records each - df = pd.DataFrame.from_dict( - { - "TXPARMCD": [ - "PLANSUB", - "PLANSUB", - "PLANSUB", - "OTHER", - "OTHER", - "OTHER", - "OTHER", - "VALUE", - "VALUE", - ], - "SETCD": [ - "SET1", - "SET1", - "SET1", - "SET2", - "SET2", - "SET2", - "SET2", - "SET3", - "SET3", - ], - "USUBJID": ["001", "001", "001", "002", "002", "002", "002", "003", "003"], - } - ) - variable = DatasetVariable(PandasDataset(df)) dataset_metadata = SDTMDatasetMetadata( first_record={"DOMAIN": "TX"}, filename="tx.xpt" @@ -179,13 +287,12 @@ def test_targeted_error_object_with_group_sensitivity( targets, df, "Missing required TXPARMCD value per set" ) - # Should have exactly 3 errors (one per SETCD group) - assert len(result.errors) == 3 + assert len(result.errors) == len(expected_groups) - group_values = [ + actual_values = sorted( error.to_representation()["value"][expected_column] for error in result.errors - ] - assert sorted(group_values) == ["SET1", "SET2", "SET3"] + ) + assert actual_values == sorted(expected_groups) # Each error should have row information from the first record in its group for error in result.errors: