diff --git a/CORE-Report-2026-07-14T16-23-17.csv b/CORE-Report-2026-07-14T16-23-17.csv new file mode 100644 index 000000000..3d2b9eeaf --- /dev/null +++ b/CORE-Report-2026-07-14T16-23-17.csv @@ -0,0 +1,3 @@ +Dataset,Record,Variable,Value +BG,,EXECUTION_ERROR,"Failed to execute rule operation. Operation: min_date, Target: SJSTDTC, Domain: SJ, Error: 'Timestamp' object has no attribute 'lower'" +CL,,EXECUTION_ERROR,"Failed to execute rule operation. Operation: min_date, Target: SJSTDTC, Domain: SJ, Error: 'Timestamp' object has no attribute 'lower'" diff --git a/cdisc_rules_engine/services/reporting/sdtm_report_data.py b/cdisc_rules_engine/services/reporting/sdtm_report_data.py index ced0b11d9..cfdcffdc7 100644 --- a/cdisc_rules_engine/services/reporting/sdtm_report_data.py +++ b/cdisc_rules_engine/services/reporting/sdtm_report_data.py @@ -67,6 +67,8 @@ def __init__( self._args.substandard if hasattr(self._args, "substandard") else None ) use_case = self._args.use_case if hasattr(self._args, "use_case") else None + self._issue_execution_status: dict[int, str] = {} + self._issue_csv_fallback_dataset: dict[int, str] = {} self.data_sheets = { "Conformance Details": self.get_conformance_details_data( define_version, @@ -340,17 +342,33 @@ def _generate_error_details( """ errors = [] for result in validation_result.results or []: - errors = ( - errors - + self._issue_details(validation_result, result) - + self._error_details(validation_result, result) - ) + issue_items = self._issue_details(validation_result, result) + error_items = self._error_details(validation_result, result) + for error_item in error_items: + self._issue_execution_status[id(error_item)] = ( + ExecutionStatus.EXECUTION_ERROR.value + ) + self._issue_csv_fallback_dataset[id(error_item)] = ( + result.get("dataset") or "" + ) + errors = errors + issue_items + error_items return errors def _get_csv_rows(self) -> tuple[list[str], list[list[str]]]: header = ["Dataset", "Record", "Variable", "Value"] rows = [] for issue in self.data_sheets.get("Issue Details", []): + if ( + self._issue_execution_status.get(id(issue)) + == ExecutionStatus.EXECUTION_ERROR.value + ): + dataset_val = issue.get( + "dataset" + ) or self._issue_csv_fallback_dataset.get(id(issue), "") + dataset = dataset_val.removesuffix(".csv") + csv_value = issue.get("values") or issue.get("message") or "" + rows.append([dataset, "", "EXECUTION_ERROR", csv_value]) + continue dataset = (issue.get("dataset") or "").removesuffix(".csv") record = str(issue.get("row", "")) variables = issue.get("variables") or [] @@ -358,6 +376,7 @@ def _get_csv_rows(self) -> tuple[list[str], list[list[str]]]: for variable, value in zip(variables, values): csv_value = "" if value in (None, "null") else value rows.append([dataset, record, variable, csv_value]) + return header, rows def get_rules_report_data(self) -> list[dict]: diff --git a/cdisc_rules_engine/services/reporting/usdm_report_data.py b/cdisc_rules_engine/services/reporting/usdm_report_data.py index f5a80d3ee..7deb35de4 100644 --- a/cdisc_rules_engine/services/reporting/usdm_report_data.py +++ b/cdisc_rules_engine/services/reporting/usdm_report_data.py @@ -41,6 +41,8 @@ def __init__( template, **kwargs, ) + self._issue_execution_status: dict[int, str] = {} + self._issue_csv_fallback_path: dict[int, str] = {} self.data_sheets = { "Conformance Details": self.get_conformance_details_data(), "Entity Details": self.get_entity_details_data(), @@ -238,23 +240,39 @@ def _generate_error_details( """ errors = [] for result in validation_result.results or []: - errors = ( - errors - + self._issue_details(validation_result, result) - + self._error_details(validation_result, result) - ) + issue_items = self._issue_details(validation_result, result) + error_items = self._error_details(validation_result, result) + for error_item in error_items: + self._issue_execution_status[id(error_item)] = ( + ExecutionStatus.EXECUTION_ERROR.value + ) + self._issue_csv_fallback_path[id(error_item)] = ( + result.get("entity") or result.get("dataset") or "" + ) + errors = errors + issue_items + error_items return errors def _get_csv_rows(self) -> tuple[list[str], list[list[str]]]: header = ["path", "attribute", "value"] rows = [] for issue in self.data_sheets.get("Issue Details", []): + if ( + self._issue_execution_status.get(id(issue)) + == ExecutionStatus.EXECUTION_ERROR.value + ): + path = issue.get("entity") or self._issue_csv_fallback_path.get( + id(issue), "" + ) + csv_value = issue.get("values") or issue.get("message") or "" + rows.append([path, "EXECUTION_ERROR", csv_value]) + continue path = issue.get("path") or "" attributes = issue.get("attributes") or [] values = issue.get("values") or [] for attribute, value in zip(attributes, values): csv_value = "" if value in (None, "null") else value rows.append([path, attribute, csv_value]) + return header, rows def get_rules_report_data(self) -> list[dict]: diff --git a/tests/unit/test_services/test_reporting/test_sdtm_report.py b/tests/unit/test_services/test_reporting/test_sdtm_report.py index 0a49cf042..88e37ff7d 100644 --- a/tests/unit/test_services/test_reporting/test_sdtm_report.py +++ b/tests/unit/test_services/test_reporting/test_sdtm_report.py @@ -238,3 +238,58 @@ def test_no_errors_when_none_value_in_one_of_the_records(mock_validation_results assert error == summary_data[i] details = report.get_detailed_data() assert len(details) == 3 + + +def test_get_csv_rows_execution_error(mock_validation_results): + mock_validation_results[1].results[0][ + "executionStatus" + ] = ExecutionStatus.EXECUTION_ERROR.value + mock_validation_results[1].results[0]["dataset"] = "TT.csv" + mock_validation_results[1].results[0]["message"] = "TTVARs are wrong" + mock_validation_results[1].results[0]["errors"] = [ + {"error": "Unexpected KeyError in rule execution"} + ] + report = SDTMReportData( + [], + ["test"], + mock_validation_results, + 10.1, + MagicMock(define_xml_path=None, max_errors_per_rule=(None, False)), + ) + _, rows = report.get_csv_rows() + error_rows = [r for r in rows if r[2] == "EXECUTION_ERROR"] + assert len(error_rows) == 1 + dataset, record, variable, value = error_rows[0] + assert dataset == "TT" + assert record == "" + assert value == "TTVARs are wrong - Unexpected KeyError in rule execution" + issue_rows = [r for r in rows if r[2] != "EXECUTION_ERROR"] + assert len(issue_rows) == 4 + + +def test_get_csv_rows_execution_error_detailed_message(mock_validation_results): + mock_validation_results[1].results[0][ + "executionStatus" + ] = ExecutionStatus.EXECUTION_ERROR.value + mock_validation_results[1].results[0]["dataset"] = "AE.csv" + mock_validation_results[1].results[0]["message"] = "rule execution error" + detailed_message = ( + "\n Error parsing JSONata Rule for Core Id: CORE-000998\n" + " AttributeError: 'Jsonata' object has no attribute 'lower'" + ) + mock_validation_results[1].results[0]["errors"] = [ + {"error": "Rule format error", "message": detailed_message} + ] + report = SDTMReportData( + [], + ["test"], + mock_validation_results, + 10.1, + MagicMock(define_xml_path=None, max_errors_per_rule=(None, False)), + ) + _, rows = report.get_csv_rows() + error_rows = [r for r in rows if r[2] == "EXECUTION_ERROR"] + assert len(error_rows) == 1 + dataset, record, variable, value = error_rows[0] + assert dataset == "AE" + assert value == detailed_message diff --git a/tests/unit/test_services/test_reporting/test_usdm_report.py b/tests/unit/test_services/test_reporting/test_usdm_report.py index 7a5cf4429..43ae926ab 100644 --- a/tests/unit/test_services/test_reporting/test_usdm_report.py +++ b/tests/unit/test_services/test_reporting/test_usdm_report.py @@ -237,3 +237,57 @@ def test_no_errors_when_none_value_in_one_of_the_records(mock_validation_results assert error == summary_data[i] details = report.get_detailed_data() assert len(details) == 3 + + +def test_get_csv_rows_execution_error(mock_validation_results): + mock_validation_results[1].results[0][ + "executionStatus" + ] = ExecutionStatus.EXECUTION_ERROR.value + mock_validation_results[1].results[0]["entity"] = "TT" + mock_validation_results[1].results[0]["message"] = "TTVARs are wrong" + mock_validation_results[1].results[0]["errors"] = [ + {"error": "Unexpected KeyError in rule execution"} + ] + report = USDMReportData( + [], + ["test"], + mock_validation_results, + 10.1, + MagicMock(define_xml_path=None, max_errors_per_rule=(None, False)), + ) + _, rows = report.get_csv_rows() + error_rows = [r for r in rows if r[1] == "EXECUTION_ERROR"] + assert len(error_rows) == 1 + path, attribute, value = error_rows[0] + assert path == "TT" + assert value == "TTVARs are wrong - Unexpected KeyError in rule execution" + issue_rows = [r for r in rows if r[1] != "EXECUTION_ERROR"] + assert len(issue_rows) == 4 + + +def test_get_csv_rows_execution_error_detailed_message(mock_validation_results): + mock_validation_results[1].results[0][ + "executionStatus" + ] = ExecutionStatus.EXECUTION_ERROR.value + mock_validation_results[1].results[0]["entity"] = "json" + mock_validation_results[1].results[0]["message"] = "rule execution error" + detailed_message = ( + "\n Error parsing JSONata Rule for Core Id: CORE-000998\n" + " AttributeError: 'Jsonata' object has no attribute 'lower'" + ) + mock_validation_results[1].results[0]["errors"] = [ + {"error": "Rule format error", "message": detailed_message} + ] + report = USDMReportData( + [], + ["test"], + mock_validation_results, + 10.1, + MagicMock(define_xml_path=None, max_errors_per_rule=(None, False)), + ) + _, rows = report.get_csv_rows() + error_rows = [r for r in rows if r[1] == "EXECUTION_ERROR"] + assert len(error_rows) == 1 + path, attribute, value = error_rows[0] + assert path == "json" + assert value == detailed_message