diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 69308d4eb..ed654c362 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -24,4 +24,3 @@ jobs: run: | python -m pytest tests/unit/ --cov=cdisc_rules_engine --cov-fail-under=75 python -m pytest tests/QARegressionTests/ - python -m pytest -m regression tests/QARegressionTests/test_Issues diff --git a/tests/QARegressionTests/globals.py b/tests/QARegressionTests/globals.py index 1ed0bc456..2512adb2a 100644 --- a/tests/QARegressionTests/globals.py +++ b/tests/QARegressionTests/globals.py @@ -1,10 +1,10 @@ -issue_datails_sheet = "Issue Details" -dataset_details_sheet = "Dataset Details" -entity_details_sheet = "Entity Details" -rules_report_sheet = "Rules Report" -issue_sheet_coreid_column = "A" -dataset_sheet_dataset_column = "A" -rules_sheet_rule_status_column = "F" -issue_sheet_record_column = "G" -issue_sheet_variable_column = "I" -issue_sheet_values_column = "J" +issue_datails_sheet = "Issue Details" +dataset_details_sheet = "Dataset Details" +entity_details_sheet = "Entity Details" +rules_report_sheet = "Rules Report" +issue_sheet_coreid_column = "A" +dataset_sheet_dataset_column = "A" +rules_sheet_rule_status_column = "F" +issue_sheet_record_column = "F" +issue_sheet_variable_column = "H" +issue_sheet_values_column = "I" diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1023.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1023.py index cb8118f64..2b9141fa2 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1023.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1023.py @@ -1,57 +1,56 @@ -import os -import subprocess -import json - -import pytest -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue1023: - - def test_dataset_utf8(self): - """Test that the engine correctly handles dataset files with utf8 encoding""" - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-r", - "CORE-000766", - "-e", - "utf8", - "-dp", - os.path.join("tests", "resources", "CoreIssue1023", "ae_utf8.json"), - "--output-format", - "json", - ] - subprocess.run(command, check=True) - - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - # Open the JSON report file - json_report = json.load(open(json_report_path, "r", encoding="utf8")) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - datasets = {x["filename"] for x in json_report["Dataset_Details"]} - assert datasets == {"AE"} - assert ( - json_report["Issue_Details"][0]["USUBJID"] - == f"utf8:{b'\xe1\xbc\x87'.decode('utf8')}" - ) - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess +import json + +from conftest import get_python_executable + + +class TestCoreIssue1023: + + def test_dataset_utf8(self): + """Test that the engine correctly handles dataset files with utf8 + encoding.""" + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3.4", + "-lr", + os.path.join("tests", "resources", "library_rules", "CORE-000766.json"), + "-e", + "utf8", + "-dp", + os.path.join("tests", "resources", "CoreIssue1023", "ae_utf8.json"), + "--output-format", + "json", + ] + subprocess.run(command, check=True) + + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + # Open the JSON report file + json_report = json.load(open(json_report_path, "r", encoding="utf8")) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + datasets = {x["filename"] for x in json_report["Dataset_Details"]} + assert datasets == {"AE"} + assert ( + json_report["Issue_Details"][0]["USUBJID"] + == f"utf8:{b'\xe1\xbc\x87'.decode('utf8')}" + ) + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1204.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1204.py index 24ec2349c..1673fbe5f 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1204.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1204.py @@ -1,40 +1,38 @@ -import subprocess -import pytest -from conftest import get_python_executable - - -@pytest.mark.regression -def test_non_existing_dataset_shows_helpful_error(): - """Test that the engine displays a helpful error message if dataset files are not found""" - command = [ - get_python_executable(), - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-dp", - "ds.json", - "-dp", - "ds2.json", - ] - - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - - stderr_text = stderr.decode() - stdout_text = stdout.decode() - - assert process.returncode == 2, "Expected non-zero exit code" - assert ( - "FileNotFoundError" not in stderr_text - ), "Should not show FileNotFoundError crash" - assert ( - "Failed to execute script" not in stderr_text - ), "Should not show script execution failure" - assert "Files ds.json, ds2.json are not found" in stderr_text, ( - f"Expected helpful error message about non-existing dataset files. " - f"stderr: {stderr_text}, stdout: {stdout_text}" - ) +import subprocess +from conftest import get_python_executable + + +def test_non_existing_dataset_shows_helpful_error(): + """Test that the engine displays a helpful error message if dataset files are not found""" + command = [ + get_python_executable(), + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3.4", + "-dp", + "ds.json", + "-dp", + "ds2.json", + ] + + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + stderr_text = stderr.decode() + stdout_text = stdout.decode() + + assert process.returncode == 2, "Expected non-zero exit code" + assert ( + "FileNotFoundError" not in stderr_text + ), "Should not show FileNotFoundError crash" + assert ( + "Failed to execute script" not in stderr_text + ), "Should not show script execution failure" + assert "Files ds.json, ds2.json are not found" in stderr_text, ( + f"Expected helpful error message about non-existing dataset files. " + f"stderr: {stderr_text}, stdout: {stdout_text}" + ) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1248.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1248.py index b5b60572d..355ad9faf 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1248.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1248.py @@ -1,186 +1,193 @@ -import os -import subprocess - -import pytest -import json -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue1248: - @pytest.mark.parametrize( - "command,rules_report,num_issues", - [ - # define path provided will lead to successful execution - ( - [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-2", - "-of", - "JSON", - "-lr", - os.path.join("tests", "resources", "CoreIssue1248", "sample.yml"), - "-cs", - "-dxp", - os.path.join( - "tests", - "resources", - "CoreIssue1248", - "define_subfolder", - "define.xml", - ), - "-ps", - "1", - "-dp", - os.path.join("tests", "resources", "CoreIssue1248", "data.xlsx"), - ], - [ - { - "core_id": "SD1129", - "version": "1", - "cdisc_rule_id": "", - "fda_rule_id": "", - "message": "TEST", - "status": "ISSUE REPORTED", - } - ], - 2, - ), - # JSON data file and no define.xml in same folder and no -dxp param will provide error - ( - [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-2", - "-of", - "JSON", - "-lr", - os.path.join("tests", "resources", "CoreIssue1248", "sample.yml"), - "-cs", - "-dp", - os.path.join("tests", "resources", "CoreIssue1248", "relrec.json"), - "-ps", - "1", - ], - [ - { - "core_id": "SD1129", - "version": "1", - "cdisc_rule_id": "", - "fda_rule_id": "", - "message": "TEST", - "status": "EXECUTION ERROR", - } - ], - 1, - ), - # no define.xml in same path as data.xlsx file will provide error - ( - [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-2", - "-of", - "JSON", - "-lr", - os.path.join("tests", "resources", "CoreIssue1248", "sample.yml"), - "-cs", - "-dp", - os.path.join("tests", "resources", "CoreIssue1248", "data.xlsx"), - "-ps", - "1", - ], - [ - { - "core_id": "SD1129", - "version": "1", - "cdisc_rule_id": "", - "fda_rule_id": "", - "message": "TEST", - "status": "EXECUTION ERROR", - } - ], - 1, - ), - # define.xml in same folder as the data.xls and no -dxp provided will provide error until - # in ExcelDataService dataset metadata creation full_path=dataset_name - ( - [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-2", - "-of", - "JSON", - "-lr", - os.path.join("tests", "resources", "CoreIssue1248", "sample.yml"), - "-cs", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue1248", - "data_and_define", - "data.xlsx", - ), - "-ps", - "1", - ], - [ - { - "core_id": "SD1129", - "version": "1", - "cdisc_rule_id": "", - "fda_rule_id": "", - "message": "TEST", - "status": "EXECUTION ERROR", - } - ], - 1, - ), - ], - ) - def test_define_path_used(self, command, rules_report, num_issues): - subprocess.run(command, check=True) - - # Get the latest created report file - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - # Open the JSON report file - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - assert len(json_report["Issue_Details"]) == num_issues - assert json_report["Rules_Report"] == rules_report - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess + +import pytest +import json +from conftest import get_python_executable + + +class TestCoreIssue1248: + @pytest.mark.parametrize( + "command,rules_report,num_issues", + [ + # Case 1: -dxp explicitly points to the Define-XML file, even though + # it lives in a subfolder unrelated to -dp -> validation succeeds. + ( + [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-2", + "-of", + "JSON", + "-lr", + os.path.join("tests", "resources", "CoreIssue1248", "sample.yml"), + "-cs", + "-dxp", + os.path.join( + "tests", + "resources", + "CoreIssue1248", + "define_subfolder", + "define.xml", + ), + "-ps", + "1", + "-dp", + os.path.join("tests", "resources", "CoreIssue1248", "data.xlsx"), + ], + [ + { + "core_id": "SD1129", + "version": "1", + "cdisc_rule_id": "", + "fda_rule_id": "", + "message": "TEST", + "status": "ISSUE REPORTED", + } + ], + 2, + ), + # Case 2: JSON data with no adjacent define.xml and no -dxp given -> + # engine cannot locate a Define-XML and reports an execution error. + ( + [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-2", + "-of", + "JSON", + "-lr", + os.path.join("tests", "resources", "CoreIssue1248", "sample.yml"), + "-cs", + "-dp", + os.path.join("tests", "resources", "CoreIssue1248", "relrec.json"), + "-ps", + "1", + ], + [ + { + "core_id": "SD1129", + "version": "1", + "cdisc_rule_id": "", + "fda_rule_id": "", + "message": "TEST", + "status": "EXECUTION ERROR", + } + ], + 1, + ), + # Case 3: xlsx data with no define.xml alongside it and no -dxp + # given -> same as case 2, execution error for a different format. + ( + [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-2", + "-of", + "JSON", + "-lr", + os.path.join("tests", "resources", "CoreIssue1248", "sample.yml"), + "-cs", + "-dp", + os.path.join("tests", "resources", "CoreIssue1248", "data.xlsx"), + "-ps", + "1", + ], + [ + { + "core_id": "SD1129", + "version": "1", + "cdisc_rule_id": "", + "fda_rule_id": "", + "message": "TEST", + "status": "EXECUTION ERROR", + } + ], + 1, + ), + # Case 4: define.xml sits in the same folder as data.xlsx but -dxp + # is still not given -> still an execution error (known limitation: + # ExcelDataService dataset metadata creation uses + # full_path=dataset_name, so it does not auto-discover a sibling + # define.xml the way -dp with a JSON/xlsx path might elsewhere). + ( + [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-2", + "-of", + "JSON", + "-lr", + os.path.join("tests", "resources", "CoreIssue1248", "sample.yml"), + "-cs", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue1248", + "data_and_define", + "data.xlsx", + ), + "-ps", + "1", + ], + [ + { + "core_id": "SD1129", + "version": "1", + "cdisc_rule_id": "", + "fda_rule_id": "", + "message": "TEST", + "status": "EXECUTION ERROR", + } + ], + 1, + ), + ], + ) + def test_define_path_used(self, command, rules_report, num_issues): + """Verify how the engine resolves the Define-XML path (-dxp) relative + to the data path (-dp) across the four scenarios documented above.""" + subprocess.run(command, check=True) + + # Get the latest created report file + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + # Open the JSON report file + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + assert len(json_report["Issue_Details"]) == num_issues + assert json_report["Rules_Report"] == rules_report + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1316.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1316.py index 44fdb8a87..bbaa14840 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1316.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1316.py @@ -1,58 +1,59 @@ -import os -import subprocess -import unittest - -import pytest -import json -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue1316(unittest.TestCase): - def test_new_rule(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-4", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue1316", - "test_dataset.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue1316", "rule_new.yml"), - "-ps", - "1", - "-of", - "json", - ] - subprocess.run(command, check=True) - - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - assert json_report["Issue_Summary"][0]["issues"] == 591 - assert json_report["Rules_Report"][0]["status"] == "ISSUE REPORTED" - - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess +import unittest + +import json +from conftest import get_python_executable + + +class TestCoreIssue1316(unittest.TestCase): + def test_new_rule(self): + """Verify a new local rule (checking EXROUTE equals a fixed value and + that library_variable_ccode is non-empty) runs against test_dataset.json + and reports exactly 591 issues with an ISSUE REPORTED status.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-4", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue1316", + "test_dataset.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue1316", "rule_new.yml"), + "-ps", + "1", + "-of", + "json", + ] + subprocess.run(command, check=True) + + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + assert json_report["Issue_Summary"][0]["issues"] == 591 + assert json_report["Rules_Report"][0]["status"] == "ISSUE REPORTED" + + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1326.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1326.py index dfef7a49c..6f4e333f8 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1326.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1326.py @@ -1,169 +1,173 @@ -import os -import subprocess -import unittest -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - entity_details_sheet, - issue_datails_sheet, - rules_report_sheet, - issue_sheet_record_column, - issue_sheet_variable_column, - issue_sheet_values_column, -) - - -@pytest.mark.regression -class TestPrefTerm(unittest.TestCase): - def test_positive_dataset(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "3-0", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue1326", - "regression-test-coreid-DDF00015-positive.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue1326", "rule.yml"), - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # Go to the "Issue Details" sheet - sheet = workbook[issue_datails_sheet] - - record_column = sheet[issue_sheet_record_column] - variables_column = sheet[issue_sheet_variable_column] - values_column = sheet[issue_sheet_values_column] - - record_values = [cell.value for cell in record_column[1:]] - variables_values = [cell.value for cell in variables_column[1:]] - values_column_values = [cell.value for cell in values_column[1:]] - - # Remove None values using list comprehension - record_values = [value for value in record_values if value is not None] - variables_values = [value for value in variables_values if value is not None] - values_column_values = [ - value for value in values_column_values if value is not None - ] - rules_values = [ - row for row in workbook[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - # Perform the assertion - # Ensure only two negative values are caught - assert rules_values[0][0] == "CORE-000409" - assert rules_values[0][-1] == "SUCCESS" - assert len(record_values) == 0 - assert len(variables_values) == 0 - assert len(values_column_values) == 0 - if os.path.exists(excel_file_path): - os.remove(excel_file_path) - - def test_negative_dataset(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "3-0", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue1326", - "regression-test-coreid-DDF00015-negative.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue1326", "rule.yml"), - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # --- Entity Details --- - dataset_sheet = workbook[entity_details_sheet] - dataset_values = [row for row in dataset_sheet.iter_rows(values_only=True)][1:] - dataset_values = [row for row in dataset_values if any(row)] - assert len(dataset_values) > 0 - assert dataset_values[0][0] == "StudyVersion" - assert dataset_values[0][-1] == 7 - - # --- Issue Summary --- - issue_summary_sheet = workbook["Issue Summary"] - summary_values = [ - row for row in issue_summary_sheet.iter_rows(values_only=True) - ][1:] - summary_values = [row for row in summary_values if any(row)] - assert len(summary_values) > 0 - assert summary_values[0][1] == "CORE-000409" - assert summary_values[0][4] == 7 - - # --- Issue Details --- - expected_values = [ - "C199989, C199989, Phase II Trial, PHASE II TRIAL, C15601, Phase Ib Trial", - "null, null, Not Applicable, NOT APPLICABLE, C48660, N/A", - "C198366, C198366, null, null, C198366xx, Phase I/II/III Trial", - "C15600, C15600, null, null, C00001x, Phase I Trial", - "C198366, C198366, Phase II Trial, PHASE II TRIAL, C15601, Phase I/II/III Trial", - "C199989, C199989, null, null, C198366xx, PHASE IB TRIAL", - "null, null, Phase Ib Trial, PHASE IB TRIAL, C199989, PHASE 2 TRIAL", - ] - issue_details_sheet = workbook[issue_datails_sheet] - details_values = [ - row for row in issue_details_sheet.iter_rows(values_only=True) - ][1:] - details_values = [row for row in details_values if any(row)] - assert all(row[0] == "CORE-000409" for row in details_values) - actual_values = [row[-1] for row in details_values] - assert actual_values == expected_values - - # --- Rules Report --- - rules_values = [ - row for row in workbook[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - assert len(rules_values) > 0 - assert rules_values[0][0] == "CORE-000409" - assert rules_values[0][-1] == "ISSUE REPORTED" - - if os.path.exists(excel_file_path): - os.remove(excel_file_path) - - -# if __name__ == "__main__": -# unittest.main() +import os +import subprocess +import unittest +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + entity_details_sheet, + issue_datails_sheet, + rules_report_sheet, + issue_sheet_record_column, + issue_sheet_variable_column, + issue_sheet_values_column, +) + + +class TestPrefTerm(unittest.TestCase): + def test_positive_dataset(self): + """Verify the USDM study-phase codelist consistency rule (DDF00015) + reports zero issues (SUCCESS) against a StudyVersion dataset where + the study phase code/decode values are consistent with the codelist.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "3-0", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue1326", + "regression-test-coreid-DDF00015-positive.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue1326", "rule.yml"), + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # Go to the "Issue Details" sheet + sheet = workbook[issue_datails_sheet] + + record_column = sheet[issue_sheet_record_column] + variables_column = sheet[issue_sheet_variable_column] + values_column = sheet[issue_sheet_values_column] + + record_values = [cell.value for cell in record_column[1:]] + variables_values = [cell.value for cell in variables_column[1:]] + values_column_values = [cell.value for cell in values_column[1:]] + + # Remove None values using list comprehension + record_values = [value for value in record_values if value is not None] + variables_values = [value for value in variables_values if value is not None] + values_column_values = [ + value for value in values_column_values if value is not None + ] + rules_values = [ + row for row in workbook[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + # Perform the assertion + # Ensure only two negative values are caught + assert rules_values[0][0] == "CORE-000409" + assert rules_values[0][-1] == "SUCCESS" + assert len(record_values) == 0 + assert len(variables_values) == 0 + assert len(values_column_values) == 0 + if os.path.exists(excel_file_path): + os.remove(excel_file_path) + + def test_negative_dataset(self): + """Verify the same DDF00015 rule flags exactly 7 rows where the study + phase code/decode pairs are mismatched or inconsistent with the + Trial Phase Response codelist (C66737).""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "3-0", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue1326", + "regression-test-coreid-DDF00015-negative.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue1326", "rule.yml"), + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # --- Entity Details --- + dataset_sheet = workbook[entity_details_sheet] + dataset_values = [row for row in dataset_sheet.iter_rows(values_only=True)][1:] + dataset_values = [row for row in dataset_values if any(row)] + assert len(dataset_values) > 0 + assert dataset_values[0][0] == "StudyVersion" + assert dataset_values[0][-1] == 7 + + # --- Issue Summary --- + issue_summary_sheet = workbook["Issue Summary"] + summary_values = [ + row for row in issue_summary_sheet.iter_rows(values_only=True) + ][1:] + summary_values = [row for row in summary_values if any(row)] + assert len(summary_values) > 0 + assert summary_values[0][1] == "CORE-000409" + assert summary_values[0][4] == 7 + + # --- Issue Details --- + expected_values = [ + "C199989, C199989, Phase II Trial, PHASE II TRIAL, C15601, Phase Ib Trial", + "null, null, Not Applicable, NOT APPLICABLE, C48660, N/A", + "C198366, C198366, null, null, C198366xx, Phase I/II/III Trial", + "C15600, C15600, null, null, C00001x, Phase I Trial", + "C198366, C198366, Phase II Trial, PHASE II TRIAL, C15601, Phase I/II/III Trial", + "C199989, C199989, null, null, C198366xx, PHASE IB TRIAL", + "null, null, Phase Ib Trial, PHASE IB TRIAL, C199989, PHASE 2 TRIAL", + ] + issue_details_sheet = workbook[issue_datails_sheet] + details_values = [ + row for row in issue_details_sheet.iter_rows(values_only=True) + ][1:] + details_values = [row for row in details_values if any(row)] + assert all(row[0] == "CORE-000409" for row in details_values) + actual_values = [row[-1] for row in details_values] + assert actual_values == expected_values + + # --- Rules Report --- + rules_values = [ + row for row in workbook[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + assert len(rules_values) > 0 + assert rules_values[0][0] == "CORE-000409" + assert rules_values[0][-1] == "ISSUE REPORTED" + + if os.path.exists(excel_file_path): + os.remove(excel_file_path) + + +# if __name__ == "__main__": +# unittest.main() diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1332.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1332.py index ff98b9148..0072e2341 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1332.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1332.py @@ -1,111 +1,112 @@ -import os -import subprocess -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - issue_datails_sheet, - rules_report_sheet, -) - - -@pytest.mark.regression -def test_ap_domain_should_be_correctly_substituted(): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-3", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue1332", - "Datasets.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue1332", "Rule.yml"), - ] - subprocess.run(command, check=True) - - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - - # # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # Go to the "Issue Details" sheet - sheet = workbook[issue_datails_sheet] - details_values = [row for row in sheet.iter_rows(values_only=True)][1:] - details_values = [row for row in details_values if any(row)] - assert len(details_values) == 8 - - # CORE-ID - assert all(row[0] == "CORE-000181" for row in details_values) - - # Message - assert ( - len( - [ - row - for row in details_values - if row[1] == "APMH Domain value length is not equal to 4." - ] - ) - == 4 - ) - assert ( - len( - [ - row - for row in details_values - if row[1] == "APSQ Domain value length is not equal to 4." - ] - ) - == 4 - ) - - # Variable(s) - assert len([row for row in details_values if row[7] == "MHTERM"]) == 4 - assert len([row for row in details_values if row[7] == "SQTERM"]) == 4 - - # Value(s) - assert all(row[8] == "POMPE DISEASE" for row in details_values) - - # Go to the "Issue Summary" sheet - issue_summary_sheet = workbook["Issue Summary"] - summary_values = [row for row in issue_summary_sheet.iter_rows(values_only=True)][ - 1: - ] - summary_values = [row for row in summary_values if any(row)] - assert len(summary_values) == 2 - assert all(row[1] == "CORE-000181" for row in summary_values) # CORE-ID - assert ( - summary_values[0][2] == "APMH Domain value length is not equal to 4." - ) # Message - assert ( - summary_values[1][2] == "APSQ Domain value length is not equal to 4." - ) # Message - assert all(row[3] == 4 for row in summary_values) # Issues - - # --- Rules Report --- - rules_values = [ - row for row in workbook[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - assert len(rules_values) == 1 - assert rules_values[0][0] == "CORE-000181" - assert "ISSUE REPORTED" in rules_values[0] - - if os.path.exists(excel_file_path): - os.remove(excel_file_path) +import os +import subprocess +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + issue_datails_sheet, + rules_report_sheet, +) + + +def test_ap_domain_should_be_correctly_substituted(): + """Verify the "AP--" domain-prefix placeholder in a rule's scope is + correctly substituted with actual AP domains (APMH, APSQ), producing + 4 issues each on MHTERM/SQTERM with value "POMPE DISEASE".""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-3", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue1332", + "Datasets.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue1332", "Rule.yml"), + ] + subprocess.run(command, check=True) + + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + + # # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # Go to the "Issue Details" sheet + sheet = workbook[issue_datails_sheet] + details_values = [row for row in sheet.iter_rows(values_only=True)][1:] + details_values = [row for row in details_values if any(row)] + assert len(details_values) == 8 + + # CORE-ID + assert all(row[0] == "CORE-000181" for row in details_values) + + # Message + assert ( + len( + [ + row + for row in details_values + if row[1] == "APMH Domain value length is not equal to 4." + ] + ) + == 4 + ) + assert ( + len( + [ + row + for row in details_values + if row[1] == "APSQ Domain value length is not equal to 4." + ] + ) + == 4 + ) + + # Variable(s) + assert len([row for row in details_values if row[7] == "MHTERM"]) == 4 + assert len([row for row in details_values if row[7] == "SQTERM"]) == 4 + + # Value(s) + assert all(row[8] == "POMPE DISEASE" for row in details_values) + + # Go to the "Issue Summary" sheet + issue_summary_sheet = workbook["Issue Summary"] + summary_values = [row for row in issue_summary_sheet.iter_rows(values_only=True)][ + 1: + ] + summary_values = [row for row in summary_values if any(row)] + assert len(summary_values) == 2 + assert all(row[1] == "CORE-000181" for row in summary_values) # CORE-ID + assert ( + summary_values[0][2] == "APMH Domain value length is not equal to 4." + ) # Message + assert ( + summary_values[1][2] == "APSQ Domain value length is not equal to 4." + ) # Message + assert all(row[3] == 4 for row in summary_values) # Issues + + # --- Rules Report --- + rules_values = [ + row for row in workbook[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + assert len(rules_values) == 1 + assert rules_values[0][0] == "CORE-000181" + assert "ISSUE REPORTED" in rules_values[0] + + if os.path.exists(excel_file_path): + os.remove(excel_file_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1345.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1345.py index bb160fd3f..15735cc11 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1345.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1345.py @@ -1,167 +1,168 @@ -import os -import subprocess - -import pytest -import json -from conftest import get_python_executable - - -@pytest.fixture -def generate_report(): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-4", - "-dxp", - os.path.join( - "tests", - "resources", - "CoreIssue1345", - "define_msg20_testsupp_core.xml", - ), - "-d", - os.path.join( - "tests", - "resources", - "CoreIssue1345", - ), - "-lr", - os.path.join( - "tests", - "resources", - "CoreIssue1345", - ), - "-r", - "CDISC.SDTMIG.CG0019", - "-l", - "error", - "-ps", - "1", - "-of", - "json", - ] - subprocess.run(command, check=True) - # Get the latest created Excel file - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - json_report = json.load(open(json_report_path)) - return json_report_path, json_report - - -@pytest.mark.regression -class TestCoreIssue1345: - def test_engine_correctly_merges_datasets_and_flags_row_uniqueness_issues( - self, generate_report - ): - json_report_path, json_report = generate_report - dataset_filenames = { - d["filename"].upper() for d in json_report.get("Dataset_Details", []) - } - - assert "DM" in dataset_filenames, "DM dataset is missing from Dataset_Details" - assert ( - "SUPPDM" in dataset_filenames - ), "SUPPDM dataset is missing from Dataset_Details" - - # 2. check for DM / SUPPDM Issue_Details - dm_related_issues = [ - issue - for issue in json_report.get("Issue_Details", []) - if issue.get("dataset", "") in {"DM", "SUPPDM"} - ] - - assert not dm_related_issues, ( - "Found issues related to DM/SUPPDM datasets:\n" f"{dm_related_issues}" - ) - - dm_related_summary = [ - s - for s in json_report.get("Issue_Summary", []) - if s.get("dataset", "") in {"DM", "SUPPDM"} - ] - - assert not dm_related_summary, ( - "Found issue summary entries related to DM/SUPPDM:\n" - f"{dm_related_summary}" - ) - - ec_detail_issues = [ - i - for i in json_report.get("Issue_Details", []) - if i.get("dataset", "") in {"EC"} - ] - - assert ( - ec_detail_issues - ), "Expected EC-related issues in Issue_Details, but none found" - assert ( - len(ec_detail_issues) == 2 - ), f"Expected 2 issues for EC dataset, but {len(ec_detail_issues)} found in Issue_Details" - - ec_summary_issues = [ - s - for s in json_report.get("Issue_Summary", []) - if s.get("dataset", "") in {"EC"} - ] - - assert ( - ec_summary_issues - ), "Expected issues for EC dataset, but none found in Issue_Summary" - - if os.path.exists(json_report_path): - os.remove(json_report_path) - - def test_engine_correctly_processes_relrec_when_supp_datasets_provided( - self, generate_report - ): - json_report_path, json_report = generate_report - # Open the JSON report file - dataset_filenames = { - d["filename"].upper() for d in json_report.get("Dataset_Details", []) - } - - assert "DM" in dataset_filenames, "DM dataset is missing from Dataset_Details" - assert ( - "SUPPDM" in dataset_filenames - ), "SUPPDM dataset is missing from Dataset_Details" - assert "EC" in dataset_filenames, "EC dataset is missing from Dataset_Details" - assert ( - "SUPPEC" in dataset_filenames - ), "SUPPEC dataset is missing from Dataset_Details" - - # check that relrec was processed and rule checked the data - assert ( - "RELREC" in dataset_filenames - ), "RELREC dataset is missing from Dataset_Details" - relrec_issues = [ - i - for i in json_report.get("Issue_Details", []) - if i.get("dataset", "") in {"RELREC"} - ] - assert ( - len(relrec_issues) == 2 - ), f"Expected 2 issues for RELREC dataset, but {len(relrec_issues)} found" - - # to confirm that EC is still processed and contains issues - ec_detail_issues = [ - i - for i in json_report.get("Issue_Details", []) - if i.get("dataset", "") in {"EC"} - ] - assert ( - len(ec_detail_issues) == 2 - ), f"Expected 2 issues for EC dataset, but {len(ec_detail_issues)} found in Issue_Details" - - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess + +import pytest +import json +from conftest import get_python_executable + + +@pytest.fixture +def generate_report(): + """Run CG0019.yml against the DM/SUPPDM/EC/SUPPEC/RELREC dataset + fixture and return the parsed JSON report for reuse by both tests.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-4", + "-dxp", + os.path.join( + "tests", + "resources", + "CoreIssue1345", + "define_msg20_testsupp_core.xml", + ), + "-d", + os.path.join( + "tests", + "resources", + "CoreIssue1345", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue1345", "CG0019.yml"), + "-l", + "error", + "-ps", + "1", + "-of", + "json", + ] + subprocess.run(command, check=True) + # Get the latest created Excel file + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + json_report = json.load(open(json_report_path)) + return json_report_path, json_report + + +class TestCoreIssue1345: + def test_engine_correctly_merges_datasets_and_flags_row_uniqueness_issues( + self, generate_report + ): + """Verify merging DM with its SUPPDM does not spuriously flag any + DM/SUPPDM rows, while the unrelated EC dataset still produces its + expected 2 issues (merge logic doesn't affect unrelated datasets).""" + json_report_path, json_report = generate_report + dataset_filenames = { + d["filename"].upper() for d in json_report.get("Dataset_Details", []) + } + + assert "DM" in dataset_filenames, "DM dataset is missing from Dataset_Details" + assert ( + "SUPPDM" in dataset_filenames + ), "SUPPDM dataset is missing from Dataset_Details" + + # 2. check for DM / SUPPDM Issue_Details + dm_related_issues = [ + issue + for issue in json_report.get("Issue_Details", []) + if issue.get("dataset", "") in {"DM", "SUPPDM"} + ] + + assert not dm_related_issues, ( + "Found issues related to DM/SUPPDM datasets:\n" f"{dm_related_issues}" + ) + + dm_related_summary = [ + s + for s in json_report.get("Issue_Summary", []) + if s.get("dataset", "") in {"DM", "SUPPDM"} + ] + + assert not dm_related_summary, ( + "Found issue summary entries related to DM/SUPPDM:\n" + f"{dm_related_summary}" + ) + + ec_detail_issues = [ + i + for i in json_report.get("Issue_Details", []) + if i.get("dataset", "") in {"EC"} + ] + + assert ( + ec_detail_issues + ), "Expected EC-related issues in Issue_Details, but none found" + assert ( + len(ec_detail_issues) == 2 + ), f"Expected 2 issues for EC dataset, but {len(ec_detail_issues)} found in Issue_Details" + + ec_summary_issues = [ + s + for s in json_report.get("Issue_Summary", []) + if s.get("dataset", "") in {"EC"} + ] + + assert ( + ec_summary_issues + ), "Expected issues for EC dataset, but none found in Issue_Summary" + + if os.path.exists(json_report_path): + os.remove(json_report_path) + + def test_engine_correctly_processes_relrec_when_supp_datasets_provided( + self, generate_report + ): + """Verify RELREC is processed correctly (yielding exactly 2 issues) + when SUPP-- datasets are also present, and that this doesn't disturb + the EC dataset's own 2 issues from the other test.""" + json_report_path, json_report = generate_report + # Open the JSON report file + dataset_filenames = { + d["filename"].upper() for d in json_report.get("Dataset_Details", []) + } + + assert "DM" in dataset_filenames, "DM dataset is missing from Dataset_Details" + assert ( + "SUPPDM" in dataset_filenames + ), "SUPPDM dataset is missing from Dataset_Details" + assert "EC" in dataset_filenames, "EC dataset is missing from Dataset_Details" + assert ( + "SUPPEC" in dataset_filenames + ), "SUPPEC dataset is missing from Dataset_Details" + + # check that relrec was processed and rule checked the data + assert ( + "RELREC" in dataset_filenames + ), "RELREC dataset is missing from Dataset_Details" + relrec_issues = [ + i + for i in json_report.get("Issue_Details", []) + if i.get("dataset", "") in {"RELREC"} + ] + assert ( + len(relrec_issues) == 2 + ), f"Expected 2 issues for RELREC dataset, but {len(relrec_issues)} found" + + # to confirm that EC is still processed and contains issues + ec_detail_issues = [ + i + for i in json_report.get("Issue_Details", []) + if i.get("dataset", "") in {"EC"} + ] + assert ( + len(ec_detail_issues) == 2 + ), f"Expected 2 issues for EC dataset, but {len(ec_detail_issues)} found in Issue_Details" + + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1348.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1348.py index 74c41a025..a15d01a0b 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1348.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1348.py @@ -1,234 +1,238 @@ -import os -import subprocess - -import pytest -import json -from conftest import get_python_executable - -_message = ( - "The study version's study phase is not specified according to the extensible Trial Phase Response" - ' (C66737) SDTM codelist - codeSystem is not "http://www.cdisc.org", codeSystemVersion is not a valid' - " terminology package date, and/or the code or decode is found in the codelist (case insensitive)" - " but the corresponding decode or code does not match the codelist value (case sensitive)." -) -_summary = [ - { - "dataset": "StudyVersion", - "core_id": "CORE-000409", - "message": _message, - "issues": 7, - } -] -_issue_details = [ - { - "core_id": "CORE-000409", - "message": _message, - "executability": "fully executable", - "dataset": "StudyVersion", - "USUBJID": "", - "row": 1, - "SEQ": "", - "variables": [ - "$code_for_pref_term", - "$code_for_value", - "$pref_term_for_code", - "$value_for_code", - "studyPhase.standardCode.code", - "studyPhase.standardCode.decode", - ], - "values": [ - "C199989", - "C199989", - "Phase II Trial", - "PHASE II TRIAL", - "C15601", - "Phase Ib Trial", - ], - }, - { - "core_id": "CORE-000409", - "message": _message, - "executability": "fully executable", - "dataset": "StudyVersion", - "USUBJID": "", - "row": 2, - "SEQ": "", - "variables": [ - "$code_for_pref_term", - "$code_for_value", - "$pref_term_for_code", - "$value_for_code", - "studyPhase.standardCode.code", - "studyPhase.standardCode.decode", - ], - "values": ["null", "null", "Not Applicable", "NOT APPLICABLE", "C48660", "N/A"], - }, - { - "core_id": "CORE-000409", - "message": _message, - "executability": "fully executable", - "dataset": "StudyVersion", - "USUBJID": "", - "row": 3, - "SEQ": "", - "variables": [ - "$code_for_pref_term", - "$code_for_value", - "$pref_term_for_code", - "$value_for_code", - "studyPhase.standardCode.code", - "studyPhase.standardCode.decode", - ], - "values": [ - "C198366", - "C198366", - "null", - "null", - "C198366xx", - "Phase I/II/III Trial", - ], - }, - { - "core_id": "CORE-000409", - "message": _message, - "executability": "fully executable", - "dataset": "StudyVersion", - "USUBJID": "", - "row": 4, - "SEQ": "", - "variables": [ - "$code_for_pref_term", - "$code_for_value", - "$pref_term_for_code", - "$value_for_code", - "studyPhase.standardCode.code", - "studyPhase.standardCode.decode", - ], - "values": ["C15600", "C15600", "null", "null", "C00001x", "Phase I Trial"], - }, - { - "core_id": "CORE-000409", - "message": _message, - "executability": "fully executable", - "dataset": "StudyVersion", - "USUBJID": "", - "row": 5, - "SEQ": "", - "variables": [ - "$code_for_pref_term", - "$code_for_value", - "$pref_term_for_code", - "$value_for_code", - "studyPhase.standardCode.code", - "studyPhase.standardCode.decode", - ], - "values": [ - "C198366", - "C198366", - "Phase II Trial", - "PHASE II TRIAL", - "C15601", - "Phase I/II/III Trial", - ], - }, - { - "core_id": "CORE-000409", - "message": _message, - "executability": "fully executable", - "dataset": "StudyVersion", - "USUBJID": "", - "row": 6, - "SEQ": "", - "variables": [ - "$code_for_pref_term", - "$code_for_value", - "$pref_term_for_code", - "$value_for_code", - "studyPhase.standardCode.code", - "studyPhase.standardCode.decode", - ], - "values": ["C199989", "C199989", "null", "null", "C198366xx", "PHASE IB TRIAL"], - }, - { - "core_id": "CORE-000409", - "message": _message, - "executability": "fully executable", - "dataset": "StudyVersion", - "USUBJID": "", - "row": 7, - "SEQ": "", - "variables": [ - "$code_for_pref_term", - "$code_for_value", - "$pref_term_for_code", - "$value_for_code", - "studyPhase.standardCode.code", - "studyPhase.standardCode.decode", - ], - "values": [ - "null", - "null", - "Phase Ib Trial", - "PHASE IB TRIAL", - "C199989", - "PHASE 2 TRIAL", - ], - }, -] - - -@pytest.mark.regression -class TestCoreIssue1348: - @pytest.mark.parametrize( - "rule_name, dataset, issue_summary, details", - [ - ("DDF00015_not.yaml", "data_pos", [], []), - ("DDF00015_not.yaml", "data_neg", _summary, _issue_details), - ("DDF00015_all.yaml", "data_pos", [], []), - ("DDF00015_all.yaml", "data_neg", _summary, _issue_details), - ], - ) - def test_not_as_first_condition_same_errors_as_all_condition( - self, rule_name, dataset, issue_summary, details - ): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-4", - "-lr", - os.path.join("tests", "resources", "CoreIssue1348", rule_name), - "-of", - "json", - "--data", - os.path.join("tests", "resources", "CoreIssue1348", dataset), - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - # Open the JSON report file - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - - assert details == json_report["Issue_Details"] - assert issue_summary == json_report["Issue_Summary"] - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess + +import pytest +import json +from conftest import get_python_executable + +_message = ( + "The study version's study phase is not specified according to the extensible Trial Phase Response" + ' (C66737) SDTM codelist - codeSystem is not "http://www.cdisc.org", codeSystemVersion is not a valid' + " terminology package date, and/or the code or decode is found in the codelist (case insensitive)" + " but the corresponding decode or code does not match the codelist value (case sensitive)." +) +_summary = [ + { + "dataset": "StudyVersion", + "core_id": "CORE-000409", + "message": _message, + "issues": 7, + } +] +_issue_details = [ + { + "core_id": "CORE-000409", + "message": _message, + "executability": "fully executable", + "dataset": "StudyVersion", + "USUBJID": "", + "row": 1, + "SEQ": "", + "variables": [ + "$code_for_pref_term", + "$code_for_value", + "$pref_term_for_code", + "$value_for_code", + "studyPhase.standardCode.code", + "studyPhase.standardCode.decode", + ], + "values": [ + "C199989", + "C199989", + "Phase II Trial", + "PHASE II TRIAL", + "C15601", + "Phase Ib Trial", + ], + }, + { + "core_id": "CORE-000409", + "message": _message, + "executability": "fully executable", + "dataset": "StudyVersion", + "USUBJID": "", + "row": 2, + "SEQ": "", + "variables": [ + "$code_for_pref_term", + "$code_for_value", + "$pref_term_for_code", + "$value_for_code", + "studyPhase.standardCode.code", + "studyPhase.standardCode.decode", + ], + "values": ["null", "null", "Not Applicable", "NOT APPLICABLE", "C48660", "N/A"], + }, + { + "core_id": "CORE-000409", + "message": _message, + "executability": "fully executable", + "dataset": "StudyVersion", + "USUBJID": "", + "row": 3, + "SEQ": "", + "variables": [ + "$code_for_pref_term", + "$code_for_value", + "$pref_term_for_code", + "$value_for_code", + "studyPhase.standardCode.code", + "studyPhase.standardCode.decode", + ], + "values": [ + "C198366", + "C198366", + "null", + "null", + "C198366xx", + "Phase I/II/III Trial", + ], + }, + { + "core_id": "CORE-000409", + "message": _message, + "executability": "fully executable", + "dataset": "StudyVersion", + "USUBJID": "", + "row": 4, + "SEQ": "", + "variables": [ + "$code_for_pref_term", + "$code_for_value", + "$pref_term_for_code", + "$value_for_code", + "studyPhase.standardCode.code", + "studyPhase.standardCode.decode", + ], + "values": ["C15600", "C15600", "null", "null", "C00001x", "Phase I Trial"], + }, + { + "core_id": "CORE-000409", + "message": _message, + "executability": "fully executable", + "dataset": "StudyVersion", + "USUBJID": "", + "row": 5, + "SEQ": "", + "variables": [ + "$code_for_pref_term", + "$code_for_value", + "$pref_term_for_code", + "$value_for_code", + "studyPhase.standardCode.code", + "studyPhase.standardCode.decode", + ], + "values": [ + "C198366", + "C198366", + "Phase II Trial", + "PHASE II TRIAL", + "C15601", + "Phase I/II/III Trial", + ], + }, + { + "core_id": "CORE-000409", + "message": _message, + "executability": "fully executable", + "dataset": "StudyVersion", + "USUBJID": "", + "row": 6, + "SEQ": "", + "variables": [ + "$code_for_pref_term", + "$code_for_value", + "$pref_term_for_code", + "$value_for_code", + "studyPhase.standardCode.code", + "studyPhase.standardCode.decode", + ], + "values": ["C199989", "C199989", "null", "null", "C198366xx", "PHASE IB TRIAL"], + }, + { + "core_id": "CORE-000409", + "message": _message, + "executability": "fully executable", + "dataset": "StudyVersion", + "USUBJID": "", + "row": 7, + "SEQ": "", + "variables": [ + "$code_for_pref_term", + "$code_for_value", + "$pref_term_for_code", + "$value_for_code", + "studyPhase.standardCode.code", + "studyPhase.standardCode.decode", + ], + "values": [ + "null", + "null", + "Phase Ib Trial", + "PHASE IB TRIAL", + "C199989", + "PHASE 2 TRIAL", + ], + }, +] + + +class TestCoreIssue1348: + @pytest.mark.parametrize( + "rule_name, dataset, issue_summary, details", + [ + ("DDF00015_not.yaml", "data_pos", [], []), + ("DDF00015_not.yaml", "data_neg", _summary, _issue_details), + ("DDF00015_all.yaml", "data_pos", [], []), + ("DDF00015_all.yaml", "data_neg", _summary, _issue_details), + ], + ) + def test_not_as_first_condition_same_errors_as_all_condition( + self, rule_name, dataset, issue_summary, details + ): + """Verify a rule whose Check starts with a `not:` condition + (DDF00015_not.yaml) produces byte-identical Issue_Details/ + Issue_Summary output to the logically-equivalent rule using `all:` + (DDF00015_all.yaml), for both positive and negative datasets -- + confirming condition-evaluation order doesn't change results.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-4", + "-lr", + os.path.join("tests", "resources", "CoreIssue1348", rule_name), + "-of", + "json", + "--data", + os.path.join("tests", "resources", "CoreIssue1348", dataset), + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + # Open the JSON report file + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + + assert details == json_report["Issue_Details"] + assert issue_summary == json_report["Issue_Summary"] + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1387.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1387.py index 3c94be9e9..7b2a4ec28 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1387.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1387.py @@ -1,240 +1,241 @@ -import subprocess -import os -import tempfile -import shutil -import pytest -from conftest import get_python_executable - - -@pytest.mark.regression -def test_multiple_xlsx_files_shows_helpful_error(): - """Test that multiple XLSX files show a helpful error message about single file limitation""" - with tempfile.TemporaryDirectory() as temp_dir: - xlsx_files = ["ae.xlsx", "dm.xlsx"] - - for filename in xlsx_files: - open(os.path.join(temp_dir, filename), "w").close() - - command = [ - get_python_executable(), - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-d", - temp_dir, - ] - - process = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - stdout, stderr = process.communicate() - - stderr_text = stderr.decode() - stdout_text = stdout.decode() - - assert ( - process.returncode != 0 - ), "Expected non-zero exit code for multiple XLSX files" - assert "KeyError" not in stderr_text, "Should not show KeyError crash" - assert ( - "Failed to execute script" not in stderr_text - ), "Should not show script execution failure" - assert ( - "Multiple XLSX files found" in stderr_text - or "Multiple XLSX files found" in stdout_text - ), f"Expected helpful error message about multiple XLSX files. stderr: {stderr_text}, stdout: {stdout_text}" - assert ( - "Excel format (XLSX) validation only supports single files" in stderr_text - or "Excel format (XLSX) validation only supports single files" - in stdout_text - ), "Expected explanation of XLSX limitation" - - -@pytest.mark.regression -def test_folder_with_xlsx_files_works_with_excel_service(): - """Test that a folder with a single XLSX file now works with ExcelDataService""" - with tempfile.TemporaryDirectory() as temp_dir: - xlsx_files = ["ae.xlsx"] - - for filename in xlsx_files: - open(os.path.join(temp_dir, filename), "w").close() - - command = [ - get_python_executable(), - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-d", - temp_dir, - ] - - process = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - stdout, stderr = process.communicate() - - stderr_text = stderr.decode() - stdout_text = stdout.decode() - - assert "KeyError: 'XLSX'" not in stderr_text, "Should not show KeyError crash" - assert ( - "Failed to execute script" not in stderr_text - ), "Should not show script execution failure" - - assert ( - "No valid dataset files found" not in stderr_text - and "No valid dataset files found" not in stdout_text - ), "XLSX files should now be recognized as valid" - - -@pytest.mark.regression -def test_folder_with_unsupported_formats_shows_helpful_error(): - """Test that folders with truly unsupported formats (like PDF) show helpful error messages""" - with tempfile.TemporaryDirectory() as temp_dir: - unsupported_files = ["ae.pdf", "dm.txt"] - - for filename in unsupported_files: - open(os.path.join(temp_dir, filename), "w").close() - - command = [ - get_python_executable(), - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-d", - temp_dir, - ] - - process = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - stdout, stderr = process.communicate() - - stderr_text = stderr.decode() - stdout_text = stdout.decode() - - assert ( - process.returncode != 0 - ), "Expected non-zero exit code for unsupported files" - assert "KeyError" not in stderr_text, "Should not show KeyError crash" - assert ( - "Failed to execute script" not in stderr_text - ), "Should not show script execution failure" - assert ( - "No valid dataset files found" in stderr_text - or "No valid dataset files found" in stdout_text - ), f"Expected helpful error message. stderr: {stderr_text}, stdout: {stdout_text}" - assert ( - "SAS V5 XPT, Dataset-JSON" in stderr_text - or "SAS V5 XPT, Dataset-JSON" in stdout_text - ), "Expected format guidance in error message" - - -@pytest.mark.regression -def test_empty_folder_shows_helpful_error(): - with tempfile.TemporaryDirectory() as temp_dir: - command = [ - get_python_executable(), - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-d", - temp_dir, - ] - - process = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - stdout, stderr = process.communicate() - - stderr_text = stderr.decode() - stdout_text = stdout.decode() - - assert process.returncode != 0, "Expected non-zero exit code for empty folder" - assert ( - "No valid dataset files found" in stderr_text - or "No valid dataset files found" in stdout_text - ), "Expected helpful error message for empty folder" - - -@pytest.mark.regression -def test_valid_xpt_files_work_normally(): - command = [ - get_python_executable(), - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-dp", - os.path.join("tests", "resources", "test_dataset.xpt"), - ] - - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - - stderr_text = stderr.decode() - - assert "KeyError: 'XLSX'" not in stderr_text, "Should not show KeyError" - assert "Failed to execute script" not in stderr_text, "Should not crash" - assert ( - "No valid dataset files found" not in stderr_text - ), "Should find valid XPT file" - - -@pytest.mark.regression -def test_mixed_folder_processes_valid_files(): - with tempfile.TemporaryDirectory() as temp_dir: - valid_xpt = os.path.join("tests", "resources", "test_dataset.xpt") - if os.path.exists(valid_xpt): - shutil.copy(valid_xpt, os.path.join(temp_dir, "ae.xpt")) - - xlsx_file = os.path.join(temp_dir, "dm.xlsx") - open(xlsx_file, "w").close() - - command = [ - get_python_executable(), - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-d", - temp_dir, - ] - - process = subprocess.Popen( - command, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - stdout, stderr = process.communicate() - - stderr_text = stderr.decode() - stdout_text = stdout.decode() - - assert "KeyError" not in stderr_text, "Should not crash on any files" - assert "Failed to execute script" not in stderr_text, "Should not crash" - - assert ( - "No valid dataset files found" not in stderr_text - and "No valid dataset files found" not in stdout_text - ), "Should find the valid XPT file" +import subprocess +import os +import tempfile +import shutil +from conftest import get_python_executable + + +def test_multiple_xlsx_files_shows_helpful_error(): + """Test that multiple XLSX files show a helpful error message about single file limitation""" + with tempfile.TemporaryDirectory() as temp_dir: + xlsx_files = ["ae.xlsx", "dm.xlsx"] + + for filename in xlsx_files: + open(os.path.join(temp_dir, filename), "w").close() + + command = [ + get_python_executable(), + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3.4", + "-d", + temp_dir, + ] + + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + stdout, stderr = process.communicate() + + stderr_text = stderr.decode() + stdout_text = stdout.decode() + + assert ( + process.returncode != 0 + ), "Expected non-zero exit code for multiple XLSX files" + assert "KeyError" not in stderr_text, "Should not show KeyError crash" + assert ( + "Failed to execute script" not in stderr_text + ), "Should not show script execution failure" + assert ( + "Multiple XLSX files found" in stderr_text + or "Multiple XLSX files found" in stdout_text + ), f"Expected helpful error message about multiple XLSX files. stderr: {stderr_text}, stdout: {stdout_text}" + assert ( + "Excel format (XLSX) validation only supports single files" in stderr_text + or "Excel format (XLSX) validation only supports single files" + in stdout_text + ), "Expected explanation of XLSX limitation" + + +def test_folder_with_xlsx_files_works_with_excel_service(): + """Test that a folder with a single XLSX file now works with ExcelDataService""" + with tempfile.TemporaryDirectory() as temp_dir: + xlsx_files = ["ae.xlsx"] + + for filename in xlsx_files: + open(os.path.join(temp_dir, filename), "w").close() + + command = [ + get_python_executable(), + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3.4", + "-d", + temp_dir, + ] + + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + stdout, stderr = process.communicate() + + stderr_text = stderr.decode() + stdout_text = stdout.decode() + + assert "KeyError: 'XLSX'" not in stderr_text, "Should not show KeyError crash" + assert ( + "Failed to execute script" not in stderr_text + ), "Should not show script execution failure" + + assert ( + "No valid dataset files found" not in stderr_text + and "No valid dataset files found" not in stdout_text + ), "XLSX files should now be recognized as valid" + + +def test_folder_with_unsupported_formats_shows_helpful_error(): + """Test that folders with truly unsupported formats (like PDF) show helpful error messages""" + with tempfile.TemporaryDirectory() as temp_dir: + unsupported_files = ["ae.pdf", "dm.txt"] + + for filename in unsupported_files: + open(os.path.join(temp_dir, filename), "w").close() + + command = [ + get_python_executable(), + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3.4", + "-d", + temp_dir, + ] + + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + stdout, stderr = process.communicate() + + stderr_text = stderr.decode() + stdout_text = stdout.decode() + + assert ( + process.returncode != 0 + ), "Expected non-zero exit code for unsupported files" + assert "KeyError" not in stderr_text, "Should not show KeyError crash" + assert ( + "Failed to execute script" not in stderr_text + ), "Should not show script execution failure" + assert ( + "No valid dataset files found" in stderr_text + or "No valid dataset files found" in stdout_text + ), f"Expected helpful error message. stderr: {stderr_text}, stdout: {stdout_text}" + assert ( + "SAS V5 XPT, Dataset-JSON" in stderr_text + or "SAS V5 XPT, Dataset-JSON" in stdout_text + ), "Expected format guidance in error message" + + +def test_empty_folder_shows_helpful_error(): + """Test that validating an empty -d folder exits non-zero and surfaces a + helpful "No valid dataset files found" message instead of crashing.""" + with tempfile.TemporaryDirectory() as temp_dir: + command = [ + get_python_executable(), + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3.4", + "-d", + temp_dir, + ] + + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + stdout, stderr = process.communicate() + + stderr_text = stderr.decode() + stdout_text = stdout.decode() + + assert process.returncode != 0, "Expected non-zero exit code for empty folder" + assert ( + "No valid dataset files found" in stderr_text + or "No valid dataset files found" in stdout_text + ), "Expected helpful error message for empty folder" + + +def test_valid_xpt_files_work_normally(): + """Regression check that a valid .xpt file passed via -dp still validates + normally, confirming the format-detection logic added for XLSX handling + didn't break existing XPT support.""" + command = [ + get_python_executable(), + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3.4", + "-dp", + os.path.join("tests", "resources", "test_dataset.xpt"), + ] + + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + stderr_text = stderr.decode() + + assert "KeyError: 'XLSX'" not in stderr_text, "Should not show KeyError" + assert "Failed to execute script" not in stderr_text, "Should not crash" + assert ( + "No valid dataset files found" not in stderr_text + ), "Should find valid XPT file" + + +def test_mixed_folder_processes_valid_files(): + """Test that a -d folder containing one valid .xpt file alongside an + empty/invalid .xlsx file still processes the valid dataset without + crashing.""" + with tempfile.TemporaryDirectory() as temp_dir: + valid_xpt = os.path.join("tests", "resources", "test_dataset.xpt") + if os.path.exists(valid_xpt): + shutil.copy(valid_xpt, os.path.join(temp_dir, "ae.xpt")) + + xlsx_file = os.path.join(temp_dir, "dm.xlsx") + open(xlsx_file, "w").close() + + command = [ + get_python_executable(), + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3.4", + "-d", + temp_dir, + ] + + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + stdout, stderr = process.communicate() + + stderr_text = stderr.decode() + stdout_text = stdout.decode() + + assert "KeyError" not in stderr_text, "Should not crash on any files" + assert "Failed to execute script" not in stderr_text, "Should not crash" + + assert ( + "No valid dataset files found" not in stderr_text + and "No valid dataset files found" not in stdout_text + ), "Should find the valid XPT file" diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1421.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1421.py index 009e7eca0..47f554231 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1421.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1421.py @@ -1,132 +1,134 @@ -import os -import subprocess -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - dataset_details_sheet, - issue_datails_sheet, - rules_report_sheet, - issue_sheet_variable_column, - issue_sheet_coreid_column, -) - - -@pytest.mark.regression -def test_validate_define_xml_against_lib_metadata(): - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-4", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue1421", - "Dataset.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue1421", "Rule.yml"), - "-dxp", - os.path.join("tests", "resources", "CoreIssue1421", "Define.xml"), - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # Go to the "Issue Details" sheet - sheet = workbook[issue_datails_sheet] - - # Check Variable(s) column (H) - variables_names_column = sheet["H"] - variables_names_values = [ - cell.value for cell in variables_names_column[1:] if cell.value is not None - ] - assert len(variables_names_values) == 3 - for value in variables_names_values: - assert len(value.split(",")) == 6 - - # Check Value(s) column (I) - variables_values_column = sheet[issue_sheet_variable_column] - variables_values = [ - cell.value for cell in variables_values_column[1:] if cell.value is not None - ] - assert len(variables_values) == 3 - for value in variables_values: - assert len(value.split(",")) == 6 - - dataset_column = sheet["D"] - dataset_column_values = [ - cell.value for cell in dataset_column[1:] if cell.value is not None - ] - assert sorted(set(dataset_column_values)) == ["DM", "SUPPEC"] - - core_id_column = sheet[issue_sheet_coreid_column] - core_id_column_values = [ - cell.value for cell in core_id_column[1:] if cell.value is not None - ] - assert set(core_id_column_values) == {"CDISC.SDTMIG.CG0999"} - - # Go to the "Rules Report" sheet - rules_values = [ - row for row in workbook[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - assert rules_values[0][0] == "CDISC.SDTMIG.CG0999" - assert "ISSUE REPORTED" in rules_values[0] - assert ( - rules_values[0][4] - == "Issue with codelist definition in the Define-XML document." - ) - - # Go to the "Dataset Details" sheet - dataset_sheet = workbook[dataset_details_sheet] - dataset_values = [row for row in dataset_sheet.iter_rows(values_only=True)][1:] - dataset_values = [row for row in dataset_values if any(row)] - assert len(dataset_values) > 0 - dataset_names = set(row[0] for row in dataset_values if row[0] is not None) - assert dataset_names == {"AE", "DM", "EC", "EX", "SUPPEC"} - expected_records = { - "AE": 74, - "DM": 18, - "EC": 1590, - "EX": 1583, - "SUPPEC": 13, - } - for row in dataset_values: - dataset_name = row[0] - records_count = row[-1] - assert records_count == expected_records[dataset_name] - - # Go to the "Issue Summary" sheet - issue_summary_sheet = workbook["Issue Summary"] - summary_values = [row for row in issue_summary_sheet.iter_rows(values_only=True)][ - 1: - ] - summary_values = [row for row in summary_values if any(row)] - assert len(summary_values) == 2 - core_ids = set(row[1] for row in summary_values if row[1] is not None) - assert core_ids == {"CDISC.SDTMIG.CG0999"} - # Check Message and dataset columns - for row in summary_values: - assert row[2] == "Issue with codelist definition in the Define-XML document." - datasets_in_summary = set(row[0] for row in summary_values if row[0] is not None) - assert datasets_in_summary == {"DM", "SUPPEC"} - - # Delete the excel file - if os.path.exists(excel_file_path): - os.remove(excel_file_path) +import os +import subprocess +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + dataset_details_sheet, + issue_datails_sheet, + rules_report_sheet, + issue_sheet_variable_column, + issue_sheet_coreid_column, +) + + +def test_validate_define_xml_against_lib_metadata(): + """Validates that codelist definitions in a Define-XML are checked + against CDISC library CT metadata via rule CDISC.SDTMIG.CG0999: verifies + issue rows/columns, per-dataset record counts (AE, DM, EC, EX, SUPPEC), + and that only DM and SUPPEC datasets report issues.""" + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-4", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue1421", + "Dataset.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue1421", "Rule.yml"), + "-dxp", + os.path.join("tests", "resources", "CoreIssue1421", "Define.xml"), + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # Go to the "Issue Details" sheet + sheet = workbook[issue_datails_sheet] + + # Check Variable(s) column (H) + variables_names_column = sheet["H"] + variables_names_values = [ + cell.value for cell in variables_names_column[1:] if cell.value is not None + ] + assert len(variables_names_values) == 3 + for value in variables_names_values: + assert len(value.split(",")) == 6 + + # Check Value(s) column (I) + variables_values_column = sheet[issue_sheet_variable_column] + variables_values = [ + cell.value for cell in variables_values_column[1:] if cell.value is not None + ] + assert len(variables_values) == 3 + for value in variables_values: + assert len(value.split(",")) == 6 + + dataset_column = sheet["D"] + dataset_column_values = [ + cell.value for cell in dataset_column[1:] if cell.value is not None + ] + assert sorted(set(dataset_column_values)) == ["DM", "SUPPEC"] + + core_id_column = sheet[issue_sheet_coreid_column] + core_id_column_values = [ + cell.value for cell in core_id_column[1:] if cell.value is not None + ] + assert set(core_id_column_values) == {"CDISC.SDTMIG.CG0999"} + + # Go to the "Rules Report" sheet + rules_values = [ + row for row in workbook[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + assert rules_values[0][0] == "CDISC.SDTMIG.CG0999" + assert "ISSUE REPORTED" in rules_values[0] + assert ( + rules_values[0][4] + == "Issue with codelist definition in the Define-XML document." + ) + + # Go to the "Dataset Details" sheet + dataset_sheet = workbook[dataset_details_sheet] + dataset_values = [row for row in dataset_sheet.iter_rows(values_only=True)][1:] + dataset_values = [row for row in dataset_values if any(row)] + assert len(dataset_values) > 0 + dataset_names = set(row[0] for row in dataset_values if row[0] is not None) + assert dataset_names == {"AE", "DM", "EC", "EX", "SUPPEC"} + expected_records = { + "AE": 74, + "DM": 18, + "EC": 1590, + "EX": 1583, + "SUPPEC": 13, + } + for row in dataset_values: + dataset_name = row[0] + records_count = row[-1] + assert records_count == expected_records[dataset_name] + + # Go to the "Issue Summary" sheet + issue_summary_sheet = workbook["Issue Summary"] + summary_values = [row for row in issue_summary_sheet.iter_rows(values_only=True)][ + 1: + ] + summary_values = [row for row in summary_values if any(row)] + assert len(summary_values) == 2 + core_ids = set(row[1] for row in summary_values if row[1] is not None) + assert core_ids == {"CDISC.SDTMIG.CG0999"} + # Check Message and dataset columns + for row in summary_values: + assert row[2] == "Issue with codelist definition in the Define-XML document." + datasets_in_summary = set(row[0] for row in summary_values if row[0] is not None) + assert datasets_in_summary == {"DM", "SUPPEC"} + + # Delete the excel file + if os.path.exists(excel_file_path): + os.remove(excel_file_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1442.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1442.py index 172516786..f90697833 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1442.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1442.py @@ -1,91 +1,94 @@ -import os -import subprocess -import openpyxl -import pytest -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue1442: - @pytest.mark.parametrize( - "command", - [ - ( - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "4-0", - "-dp", - os.path.join( - "tests", "resources", "CoreIssue1442", "test_adam_dataset.xpt" - ), - "-dp", - os.path.join( - "tests", "resources", "CoreIssue1442", "test_dataset.ndjson" - ), - "-dp", - os.path.join( - "tests", "resources", "CoreIssue1442", "CDISC_Pilot_Study.json" - ), - "-ft", - "json", - "-lr", - os.path.join("tests", "resources", "CoreIssue1442", "rule.yml"), - ), - ( - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "4-0", - "-d", - os.path.join("tests", "resources", "CoreIssue1442"), - "-ft", - "json", - "-lr", - os.path.join("tests", "resources", "CoreIssue1442", "rule.yml"), - ), - ], - ) - def test_positive_dataset(self, command): - # Run the command in the terminal - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - if "Conformance Details" in workbook.sheetnames: - conformance_sheet = workbook["Conformance Details"] - found = False - for row in conformance_sheet.iter_rows(min_row=2, values_only=True): - for idx, cell in enumerate(row[:-1]): - if ( - cell == "JSON file name" - and row[idx + 1] == "CDISC_Pilot_Study.json" - ): - found = True - break - if found: - break - assert ( - found - ), "Pair ('JSON file name', 'CDISC_Pilot_Study.json') not found in any row of 'Conformance Details' sheet." - else: - assert False, "'Conformance Details' sheet not found in report." - - if os.path.exists(excel_file_path): - os.remove(excel_file_path) +import os +import subprocess +import openpyxl +import pytest +from conftest import get_python_executable + + +class TestCoreIssue1442: + @pytest.mark.parametrize( + "command", + [ + ( + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "4-0", + "-dp", + os.path.join( + "tests", "resources", "CoreIssue1442", "test_adam_dataset.xpt" + ), + "-dp", + os.path.join( + "tests", "resources", "CoreIssue1442", "test_dataset.ndjson" + ), + "-dp", + os.path.join( + "tests", "resources", "CoreIssue1442", "CDISC_Pilot_Study.json" + ), + "-ft", + "json", + "-lr", + os.path.join("tests", "resources", "CoreIssue1442", "rule.yml"), + ), + ( + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "4-0", + "-d", + os.path.join("tests", "resources", "CoreIssue1442"), + "-ft", + "json", + "-lr", + os.path.join("tests", "resources", "CoreIssue1442", "rule.yml"), + ), + ], + ) + def test_positive_dataset(self, command): + """Test that both ways of targeting mixed-format datasets - explicit + -dp paths mixing .xpt/.ndjson/.json, and a -d directory filtered + with -ft json - correctly identify CDISC_Pilot_Study.json as the + "JSON file name" entry in the Conformance Details sheet.""" + # Run the command in the terminal + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + if "Conformance Details" in workbook.sheetnames: + conformance_sheet = workbook["Conformance Details"] + found = False + for row in conformance_sheet.iter_rows(min_row=2, values_only=True): + for idx, cell in enumerate(row[:-1]): + if ( + cell == "JSON file name" + and row[idx + 1] == "CDISC_Pilot_Study.json" + ): + found = True + break + if found: + break + assert ( + found + ), "Pair ('JSON file name', 'CDISC_Pilot_Study.json') not found in any row of 'Conformance Details' sheet." + else: + assert False, "'Conformance Details' sheet not found in report." + + if os.path.exists(excel_file_path): + os.remove(excel_file_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1487.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1487.py index 932a67f5b..44ed1df7b 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1487.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1487.py @@ -1,54 +1,52 @@ -import os -import subprocess -import unittest -import openpyxl -import pytest -from conftest import get_python_executable - - -@pytest.mark.skip(reason="No library metadata found") -class TestCoreIssue1487(unittest.TestCase): - def test_positive_dataset(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "5-0", - "-d", - os.path.join("tests", "resources", "CoreIssue1487"), - "-r", - "CORE-000354", - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - assert "Rules Report" in workbook.sheetnames - rules_sheet = workbook["Rules Report"] - target_row = None - for row in rules_sheet.iter_rows(min_row=2, values_only=True): - if row[0] == "CORE-000354": - target_row = row - break - assert target_row, "Rule CORE-000354 not present in 'Rules Report' sheet." - assert ( - target_row[4] and "was requested but is not available" in target_row[4] - ), "Expected error message for CORE-000354 not found." - assert target_row[5] == "SKIPPED", "CORE-000354 status should be SKIPPED." - - if os.path.exists(excel_file_path): - os.remove(excel_file_path) +import os +import subprocess +import unittest +from conftest import get_python_executable + + +class TestCoreIssue1487(unittest.TestCase): + "The updated engine throws an error for unsupported version, which is expected behavior." + + def test_unsupported_sdtmig_5_0_version_returns_metadata_error(self): + """Test that requesting an unsupported standard version (sdtmig 5.0, + for which no library metadata is cached) fails fast with + LibraryMetadataNotFoundError, before rules are even loaded. + """ + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "5.0", + "-d", + os.path.join("tests", "resources", "CoreIssue1487"), + "-lr", + os.path.join("tests", "resources", "library_rules", "CORE-000354.json"), + ] + + result = subprocess.run( + command, + capture_output=True, + text=True, + ) + + error_output = result.stderr + result.stdout + + assert ( + result.returncode != 0 + ), "Expected validation command to fail, but it succeeded." + + assert ( + "LibraryMetadataNotFoundError" in error_output + ), f"Expected LibraryMetadataNotFoundError, but got:\n{error_output}" + + assert ( + "No library metadata found for standard 'sdtmig' version '5.0'." + in error_output + ), ( + "Expected missing library metadata error for SDTMIG 5.0, " + f"but got:\n{error_output}" + ) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1501.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1501.py index d79dbc2a0..43201db25 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1501.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1501.py @@ -1,62 +1,63 @@ -import os -import subprocess -import unittest -import pytest -import json -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue1501(unittest.TestCase): - def test_raw_report(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sendig", - "-v", - "3-1", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue1501", - "unit-test-coreid-SENDIG282-negative.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue1501", "Rule.yml"), - "-ps", - "1", - "-of", - "json", - "-rr", - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - # Open the JSON report file - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - - if "results_data" not in json_report.keys(): - assert ( - False - ), "'results_data' key not found in report. Expected while using -rr, --raw_report flag" - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess +import unittest +import json +from conftest import get_python_executable + + +class TestCoreIssue1501(unittest.TestCase): + def test_raw_report(self): + """Test that the --raw-report/-rr CLI flag causes the JSON report + output to include an additional "results_data" key alongside the + standard report sections.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sendig", + "-v", + "3-1", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue1501", + "unit-test-coreid-SENDIG282-negative.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue1501", "Rule.yml"), + "-ps", + "1", + "-of", + "json", + "-rr", + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + # Open the JSON report file + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + + if "results_data" not in json_report.keys(): + assert ( + False + ), "'results_data' key not found in report. Expected while using -rr, --raw_report flag" + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1558.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1558.py index 2c424e832..1a96049ee 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1558.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1558.py @@ -1,100 +1,105 @@ -import os -import subprocess -import unittest - -import pytest -import json -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue1558(unittest.TestCase): - def test_raw_report(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-r", - "CORE-000007", - "-v", - "3.4", - "-d", - os.path.join( - "tests", - "resources", - "CoreIssue1558", - "datasets", - ), - "--output-format", - "json", - "-ps", - "1", - ] - subprocess.run(command, check=True) - - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - # Open the JSON report file - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - datasets = {x["filename"] for x in json_report["Dataset_Details"]} - assert {"LB", "DM"}.issubset(datasets) - if os.path.exists(json_report_path): - os.remove(json_report_path) - - def test_env_vars_loaded(self): - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-r", - "CORE-000007", - "--dotenv-path", - os.path.join("tests", "resources", "CoreIssue1558", "test.env"), - "-d", - os.path.join( - "tests", - "resources", - "CoreIssue1558", - "datasets", - ), - "--output-format", - "json", - "-ps", - "1", - ] - subprocess.run(command, capture_output=True, text=True) - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - # Open the JSON report file - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - assert json_report["Conformance_Details"]["Standard"] == "SDTMIG" - assert json_report["Conformance_Details"]["Version"] == "V3.4" +import os +import subprocess +import unittest + +import json +from conftest import get_python_executable + + +class TestCoreIssue1558(unittest.TestCase): + def test_raw_report(self): + """Test that validating a -d directory with multiple dataset files + includes all of them (LB, DM) in Dataset_Details. + """ + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-lr", + os.path.join("tests", "resources", "library_rules", "CORE-000007.json"), + "-v", + "3.4", + "-d", + os.path.join( + "tests", + "resources", + "CoreIssue1558", + "datasets", + ), + "--output-format", + "json", + "-ps", + "1", + ] + subprocess.run(command, check=True) + + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + # Open the JSON report file + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + datasets = {x["filename"] for x in json_report["Dataset_Details"]} + assert {"LB", "DM"}.issubset(datasets) + if os.path.exists(json_report_path): + os.remove(json_report_path) + + def test_env_vars_loaded(self): + """Test that --dotenv-path loads PRODUCT/VERSION env vars so -s/-v + don't need to be passed on the CLI (Conformance_Details still reports + SDTMIG / V3.4). + """ + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-lr", + os.path.join("tests", "resources", "library_rules", "CORE-000007.json"), + "--dotenv-path", + os.path.join("tests", "resources", "CoreIssue1558", "test.env"), + "-d", + os.path.join( + "tests", + "resources", + "CoreIssue1558", + "datasets", + ), + "--output-format", + "json", + "-ps", + "1", + ] + subprocess.run(command, capture_output=True, text=True) + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + # Open the JSON report file + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + assert json_report["Conformance_Details"]["Standard"] == "SDTMIG" + assert json_report["Conformance_Details"]["Version"] == "V3.4" diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue164.py b/tests/QARegressionTests/test_Issues/test_CoreIssue164.py index b62fc4456..f4932ad64 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue164.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue164.py @@ -1,125 +1,128 @@ -import subprocess -import os -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - issue_datails_sheet, - issue_sheet_record_column, - issue_sheet_variable_column, - issue_sheet_values_column, -) - - -@pytest.mark.skip(reason="The test is obsolete and requires modernization.") -def test_negative_dataset(): - command = ( - f"{get_python_executable()} -m core validate -s sdtmig -v 3.4 -r " - + os.path.join("tests", "resources", "CoreIssue164", "rule.json") - + " -dp " - + os.path.join("tests", "resources", "CoreIssue164", "Negative_Dataset.json") - ) - - # Construct the command - command = command.split(" ") - - # Run the command in the terminal - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - - file_name = stdout.decode().strip().split(": ")[1] - # Open the Excel file - workbook = openpyxl.load_workbook(file_name) - - # Go to the "Issue Details" sheet - sheet = workbook[issue_datails_sheet] - - record_column = sheet[issue_sheet_record_column] - variables_column = sheet[issue_sheet_variable_column] - values_column = sheet[issue_sheet_values_column] - - record_values = [cell.value for cell in record_column[1:]] - variables_values = [cell.value for cell in variables_column[1:]] - values_column_values = [cell.value for cell in values_column[1:]] - - # Remove None values using list comprehension - record_values = [value for value in record_values if value is not None] - variables_values = [value for value in variables_values if value is not None] - values_column_values = [ - value for value in values_column_values if value is not None - ] - - # Perform the assertion - # Ensure only two negative values are caught - assert len(record_values) == 2 - assert len(variables_values) == 2 - assert len(values_column_values) == 2 - - # Ensure negatives are detected at correct rows in dataset - assert record_values[0] == 1 - assert record_values[1] == 3 - - # Ensure correct variable is marked as negative - assert variables_values[0] == variables_values[1] == "DSDY" - - # Ensure correct values were marked negatives - assert values_column_values[0] == "0" - assert values_column_values[1] == "alolo" - - # Close the workbook - workbook.close() - - # Delete the file - os.remove(file_name) - - -@pytest.mark.skip(reason="The test is obsolete and requires modernization.") -def test_positive_dataset(): - command = ( - f"{get_python_executable()} -m core validate -s sdtmig -v 3.4 -r " - + os.path.join("tests", "resources", "CoreIssue164", "rule.json") - + " -dp " - + os.path.join("tests", "resources", "CoreIssue164", "Positive_Dataset.json") - ) - - # Construct the command - command = command.split(" ") - - # Run the command in the terminal - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - - file_name = stdout.decode().strip().split(": ")[1] - print(file_name) - # Open the Excel file - workbook = openpyxl.load_workbook(file_name) - - # Go to the "Issue Details" sheet - sheet = workbook[issue_datails_sheet] - - record_column = sheet[issue_sheet_record_column] - variables_column = sheet[issue_sheet_variable_column] - values_column = sheet[issue_sheet_values_column] - - record_values = [cell.value for cell in record_column[1:]] - variables_values = [cell.value for cell in variables_column[1:]] - values_column_values = [cell.value for cell in values_column[1:]] - - # Remove None values using list comprehension - record_values = [value for value in record_values if value is not None] - variables_values = [value for value in variables_values if value is not None] - values_column_values = [ - value for value in values_column_values if value is not None - ] - - # Perform the assertion - # Ensure only two negative values are caught - assert len(record_values) == 0 - assert len(variables_values) == 0 - assert len(values_column_values) == 0 - - # Close the workbook - workbook.close() - - # Delete the file - os.remove(file_name) +import subprocess +import os +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + issue_datails_sheet, + issue_sheet_record_column, + issue_sheet_variable_column, + issue_sheet_values_column, +) + + +def test_negative_dataset(): + """Verify the "All Study Day values are integers" rule flags exactly the + two non-integer DSDY values ("0" and "alolo") in the negative dataset, + at the correct rows and variable.""" + command = ( + f"{get_python_executable()} -m core validate -s sdtmig -v 3.4 -lr " + + os.path.join("tests", "resources", "CoreIssue164", "rule.json") + + " -dp " + + os.path.join("tests", "resources", "CoreIssue164", "Negative_Dataset.json") + ) + + # Construct the command + command = command.split(" ") + + # Run the command in the terminal + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + file_name = stdout.decode().strip().split(": ")[1] + # Open the Excel file + workbook = openpyxl.load_workbook(file_name) + + # Go to the "Issue Details" sheet + sheet = workbook[issue_datails_sheet] + + record_column = sheet[issue_sheet_record_column] + variables_column = sheet[issue_sheet_variable_column] + values_column = sheet[issue_sheet_values_column] + + record_values = [cell.value for cell in record_column[1:]] + variables_values = [cell.value for cell in variables_column[1:]] + values_column_values = [cell.value for cell in values_column[1:]] + + # Remove None values using list comprehension + record_values = [value for value in record_values if value is not None] + variables_values = [value for value in variables_values if value is not None] + values_column_values = [ + value for value in values_column_values if value is not None + ] + + # Perform the assertion + # Ensure only two negative values are caught + assert len(record_values) == 2 + assert len(variables_values) == 2 + assert len(values_column_values) == 2 + + # Ensure negatives are detected at correct rows in dataset + assert record_values[0] == 1 + assert record_values[1] == 3 + + # Ensure correct variable is marked as negative + assert variables_values[0] == variables_values[1] == "DSDY" + + # Ensure correct values were marked negatives + assert values_column_values[0] == "0" + assert values_column_values[1] == "alolo" + + # Close the workbook + workbook.close() + + # Delete the file + os.remove(file_name) + + +def test_positive_dataset(): + """Verify the same "All Study Day values are integers" rule reports zero + issues against the positive dataset, confirming valid integer DSDY + values are not flagged (companion to test_negative_dataset).""" + command = ( + f"{get_python_executable()} -m core validate -s sdtmig -v 3.4 -lr " + + os.path.join("tests", "resources", "CoreIssue164", "rule.json") + + " -dp " + + os.path.join("tests", "resources", "CoreIssue164", "Positive_Dataset.json") + ) + + # Construct the command + command = command.split(" ") + + # Run the command in the terminal + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + file_name = stdout.decode().strip().split(": ")[1] + print(file_name) + # Open the Excel file + workbook = openpyxl.load_workbook(file_name) + + # Go to the "Issue Details" sheet + sheet = workbook[issue_datails_sheet] + + record_column = sheet[issue_sheet_record_column] + variables_column = sheet[issue_sheet_variable_column] + values_column = sheet[issue_sheet_values_column] + + record_values = [cell.value for cell in record_column[1:]] + variables_values = [cell.value for cell in variables_column[1:]] + values_column_values = [cell.value for cell in values_column[1:]] + + # Remove None values using list comprehension + record_values = [value for value in record_values if value is not None] + variables_values = [value for value in variables_values if value is not None] + values_column_values = [ + value for value in values_column_values if value is not None + ] + + # Perform the assertion + # Ensure only two negative values are caught + assert len(record_values) == 0 + assert len(variables_values) == 0 + assert len(values_column_values) == 0 + + # Close the workbook + workbook.close() + + # Delete the file + os.remove(file_name) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1699.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1699.py index c2064523a..39dbd1da8 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1699.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1699.py @@ -1,56 +1,58 @@ -import os -import subprocess -import unittest -import pytest -import json -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue1501(unittest.TestCase): - def test_raw_report(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sendig", - "-v", - "3-1", - "-d", - os.path.join( - "tests", - "resources", - "CoreIssue1699", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue1699", "rule.yml"), - "-ps", - "1", - "-of", - "json", - ] - subprocess.run(command, check=True) - - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - # Open the JSON report file - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - assert json_report["Rules_Report"][0]["status"] == "SUCCESS" - - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess +import unittest +import json +from conftest import get_python_executable + + +class TestCoreIssue1699(unittest.TestCase): + def test_cg0314_supp_rule_reports_success(self): + """Validates the CG0314 SUPP (Supplemental Qualifiers) rule via + rule.yml against the CoreIssue1699 dataset, asserting the rule + reports a SUCCESS status. + """ + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sendig", + "-v", + "3-1", + "-d", + os.path.join( + "tests", + "resources", + "CoreIssue1699", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue1699", "rule.yml"), + "-ps", + "1", + "-of", + "json", + ] + subprocess.run(command, check=True) + + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + # Open the JSON report file + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + assert json_report["Rules_Report"][0]["status"] == "SUCCESS" + + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue1718.py b/tests/QARegressionTests/test_Issues/test_CoreIssue1718.py index 926839bdf..8939d676a 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue1718.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue1718.py @@ -1,60 +1,61 @@ -import os -import subprocess -import pytest -import json -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue1718: - def test_max_issues(self): - # Run the command in the terminal - max_issues = 3 - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-v", - "3-4", - "-d", - os.path.join( - "tests", - "resources", - "CoreIssue1718", - ), - "-r", - "CORE-000356", - "-ps", - "1", - "-of", - "json", - "-me", - f"{max_issues}", - "true", - ] - subprocess.run(command, check=True) - - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - assert json_report["Rules_Report"][0]["status"] == "ISSUE REPORTED" - assert json_report["Issue_Summary"][0]["issues"] == 74 - assert len(json_report["Issue_Details"]) == max_issues - - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess +import json +from conftest import get_python_executable + + +class TestCoreIssue1718: + def test_max_issues(self): + """Test that -me/--max-errors-per-rule (per_dataset_flag=true) caps + Issue_Details rows while Issue_Summary still reports the true total. + """ + # Run the command in the terminal + max_issues = 3 + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-v", + "3-4", + "-d", + os.path.join( + "tests", + "resources", + "CoreIssue1718", + ), + "-lr", + os.path.join("tests", "resources", "library_rules", "CORE-000356.json"), + "-ps", + "1", + "-of", + "json", + "-me", + f"{max_issues}", + "true", + ] + subprocess.run(command, check=True) + + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + assert json_report["Rules_Report"][0]["status"] == "ISSUE REPORTED" + assert json_report["Issue_Summary"][0]["issues"] == 74 + assert len(json_report["Issue_Details"]) == max_issues + + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue208.py b/tests/QARegressionTests/test_Issues/test_CoreIssue208.py index 039a51810..7aabd6f76 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue208.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue208.py @@ -2,7 +2,6 @@ import os import subprocess import unittest -import pytest from conftest import get_python_executable """This regression test is for automating the validation of acceptancce criteria @@ -28,7 +27,6 @@ def find_value(json_data, key): return results -@pytest.mark.regression class JSONSearchTestCase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -59,6 +57,10 @@ def setUpClass(cls): cls.json_file_path = sorted(json_files)[-1] def test_searched_values_are_capital(self): + """Verifies that USUBJID/SEQ values surfaced in the generated JSON + report retain the same (uppercase) case as the corresponding values + in the source dataset, rather than being altered by report + generation.""" # Read the generated JSON file with open(self.json_file_path, "r") as file: json_data = json.load(file) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue324.py b/tests/QARegressionTests/test_Issues/test_CoreIssue324.py index 62e926b38..b0e2fd4bf 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue324.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue324.py @@ -2,11 +2,9 @@ import subprocess import unittest import openpyxl -import pytest from conftest import get_python_executable -@pytest.mark.regression class TerminalCommandTestCase(unittest.TestCase): @classmethod def setUpClass(cls): @@ -35,6 +33,11 @@ def setUpClass(cls): cls.excel_file_path = sorted(excel_files)[-1] def test_excel_file_contents(self): + """Regression check for the Excel report's structure/terminology + migration: the legacy "Bundle Details" sheet no longer exists, a + "Conformance Details" sheet is present, and the Issue Summary, + Issue Details, and Rules Report sheets use the "CORE-ID" column + instead of the legacy "RULE-ID" column.""" # Check if the Excel file is created self.assertTrue( os.path.exists(self.excel_file_path), diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue357.py b/tests/QARegressionTests/test_Issues/test_CoreIssue357.py index e1f223509..5e5ece7d0 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue357.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue357.py @@ -1,12 +1,17 @@ import os import subprocess import unittest -import pytest from conftest import get_python_executable -@pytest.mark.regression class TerminalCommandTestCase(unittest.TestCase): + """Regression test for SEND rule 266 (variable not allowed in SDTM/SEND + model) run against a negative dataset. + + Note: this only asserts that a report file is produced, it does not check + the rule's actual findings/issue content. + """ + @classmethod def setUpClass(cls): # Run the command in the terminal @@ -19,7 +24,7 @@ def setUpClass(cls): "sdtmig", "-v", "3.4", - "-r", + "-lr", "tests/resources/CoreIssue357/SENDIG_266_rule.json", "-dp", "tests/resources/CoreIssue357/SENDIG_266_negative_testdata_datasets.json", @@ -36,6 +41,8 @@ def setUpClass(cls): cls.excel_file_path = sorted(excel_files)[-1] def test_command_execution(self): + """Test that validating against the local SEND-266 rule produces an + Excel report file.""" # Check if the Excel file is created self.assertTrue( os.path.exists(self.excel_file_path), diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue363.py b/tests/QARegressionTests/test_Issues/test_CoreIssue363.py index 8cc43263f..dcd668b6c 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue363.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue363.py @@ -5,9 +5,15 @@ import pytest -@pytest.mark.skip(reason="The test is obsolete and requires modernization.") +@pytest.mark.skip( + reason="Requires API key to run. Run Locally with valid API key to test." +) class TestCoreIssue363(unittest.TestCase): def test_post_request(self): + """Currently skipped (requires an API key). Hits the external Azure + "TestRule" rule-generation API directly over HTTP; not part of the + local CLI-based validation regression suite covered elsewhere in + this directory.""" # URL to send the POST request to url = ( "https://cdisc-library-conformance-rules-generator-dev.azurewebsites.net" diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue390.py b/tests/QARegressionTests/test_Issues/test_CoreIssue390.py index 5fd25cdd4..a0f16dcce 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue390.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue390.py @@ -5,17 +5,21 @@ from cdisc_rules_engine.services.define_xml.define_xml_reader_factory import ( DefineXMLReaderFactory, ) -import pytest -@pytest.mark.regression class TestCoreIssue390(unittest.TestCase): + # NOTE: This is a plain unit test of DefineXMLReaderFactory's version + # detection, not a CLI regression test, even though it lives in this folder. def test_get_define_version_from_define20(self): + """Verify a Define-XML v2.0 file is correctly identified as version + "2.0.0" by DefineXMLReaderFactory.get_define_version().""" path_to_define = os.path.join("tests", "resources", "test_defineV20-SDTM.xml") reader = DefineXMLReaderFactory.from_filename(path_to_define) self.assertEqual(reader.get_define_version(), "2.0.0") def test_get_define_version_from_define21(self): + """Verify a Define-XML v2.1 file is correctly identified as version + "2.1.0" by DefineXMLReaderFactory.get_define_version().""" path_to_define = os.path.join("tests", "resources", "test_defineV21-SDTM.xml") reader = DefineXMLReaderFactory.from_filename(path_to_define) self.assertEqual(reader.get_define_version(), "2.1.0") diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue397.py b/tests/QARegressionTests/test_Issues/test_CoreIssue397.py index 716bdd123..2eec0b52a 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue397.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue397.py @@ -1,24 +1,31 @@ import unittest import pandas as pd from cdisc_rules_engine.check_operators.helpers import flatten_list -import pytest -@pytest.mark.regression class TestFlattenList(unittest.TestCase): + # NOTE: These are plain unit tests of the flatten_list() helper, not CLI + # regression tests, even though they live in this folder. def test_flatten_list_with_array_column(self): + """Verify flatten_list() flattens a DataFrame column of list values + into a single flat list of scalars.""" data = pd.DataFrame({"ARRAY_COLUMN": [[1, 2, 3], [4, 5, 6], [7, 8, 9]]}) result = list(flatten_list(data, ["ARRAY_COLUMN"])) expected = [1, 2, 3] self.assertEqual(result, expected) def test_flatten_list_with_nonexistent_column(self): + """Verify flatten_list() falls back to yielding the column name + itself when the requested column is not present in the DataFrame.""" data = pd.DataFrame({"COLUMN_A": [1, 2, 3], "COLUMN_B": ["A", "B", "C"]}) result = list(flatten_list(data, ["COLUMN_C"])) expected = ["COLUMN_C"] self.assertEqual(result, expected) def test_flatten_list_with_mixed_columns(self): + """Verify flatten_list() handles a mix of array-valued and scalar + columns, flattening the array column while passing through the + scalar column's name unchanged.""" data = pd.DataFrame( { "ARRAY_COLUMN": [[1, 2, 3], [4, 5, 6], [7, 8, 9]], @@ -30,6 +37,8 @@ def test_flatten_list_with_mixed_columns(self): self.assertEqual(result, expected) def test_flatten_list_with_empty_dataframe(self): + """Verify flatten_list() degrades gracefully on an empty DataFrame, + yielding the requested column name instead of raising an error.""" empty_data = pd.DataFrame() result = list(flatten_list(empty_data, ["COLUMN"])) expected = ["COLUMN"] diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue576.py b/tests/QARegressionTests/test_Issues/test_CoreIssue576.py index 6aff31758..1a5a90914 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue576.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue576.py @@ -1,131 +1,133 @@ -import subprocess -import os -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - issue_datails_sheet, - dataset_details_sheet, - rules_report_sheet, - issue_sheet_coreid_column, - dataset_sheet_dataset_column, - rules_sheet_rule_status_column, -) - -""" These tests utilize positive and negative dataset -to validate successful working CG0202. Positive -dataset have supp datasets so rule should run -successfully. while Negative datasets have no -supp datasets so rule should be skipped. The -succesfuly running against postive dataset means -the scope skip problem is resolved """ - - -@pytest.mark.skip(reason="The test is obsolete and requires modernization.") -def test_negative_dataset(): - command = ( - f"{get_python_executable()} -m core test -s sdtmig -v 3.4 -r " - + os.path.join("tests", "resources", "CoreIssue576", "Rule_underscores.json") - + " -dp " - + os.path.join("tests", "resources", "CoreIssue576", "Datasets_Negative.json") - ) - - # Construct the command - command = command.split(" ") - - # Run the command in the terminal - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - - file_name = stdout.decode().strip().split(": ")[1] - # Open the Excel file - workbook = openpyxl.load_workbook(file_name) - - # Go to the "Issue Details" sheet - issues_sheet = workbook[issue_datails_sheet] - dataset_sheet = workbook[dataset_details_sheet] - rules_sheet = workbook[rules_report_sheet] - - coreid_column = issues_sheet[issue_sheet_coreid_column] - dataset_column = dataset_sheet[dataset_sheet_dataset_column] - rule_status_column = rules_sheet[rules_sheet_rule_status_column] - - coreid_values = [cell.value for cell in coreid_column[1:]] - dataset_values = [cell.value for cell in dataset_column[1:]] - rule_status_column_values = [cell.value for cell in rule_status_column[1:]] - - # Remove None values using list comprehension - coreid_values = [value for value in coreid_values if value is not None] - dataset_values = [value for value in dataset_values if value is not None] - rule_status_column_values = [ - value for value in rule_status_column_values if value is not None - ] - - # Perform the assertion - assert dataset_values[0] == "DM" - assert len(dataset_values) == 1 - - assert len(coreid_values) == 0 - - assert rule_status_column_values[0] == "SKIPPED" - - # Close the workbook - workbook.close() - - # Delete the file - os.remove(file_name) - - -@pytest.mark.skip(reason="The test is obsolete and requires modernization.") -def test_positive_dataset(): - command = ( - f"{get_python_executable()} -m core validate -s sdtmig -v 3.4 -r " - + os.path.join("tests", "resources", "CoreIssue576", "Rule_underscores.json") - + " -dp " - + os.path.join("tests", "resources", "CoreIssue576", "Datasets_positive.json") - ) - - # Construct the command - command = command.split(" ") - - # Run the command in the terminal - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - - file_name = stdout.decode().strip().split(": ")[1] - # Open the Excel file - workbook = openpyxl.load_workbook(file_name) - - # Go to the "Issue Details" sheet - issues_sheet = workbook[issue_datails_sheet] - dataset_sheet = workbook[dataset_details_sheet] - rules_sheet = workbook[rules_report_sheet] - - coreid_column = issues_sheet[issue_sheet_coreid_column] - dataset_column = dataset_sheet[dataset_sheet_dataset_column] - rule_status_column = rules_sheet[rules_sheet_rule_status_column] - - coreid_values = [cell.value for cell in coreid_column[1:]] - dataset_values = [cell.value for cell in dataset_column[1:]] - rule_status_column_values = [cell.value for cell in rule_status_column[1:]] - - # Remove None values using list comprehension - coreid_values = [value for value in coreid_values if value is not None] - dataset_values = [value for value in dataset_values if value is not None] - rule_status_column_values = [ - value for value in rule_status_column_values if value is not None - ] - - # Perform the assertion - assert dataset_values[0] == "SUPPAE" - assert dataset_values[1] == "SUPPEC" - - assert len(coreid_values) == 0 - - assert rule_status_column_values[0] == "SUCCESS" - - # Close the workbook - workbook.close() - - # Delete the file - os.remove(file_name) +import subprocess +import os +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + issue_datails_sheet, + dataset_details_sheet, + rules_report_sheet, + issue_sheet_coreid_column, + dataset_sheet_dataset_column, + rules_sheet_rule_status_column, +) + +""" These tests utilize positive and negative dataset +to validate successful working CG0202. Positive +dataset have supp datasets so rule should run +successfully. while Negative datasets have no +supp datasets so rule should be skipped. The +succesfuly running against postive dataset means +the scope skip problem is resolved """ + + +def test_negative_dataset(): + """Verify CG0202 is SKIPPED (0 issues) when the dataset contains no SUPP + domains — only DM is present, so the rule's scope has nothing to check.""" + command = ( + f"{get_python_executable()} -m core validate -s sdtmig -v 3.3 -lr " + + os.path.join("tests", "resources", "CoreIssue576", "Rule_underscores.json") + + " -dp " + + os.path.join("tests", "resources", "CoreIssue576", "Datasets_Negative.json") + ) + + # Construct the command + command = command.split(" ") + + # Run the command in the terminal + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + file_name = stdout.decode().strip().split(": ")[1] + # Open the Excel file + workbook = openpyxl.load_workbook(file_name) + + # Go to the "Issue Details" sheet + issues_sheet = workbook[issue_datails_sheet] + dataset_sheet = workbook[dataset_details_sheet] + rules_sheet = workbook[rules_report_sheet] + + coreid_column = issues_sheet[issue_sheet_coreid_column] + dataset_column = dataset_sheet[dataset_sheet_dataset_column] + rule_status_column = rules_sheet[rules_sheet_rule_status_column] + + coreid_values = [cell.value for cell in coreid_column[1:]] + dataset_values = [cell.value for cell in dataset_column[1:]] + rule_status_column_values = [cell.value for cell in rule_status_column[1:]] + + # Remove None values using list comprehension + coreid_values = [value for value in coreid_values if value is not None] + dataset_values = [value for value in dataset_values if value is not None] + rule_status_column_values = [ + value for value in rule_status_column_values if value is not None + ] + + # Perform the assertion + assert dataset_values[0] == "DM" + assert len(dataset_values) == 1 + + assert len(coreid_values) == 0 + + assert rule_status_column_values[0] == "SKIPPED" + + # Close the workbook + workbook.close() + + # Delete the file + os.remove(file_name) + + +def test_positive_dataset(): + """Verify CG0202 runs to SUCCESS (0 issues) when SUPPAE and SUPPEC are + present, confirming the fix for the scope-skip bug that previously + prevented the rule from evaluating datasets with SUPP domains.""" + command = ( + f"{get_python_executable()} -m core validate -s sdtmig -v 3.3 -lr " + + os.path.join("tests", "resources", "CoreIssue576", "Rule_underscores.json") + + " -dp " + + os.path.join("tests", "resources", "CoreIssue576", "Datasets_positive.json") + ) + + # Construct the command + command = command.split(" ") + + # Run the command in the terminal + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + file_name = stdout.decode().strip().split(": ")[1] + # Open the Excel file + workbook = openpyxl.load_workbook(file_name) + + # Go to the "Issue Details" sheet + issues_sheet = workbook[issue_datails_sheet] + dataset_sheet = workbook[dataset_details_sheet] + rules_sheet = workbook[rules_report_sheet] + + coreid_column = issues_sheet[issue_sheet_coreid_column] + dataset_column = dataset_sheet[dataset_sheet_dataset_column] + rule_status_column = rules_sheet[rules_sheet_rule_status_column] + + coreid_values = [cell.value for cell in coreid_column[1:]] + dataset_values = [cell.value for cell in dataset_column[1:]] + rule_status_column_values = [cell.value for cell in rule_status_column[1:]] + + # Remove None values using list comprehension + coreid_values = [value for value in coreid_values if value is not None] + dataset_values = [value for value in dataset_values if value is not None] + rule_status_column_values = [ + value for value in rule_status_column_values if value is not None + ] + + # Perform the assertion + assert dataset_values[0] == "SUPPAE" + assert dataset_values[1] == "SUPPEC" + + assert len(coreid_values) == 0 + + assert rule_status_column_values[0] == "SUCCESS" + + # Close the workbook + workbook.close() + + # Delete the file + os.remove(file_name) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue715.py b/tests/QARegressionTests/test_Issues/test_CoreIssue715.py index a69f3421b..fc0982367 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue715.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue715.py @@ -1,139 +1,143 @@ -import os -import subprocess -import unittest -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - issue_datails_sheet, - rules_report_sheet, - issue_sheet_record_column, - issue_sheet_variable_column, - issue_sheet_values_column, -) - - -@pytest.mark.regression -class TestCoreIssue715(unittest.TestCase): - def test_positive_dataset(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "4-0", - "-dp", - os.path.join( - "tests", "resources", "CoreIssue715", "CDISC_Pilot_Study.json" - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue715", "rule.yml"), - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # Go to the "Issue Details" sheet - sheet = workbook[issue_datails_sheet] - - record_column = sheet[issue_sheet_record_column] - variables_column = sheet[issue_sheet_variable_column] - values_column = sheet[issue_sheet_values_column] - - record_values = [cell.value for cell in record_column[1:]] - variables_values = [cell.value for cell in variables_column[1:]] - values_column_values = [cell.value for cell in values_column[1:]] - - # Remove None values using list comprehension - record_values = [value for value in record_values if value is not None] - variables_values = [value for value in variables_values if value is not None] - values_column_values = [ - value for value in values_column_values if value is not None - ] - rules_values = [ - row for row in workbook[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - # Perform the assertion - assert rules_values[0][0] == "DDF00081" - assert "SUCCESS" in rules_values[0] - assert len(record_values) == 0 - assert len(variables_values) == 0 - assert len(values_column_values) == 0 - if os.path.exists(excel_file_path): - os.remove(excel_file_path) - - def test_negative_dataset(self): - # Run validation for invalid JSON - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "4-0", - "-dp", - os.path.join( - "tests", "resources", "CoreIssue715", "CDISC_Pilot_Study_Invalid.json" - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue715", "rule.yml"), - ] - subprocess.run(command, check=True) - - files = os.listdir() - excel_files = [ - f for f in files if f.startswith("CORE-Report-") and f.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - workbook = openpyxl.load_workbook(excel_file_path) - - # Issue Summary basic checks - issue_summary_sheet = workbook["Issue Summary"] - summary_values = [r for r in issue_summary_sheet.iter_rows(values_only=True)][ - 1: - ] - summary_values = [r for r in summary_values if any(r)] - assert summary_values and summary_values[0][1] == "DDF00081" - assert summary_values[0][4] == 1 - - # Issue Details strict checks: now expect one row per error - issue_details_sheet = workbook[issue_datails_sheet] - details_rows = [r for r in issue_details_sheet.iter_rows(values_only=True)][1:] - details_rows = [r for r in details_rows if any(r)] - # Expect exactly 15 rows - assert len(details_rows) == 15 - - # Expected exact strings - for row in details_rows: - assert row[0] == "DDF00081" - assert ( - row[2] - == "The class relationship does not conform with the USDM schema." - ) - assert row[7] == ("json_path, message") - - # Rules Report - rules_rows = [ - r for r in workbook[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_rows = [r for r in rules_rows if any(r)] - assert rules_rows and rules_rows[0][0] == "DDF00081" - assert "ISSUE REPORTED" in rules_rows[0] - if os.path.exists(excel_file_path): - os.remove(excel_file_path) +import os +import subprocess +import unittest +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + issue_datails_sheet, + rules_report_sheet, + issue_sheet_record_column, + issue_sheet_variable_column, + issue_sheet_values_column, +) + + +class TestCoreIssue715(unittest.TestCase): + def test_positive_dataset(self): + """Verify the USDM DDF00081 class-relationship schema-conformance + rule reports SUCCESS (0 issues) against a valid CDISC Pilot Study + USDM JSON dataset.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "4-0", + "-dp", + os.path.join( + "tests", "resources", "CoreIssue715", "CDISC_Pilot_Study.json" + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue715", "rule.yml"), + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # Go to the "Issue Details" sheet + sheet = workbook[issue_datails_sheet] + + record_column = sheet[issue_sheet_record_column] + variables_column = sheet[issue_sheet_variable_column] + values_column = sheet[issue_sheet_values_column] + + record_values = [cell.value for cell in record_column[1:]] + variables_values = [cell.value for cell in variables_column[1:]] + values_column_values = [cell.value for cell in values_column[1:]] + + # Remove None values using list comprehension + record_values = [value for value in record_values if value is not None] + variables_values = [value for value in variables_values if value is not None] + values_column_values = [ + value for value in values_column_values if value is not None + ] + rules_values = [ + row for row in workbook[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + # Perform the assertion + assert rules_values[0][0] == "DDF00081" + assert "SUCCESS" in rules_values[0] + assert len(record_values) == 0 + assert len(variables_values) == 0 + assert len(values_column_values) == 0 + if os.path.exists(excel_file_path): + os.remove(excel_file_path) + + def test_negative_dataset(self): + """Verify the same DDF00081 rule flags exactly 15 issue rows, each + reporting "class relationship does not conform", against an + intentionally invalid USDM JSON dataset.""" + # Run validation for invalid JSON + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "4-0", + "-dp", + os.path.join( + "tests", "resources", "CoreIssue715", "CDISC_Pilot_Study_Invalid.json" + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue715", "rule.yml"), + ] + subprocess.run(command, check=True) + + files = os.listdir() + excel_files = [ + f for f in files if f.startswith("CORE-Report-") and f.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + workbook = openpyxl.load_workbook(excel_file_path) + + # Issue Summary basic checks + issue_summary_sheet = workbook["Issue Summary"] + summary_values = [r for r in issue_summary_sheet.iter_rows(values_only=True)][ + 1: + ] + summary_values = [r for r in summary_values if any(r)] + assert summary_values and summary_values[0][1] == "DDF00081" + assert summary_values[0][4] == 1 + + # Issue Details strict checks: now expect one row per error + issue_details_sheet = workbook[issue_datails_sheet] + details_rows = [r for r in issue_details_sheet.iter_rows(values_only=True)][1:] + details_rows = [r for r in details_rows if any(r)] + # Expect exactly 15 rows + assert len(details_rows) == 15 + + # Expected exact strings + for row in details_rows: + assert row[0] == "DDF00081" + assert ( + row[2] + == "The class relationship does not conform with the USDM schema." + ) + assert row[7] == ("json_path, message") + + # Rules Report + rules_rows = [ + r for r in workbook[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_rows = [r for r in rules_rows if any(r)] + assert rules_rows and rules_rows[0][0] == "DDF00081" + assert "ISSUE REPORTED" in rules_rows[0] + if os.path.exists(excel_file_path): + os.remove(excel_file_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue720.py b/tests/QARegressionTests/test_Issues/test_CoreIssue720.py index a00aa158a..a655deb8d 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue720.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue720.py @@ -1,149 +1,149 @@ -import os -import subprocess -import unittest - -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - issue_datails_sheet, - rules_report_sheet, -) - - -@pytest.mark.skip(reason="The test is obsolete and requires modernization.") -class TestCoreIssue720(unittest.TestCase): - def test_negative_dataset(self): - """Negative scenario: SPECIES missing -> expect one populated issue.""" - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "4-0", - "-dp", - os.path.join("tests", "resources", "CoreIssue720", "Invalid_datasets.json"), - "-lr", - os.path.join("tests", "resources", "CoreIssue720", "Rule.yml"), - ] - subprocess.run(command, check=True) - - excel_files = [ - f - for f in os.listdir() - if f.startswith("CORE-Report-") and f.endswith(".xlsx") - ] - assert excel_files, "No CORE report generated" - excel_file_path = sorted(excel_files)[-1] - wb = openpyxl.load_workbook(excel_file_path) - - # Conformance Details - assert "Conformance Details" in wb.sheetnames - conf_rows = [ - row for row in wb["Conformance Details"].iter_rows(values_only=True) - ] - assert conf_rows[6][0] == "Standard" - assert conf_rows[6][1] == "USDM" - assert conf_rows[7][0] == "Version" - assert conf_rows[7][1] == "V4.0" - - # Entity Details - entity_rows = [row for row in wb["Entity Details"].iter_rows(values_only=True)][ - 1: - ] - entity_rows = [r for r in entity_rows if any(r)] - assert len(entity_rows) >= 2 - assert entity_rows[0][0] == "DM" and entity_rows[0][1] == 4 - assert entity_rows[1][0] == "TS" and entity_rows[1][1] == 4 - - # Issue Summary - summary_rows = [row for row in wb["Issue Summary"].iter_rows(values_only=True)][ - 1: - ] - summary_rows = [r for r in summary_rows if any(r)] - assert summary_rows[0][0] == "dm.xpt" - assert summary_rows[0][1] == "CDISC.SENDIG.105" - assert summary_rows[0][4] == 1 - - # Issue Details - details_rows = [ - row for row in wb[issue_datails_sheet].iter_rows(values_only=True) - ][1:] - details_rows = [r for r in details_rows if any(r)] - assert details_rows, "Issue Details should have at least one populated row" - first_issue = details_rows[0] - assert first_issue[0] == "CDISC.SENDIG.105" - assert first_issue[1] == "SEND105" - assert "SPECIES in not present" in first_issue[2] - assert first_issue[3] == "fully executable" - assert first_issue[4] == "dm.xpt" - assert first_issue[7] == "$distinct_tsparmcd, $distinct_txparmcd, SPECIES" - assert "Not in dataset" in first_issue[8] - - # Rules Report - rules_rows = [ - row for row in wb[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_rows = [r for r in rules_rows if any(r)] - assert rules_rows, "Rules Report must have at least one populated row" - assert rules_rows[0][0] == "CDISC.SENDIG.105" - assert rules_rows[0][-1] == "SUCCESS" - - if os.path.exists(excel_file_path): - os.remove(excel_file_path) - - def test_positive_dataset(self): - """Positive scenario: SPECIES present -> expect no issue rows.""" - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "4-0", - "-dp", - os.path.join("tests", "resources", "CoreIssue720", "Valid_datasets.json"), - "-lr", - os.path.join("tests", "resources", "CoreIssue720", "Rule.yml"), - ] - subprocess.run(command, check=True) - - excel_files = [ - f - for f in os.listdir() - if f.startswith("CORE-Report-") and f.endswith(".xlsx") - ] - assert excel_files, "No CORE report generated" - excel_file_path = sorted(excel_files)[-1] - wb = openpyxl.load_workbook(excel_file_path) - - # Issue Summary empty - summary_rows = [row for row in wb["Issue Summary"].iter_rows(values_only=True)][ - 1: - ] - summary_rows = [r for r in summary_rows if any(r)] - assert summary_rows == [] - - # Issue Details empty - details_rows = [ - row for row in wb[issue_datails_sheet].iter_rows(values_only=True) - ][1:] - details_rows = [r for r in details_rows if any(r)] - assert details_rows == [] - - # Rules Report has success row - rules_rows = [ - row for row in wb[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_rows = [r for r in rules_rows if any(r)] - assert len(rules_rows) == 1 - assert rules_rows[0][0] == "CDISC.SENDIG.105" - assert rules_rows[0][-1] == "SUCCESS" - - if os.path.exists(excel_file_path): - os.remove(excel_file_path) +import os +import subprocess +import unittest + +import openpyxl +import pytest +from conftest import get_python_executable +from QARegressionTests.globals import ( + issue_datails_sheet, + rules_report_sheet, +) + + +@pytest.mark.skip(reason="The test is obsolete and requires modernization.") +class TestCoreIssue720(unittest.TestCase): + def test_negative_dataset(self): + """Negative scenario: SPECIES missing -> expect one populated issue.""" + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "4-0", + "-dp", + os.path.join("tests", "resources", "CoreIssue720", "Invalid_datasets.json"), + "-lr", + os.path.join("tests", "resources", "CoreIssue720", "Rule.yml"), + ] + subprocess.run(command, check=True) + + excel_files = [ + f + for f in os.listdir() + if f.startswith("CORE-Report-") and f.endswith(".xlsx") + ] + assert excel_files, "No CORE report generated" + excel_file_path = sorted(excel_files)[-1] + wb = openpyxl.load_workbook(excel_file_path) + + # Conformance Details + assert "Conformance Details" in wb.sheetnames + conf_rows = [ + row for row in wb["Conformance Details"].iter_rows(values_only=True) + ] + assert conf_rows[6][0] == "Standard" + assert conf_rows[6][1] == "USDM" + assert conf_rows[7][0] == "Version" + assert conf_rows[7][1] == "V4.0" + + # Entity Details + entity_rows = [row for row in wb["Entity Details"].iter_rows(values_only=True)][ + 1: + ] + entity_rows = [r for r in entity_rows if any(r)] + assert len(entity_rows) >= 2 + assert entity_rows[0][0] == "DM" and entity_rows[0][1] == 4 + assert entity_rows[1][0] == "TS" and entity_rows[1][1] == 4 + + # Issue Summary + summary_rows = [row for row in wb["Issue Summary"].iter_rows(values_only=True)][ + 1: + ] + summary_rows = [r for r in summary_rows if any(r)] + assert summary_rows[0][0] == "dm.xpt" + assert summary_rows[0][1] == "CDISC.SENDIG.105" + assert summary_rows[0][4] == 1 + + # Issue Details + details_rows = [ + row for row in wb[issue_datails_sheet].iter_rows(values_only=True) + ][1:] + details_rows = [r for r in details_rows if any(r)] + assert details_rows, "Issue Details should have at least one populated row" + first_issue = details_rows[0] + assert first_issue[0] == "CDISC.SENDIG.105" + assert first_issue[1] == "SEND105" + assert "SPECIES in not present" in first_issue[2] + assert first_issue[3] == "fully executable" + assert first_issue[4] == "dm.xpt" + assert first_issue[7] == "$distinct_tsparmcd, $distinct_txparmcd, SPECIES" + assert "Not in dataset" in first_issue[8] + + # Rules Report + rules_rows = [ + row for row in wb[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_rows = [r for r in rules_rows if any(r)] + assert rules_rows, "Rules Report must have at least one populated row" + assert rules_rows[0][0] == "CDISC.SENDIG.105" + assert rules_rows[0][-1] == "SUCCESS" + + if os.path.exists(excel_file_path): + os.remove(excel_file_path) + + def test_positive_dataset(self): + """Positive scenario: SPECIES present -> expect no issue rows.""" + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "4-0", + "-dp", + os.path.join("tests", "resources", "CoreIssue720", "Valid_datasets.json"), + "-lr", + os.path.join("tests", "resources", "CoreIssue720", "Rule.yml"), + ] + subprocess.run(command, check=True) + + excel_files = [ + f + for f in os.listdir() + if f.startswith("CORE-Report-") and f.endswith(".xlsx") + ] + assert excel_files, "No CORE report generated" + excel_file_path = sorted(excel_files)[-1] + wb = openpyxl.load_workbook(excel_file_path) + + # Issue Summary empty + summary_rows = [row for row in wb["Issue Summary"].iter_rows(values_only=True)][ + 1: + ] + summary_rows = [r for r in summary_rows if any(r)] + assert summary_rows == [] + + # Issue Details empty + details_rows = [ + row for row in wb[issue_datails_sheet].iter_rows(values_only=True) + ][1:] + details_rows = [r for r in details_rows if any(r)] + assert details_rows == [] + + # Rules Report has success row + rules_rows = [ + row for row in wb[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_rows = [r for r in rules_rows if any(r)] + assert len(rules_rows) == 1 + assert rules_rows[0][0] == "CDISC.SENDIG.105" + assert rules_rows[0][-1] == "SUCCESS" + + if os.path.exists(excel_file_path): + os.remove(excel_file_path) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue747.py b/tests/QARegressionTests/test_Issues/test_CoreIssue747.py index 241d26391..24baa6e3e 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue747.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue747.py @@ -1,80 +1,82 @@ -import subprocess -import os -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - issue_datails_sheet, - dataset_details_sheet, - rules_report_sheet, - issue_sheet_coreid_column, - dataset_sheet_dataset_column, - rules_sheet_rule_status_column, -) - -""" These tests utilizes a dataset -to validate successful working CG0019. The dataset -have both positive and negative cases, -including the supp and split domains""" - - -@pytest.mark.skip(reason="The test is obsolete and requires modernization.") -def test_CG0019(): - command = ( - f"{get_python_executable()} -m core test -s sdtmig -v 3.4 -r " - + os.path.join("tests", "resources", "CoreIssue747", "Rule_underscores.json") - + " -dp " - + os.path.join("tests", "resources", "CoreIssue747", "Datasets.json") - + " -dxp " - + os.path.join( - "tests", "resources", "CoreIssue747", "define_CG0019_split_and_supp.xml" - ) - ) - - # Construct the command - command = command.split(" ") - - # Run the command in the terminal - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = process.communicate() - - file_name = stdout.decode().strip().split(": ")[1] - # Open the Excel file - workbook = openpyxl.load_workbook(file_name) - - # Go to the "Issue Details" sheet - issues_sheet = workbook[issue_datails_sheet] - dataset_sheet = workbook[dataset_details_sheet] - rules_sheet = workbook[rules_report_sheet] - - coreid_column = issues_sheet[issue_sheet_coreid_column] - dataset_column = dataset_sheet[dataset_sheet_dataset_column] - rule_status_column = rules_sheet[rules_sheet_rule_status_column] - - coreid_values = [cell.value for cell in coreid_column[1:]] - dataset_values = [cell.value for cell in dataset_column[1:]] - rule_status_column_values = [cell.value for cell in rule_status_column[1:]] - - # Remove None values using list comprehension - coreid_values = [value for value in coreid_values if value is not None] - dataset_values = [value for value in dataset_values if value is not None] - rule_status_column_values = [ - value for value in rule_status_column_values if value is not None - ] - - # Perform the assertion - assert process.returncode == 0, f"Process failed with error: {stderr.decode()}" - assert dataset_values[0] == "ECAA" - assert dataset_values[1] == "ECBB" - assert dataset_values[2] == "SUPPEC" - assert len(dataset_values) == 3 - - assert len(coreid_values) == 4 - - assert rule_status_column_values[0] == "SUCCESS" - - # Close the workbook - workbook.close() - - # Delete the file - os.remove(file_name) +import subprocess +import os +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + issue_datails_sheet, + dataset_details_sheet, + rules_report_sheet, + issue_sheet_coreid_column, + dataset_sheet_dataset_column, + rules_sheet_rule_status_column, +) + +""" These tests utilizes a dataset +to validate successful working CG0019. The dataset +have both positive and negative cases, +including the supp and split domains""" + + +def test_CG0019(): + """Verify CG0019 reports 4 issues across split EC domains (ECAA, ECBB) + plus SUPPEC, using a Define-XML that describes the split/supp structure, + confirming the rule correctly evaluates split and supplemental datasets + together.""" + command = ( + f"{get_python_executable()} -m core validate -s sdtmig -v 3.4 -lr " + + os.path.join("tests", "resources", "CoreIssue747", "Rule_underscores.json") + + " -dp " + + os.path.join("tests", "resources", "CoreIssue747", "Datasets.json") + + " -dxp " + + os.path.join( + "tests", "resources", "CoreIssue747", "define_CG0019_split_and_supp.xml" + ) + ) + + # Construct the command + command = command.split(" ") + + # Run the command in the terminal + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = process.communicate() + + file_name = stdout.decode().strip().split(": ")[1] + # Open the Excel file + workbook = openpyxl.load_workbook(file_name) + + # Go to the "Issue Details" sheet + issues_sheet = workbook[issue_datails_sheet] + dataset_sheet = workbook[dataset_details_sheet] + rules_sheet = workbook[rules_report_sheet] + + coreid_column = issues_sheet[issue_sheet_coreid_column] + dataset_column = dataset_sheet[dataset_sheet_dataset_column] + rule_status_column = rules_sheet[rules_sheet_rule_status_column] + + coreid_values = [cell.value for cell in coreid_column[1:]] + dataset_values = [cell.value for cell in dataset_column[1:]] + rule_status_column_values = [cell.value for cell in rule_status_column[1:]] + + # Remove None values using list comprehension + coreid_values = [value for value in coreid_values if value is not None] + dataset_values = [value for value in dataset_values if value is not None] + rule_status_column_values = [ + value for value in rule_status_column_values if value is not None + ] + + # Perform the assertion + assert process.returncode == 0, f"Process failed with error: {stderr.decode()}" + assert dataset_values[0] == "ECAA" + assert dataset_values[1] == "ECBB" + assert dataset_values[2] == "SUPPEC" + assert len(dataset_values) == 3 + + assert len(coreid_values) == 4 + + assert rule_status_column_values[0] == "ISSUE REPORTED" + + # Close the workbook + workbook.close() + + # Delete the file + os.remove(file_name) diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue890.py b/tests/QARegressionTests/test_Issues/test_CoreIssue890.py index 9e8f8ce27..578b3586f 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue890.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue890.py @@ -1,162 +1,166 @@ -import os -import subprocess -import unittest -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - issue_datails_sheet, - rules_report_sheet, - issue_sheet_record_column, - issue_sheet_variable_column, - issue_sheet_values_column, -) - - -@pytest.mark.skip(reason="The test is obsolete and requires modernization.") -class TestColumnConsistsOfDelimitedCodelists(unittest.TestCase): - def test_positive_dataset(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "send", - "-v", - "1-0", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue890", - "unit-test-coreid-SENDIG282-positive.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue890", "Rule.yml"), - "-ct", - "sendct-2025-09-26", - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # Go to the "Issue Details" sheet - sheet = workbook[issue_datails_sheet] - - record_column = sheet[issue_sheet_record_column] - variables_column = sheet[issue_sheet_variable_column] - values_column = sheet[issue_sheet_values_column] - - record_values = [cell.value for cell in record_column[1:]] - variables_values = [cell.value for cell in variables_column[1:]] - values_column_values = [cell.value for cell in values_column[1:]] - - # Remove None values using list comprehension - record_values = [value for value in record_values if value is not None] - variables_values = [value for value in variables_values if value is not None] - values_column_values = [ - value for value in values_column_values if value is not None - ] - rules_values = [ - row for row in workbook[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - # Perform the assertion - # Ensure only two negative values are caught - assert rules_values[0][0] == "CDISC.SENDIG.SEND282" - assert len(record_values) == 0 - assert len(variables_values) == 0 - assert len(values_column_values) == 0 - if os.path.exists(excel_file_path): - os.remove(excel_file_path) - - def test_negaive_dataset(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "send", - "-v", - "1-0", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue890", - "unit-test-coreid-SENDIG282-negative.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue890", "Rule.yml"), - "-ct", - "sendct-2025-09-26", - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # --- Dataset Details --- - dataset_sheet = workbook["Dataset Details"] - dataset_values = [row for row in dataset_sheet.iter_rows(values_only=True)][1:] - dataset_values = [row for row in dataset_values if any(row)] - assert len(dataset_values) > 0 - assert dataset_values[0][0] == "pp.xpt" - assert dataset_values[0][1] == "Pharmacokinetics Parameters" - assert dataset_values[0][-1] == 4 - - # --- Issue Summary --- - issue_summary_sheet = workbook["Issue Summary"] - summary_values = [ - row for row in issue_summary_sheet.iter_rows(values_only=True) - ][1:] - summary_values = [row for row in summary_values if any(row)] - assert len(summary_values) > 0 - assert summary_values[0][0] == "pp.xpt" - assert summary_values[0][1] == "CDISC.SENDIG.SEND282" - assert summary_values[0][3] == 2 - - # --- Issue Details --- - issue_details_sheet = workbook["Issue Details"] - details_values = [ - row for row in issue_details_sheet.iter_rows(values_only=True) - ][1:] - details_values = [row for row in details_values if any(row)] - assert all(row[0] == "CDISC.SENDIG.SEND282" for row in details_values) - assert len(details_values) == 2 - - # --- Rules Report --- - rules_values = [ - row for row in workbook["Rules Report"].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - assert len(rules_values) > 0 - assert rules_values[0][0] == "CDISC.SENDIG.SEND282" - - if os.path.exists(excel_file_path): - os.remove(excel_file_path) - - -# if __name__ == "__main__": -# unittest.main() +import os +import subprocess +import unittest +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + issue_datails_sheet, + rules_report_sheet, + issue_sheet_record_column, + issue_sheet_variable_column, + issue_sheet_values_column, +) + + +class TestColumnConsistsOfDelimitedCodelists(unittest.TestCase): + def test_positive_dataset(self): + """Verify the SENDIG SEND282 delimited-codelist rule reports SUCCESS + (0 issues) against a positive dataset when validated with an explicit + CT package (sendct-2025-09-26).""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sendig", + "-v", + "3-1", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue890", + "unit-test-coreid-SENDIG282-positive.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue890", "Rule.yml"), + "-ct", + "sendct-2025-09-26", + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # Go to the "Issue Details" sheet + sheet = workbook[issue_datails_sheet] + + record_column = sheet[issue_sheet_record_column] + variables_column = sheet[issue_sheet_variable_column] + values_column = sheet[issue_sheet_values_column] + + record_values = [cell.value for cell in record_column[1:]] + variables_values = [cell.value for cell in variables_column[1:]] + values_column_values = [cell.value for cell in values_column[1:]] + + # Remove None values using list comprehension + record_values = [value for value in record_values if value is not None] + variables_values = [value for value in variables_values if value is not None] + values_column_values = [ + value for value in values_column_values if value is not None + ] + rules_values = [ + row for row in workbook[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + # Perform the assertion + # Ensure only two negative values are caught + assert rules_values[0][0] == "CDISC.SENDIG.SEND282" + assert len(record_values) == 0 + assert len(variables_values) == 0 + assert len(values_column_values) == 0 + if os.path.exists(excel_file_path): + os.remove(excel_file_path) + + def test_negative_dataset(self): + """Verify the same SEND282 rule flags 2 issues on the PP domain + (4 records) when validated against a negative dataset with the same + explicit CT package.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sendig", + "-v", + "3-1", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue890", + "unit-test-coreid-SENDIG282-negative.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue890", "Rule.yml"), + "-ct", + "sendct-2025-09-26", + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # --- Dataset Details --- + dataset_sheet = workbook["Dataset Details"] + dataset_values = [row for row in dataset_sheet.iter_rows(values_only=True)][1:] + dataset_values = [row for row in dataset_values if any(row)] + assert len(dataset_values) > 0 + assert dataset_values[0][0] == "PP" + assert dataset_values[0][1] == "Pharmacokinetics Parameters" + assert dataset_values[0][-1] == 4 + + # --- Issue Summary --- + issue_summary_sheet = workbook["Issue Summary"] + summary_values = [ + row for row in issue_summary_sheet.iter_rows(values_only=True) + ][1:] + summary_values = [row for row in summary_values if any(row)] + assert len(summary_values) > 0 + assert summary_values[0][0] == "PP" + assert summary_values[0][1] == "CDISC.SENDIG.SEND282" + assert summary_values[0][3] == 2 + + # --- Issue Details --- + issue_details_sheet = workbook["Issue Details"] + details_values = [ + row for row in issue_details_sheet.iter_rows(values_only=True) + ][1:] + details_values = [row for row in details_values if any(row)] + assert all(row[0] == "CDISC.SENDIG.SEND282" for row in details_values) + assert len(details_values) == 2 + + # --- Rules Report --- + rules_values = [ + row for row in workbook["Rules Report"].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + assert len(rules_values) > 0 + assert rules_values[0][0] == "CDISC.SENDIG.SEND282" + + if os.path.exists(excel_file_path): + os.remove(excel_file_path) + + +# if __name__ == "__main__": +# unittest.main() diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue897.py b/tests/QARegressionTests/test_Issues/test_CoreIssue897.py index 67758617e..150f2f08f 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue897.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue897.py @@ -1,159 +1,162 @@ -import os -import subprocess -import unittest -import openpyxl -import pytest -from conftest import get_python_executable -from QARegressionTests.globals import ( - entity_details_sheet, - issue_datails_sheet, - rules_report_sheet, - issue_sheet_record_column, - issue_sheet_variable_column, - issue_sheet_values_column, -) - - -@pytest.mark.regression -class TestGetXHTMLErrors(unittest.TestCase): - def test_positive_dataset(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "4-0", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue897", - "Positive_datasets.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue897", "Rule.yml"), - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # Go to the "Issue Details" sheet - sheet = workbook[issue_datails_sheet] - - record_column = sheet[issue_sheet_record_column] - variables_column = sheet[issue_sheet_variable_column] - values_column = sheet[issue_sheet_values_column] - - record_values = [cell.value for cell in record_column[1:]] - variables_values = [cell.value for cell in variables_column[1:]] - values_column_values = [cell.value for cell in values_column[1:]] - - # Remove None values using list comprehension - record_values = [value for value in record_values if value is not None] - variables_values = [value for value in variables_values if value is not None] - values_column_values = [ - value for value in values_column_values if value is not None - ] - rules_values = [ - row for row in workbook[rules_report_sheet].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - # Perform the assertion - # Ensure only two negative values are caught - assert rules_values[0][0] == "CORE-000409" - assert rules_values[0][-1] == "SUCCESS" - assert len(record_values) == 0 - assert len(variables_values) == 0 - assert len(values_column_values) == 0 - if os.path.exists(excel_file_path): - os.remove(excel_file_path) - - def test_negative_dataset(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "usdm", - "-v", - "4-0", - "-dp", - os.path.join( - "tests", - "resources", - "CoreIssue897", - "Negative_datasets.json", - ), - "-lr", - os.path.join("tests", "resources", "CoreIssue897", "Rule.yml"), - ] - subprocess.run(command, check=True) - - # Get the latest created Excel file - files = os.listdir() - excel_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".xlsx") - ] - excel_file_path = sorted(excel_files)[-1] - # Open the Excel file - workbook = openpyxl.load_workbook(excel_file_path) - - # --- Dataset Details --- - dataset_sheet = workbook[entity_details_sheet] - dataset_values = [row for row in dataset_sheet.iter_rows(values_only=True)][1:] - dataset_values = [row for row in dataset_values if any(row)] - assert len(dataset_values) > 0 - assert dataset_values[0][0] == "NarrativeContentItem" - assert dataset_values[0][-1] == 170 - - # --- Issue Summary --- - issue_summary_sheet = workbook["Issue Summary"] - summary_values = [ - row for row in issue_summary_sheet.iter_rows(values_only=True) - ][1:] - summary_values = [row for row in summary_values if any(row)] - assert len(summary_values) > 0 - assert summary_values[0][1] == "CORE-000409" - assert summary_values[0][4] == 4 - - # --- Issue Details --- - issue_details_sheet = workbook["Issue Details"] - details_values = [ - row for row in issue_details_sheet.iter_rows(values_only=True) - ][1:] - details_values = [row for row in details_values if any(row)] - assert all(row[0] == "CORE-000409" for row in details_values) - assert len(details_values) == 4 - - # --- Rules Report --- - rules_values = [ - row for row in workbook["Rules Report"].iter_rows(values_only=True) - ][1:] - rules_values = [row for row in rules_values if any(row)] - assert len(rules_values) > 0 - assert rules_values[0][0] == "CORE-000409" - assert rules_values[0][-1] == "ISSUE REPORTED" - - if os.path.exists(excel_file_path): - os.remove(excel_file_path) - - -# if __name__ == "__main__": -# unittest.main() +import os +import subprocess +import unittest +import openpyxl +from conftest import get_python_executable +from QARegressionTests.globals import ( + entity_details_sheet, + issue_datails_sheet, + rules_report_sheet, + issue_sheet_record_column, + issue_sheet_variable_column, + issue_sheet_values_column, +) + + +class TestGetXHTMLErrors(unittest.TestCase): + def test_positive_dataset(self): + """Verify the get_xhtml_errors operator (DDF00187) reports SUCCESS + (0 issues) when narrative content is valid, well-formed XHTML.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "4-0", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue897", + "Positive_datasets.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue897", "Rule.yml"), + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # Go to the "Issue Details" sheet + sheet = workbook[issue_datails_sheet] + + record_column = sheet[issue_sheet_record_column] + variables_column = sheet[issue_sheet_variable_column] + values_column = sheet[issue_sheet_values_column] + + record_values = [cell.value for cell in record_column[1:]] + variables_values = [cell.value for cell in variables_column[1:]] + values_column_values = [cell.value for cell in values_column[1:]] + + # Remove None values using list comprehension + record_values = [value for value in record_values if value is not None] + variables_values = [value for value in variables_values if value is not None] + values_column_values = [ + value for value in values_column_values if value is not None + ] + rules_values = [ + row for row in workbook[rules_report_sheet].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + # Perform the assertion + # Ensure only two negative values are caught + assert rules_values[0][0] == "CORE-000409" + assert rules_values[0][-1] == "SUCCESS" + assert len(record_values) == 0 + assert len(variables_values) == 0 + assert len(values_column_values) == 0 + if os.path.exists(excel_file_path): + os.remove(excel_file_path) + + def test_negative_dataset(self): + """Verify the same rule flags 4 issues on NarrativeContentItem + (out of 170 total entities) when the narrative content contains + malformed/non-conformant XHTML.""" + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "usdm", + "-v", + "4-0", + "-dp", + os.path.join( + "tests", + "resources", + "CoreIssue897", + "Negative_datasets.json", + ), + "-lr", + os.path.join("tests", "resources", "CoreIssue897", "Rule.yml"), + ] + subprocess.run(command, check=True) + + # Get the latest created Excel file + files = os.listdir() + excel_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".xlsx") + ] + excel_file_path = sorted(excel_files)[-1] + # Open the Excel file + workbook = openpyxl.load_workbook(excel_file_path) + + # --- Dataset Details --- + dataset_sheet = workbook[entity_details_sheet] + dataset_values = [row for row in dataset_sheet.iter_rows(values_only=True)][1:] + dataset_values = [row for row in dataset_values if any(row)] + assert len(dataset_values) > 0 + assert dataset_values[0][0] == "NarrativeContentItem" + assert dataset_values[0][-1] == 170 + + # --- Issue Summary --- + issue_summary_sheet = workbook["Issue Summary"] + summary_values = [ + row for row in issue_summary_sheet.iter_rows(values_only=True) + ][1:] + summary_values = [row for row in summary_values if any(row)] + assert len(summary_values) > 0 + assert summary_values[0][1] == "CORE-000409" + assert summary_values[0][4] == 4 + + # --- Issue Details --- + issue_details_sheet = workbook["Issue Details"] + details_values = [ + row for row in issue_details_sheet.iter_rows(values_only=True) + ][1:] + details_values = [row for row in details_values if any(row)] + assert all(row[0] == "CORE-000409" for row in details_values) + assert len(details_values) == 4 + + # --- Rules Report --- + rules_values = [ + row for row in workbook["Rules Report"].iter_rows(values_only=True) + ][1:] + rules_values = [row for row in rules_values if any(row)] + assert len(rules_values) > 0 + assert rules_values[0][0] == "CORE-000409" + assert rules_values[0][-1] == "ISSUE REPORTED" + + if os.path.exists(excel_file_path): + os.remove(excel_file_path) + + +# if __name__ == "__main__": +# unittest.main() diff --git a/tests/QARegressionTests/test_Issues/test_CoreIssue984.py b/tests/QARegressionTests/test_Issues/test_CoreIssue984.py index 1bdda31f0..7224b084a 100644 --- a/tests/QARegressionTests/test_Issues/test_CoreIssue984.py +++ b/tests/QARegressionTests/test_Issues/test_CoreIssue984.py @@ -1,67 +1,68 @@ -import os -import subprocess - -import pytest -import json -from conftest import get_python_executable - - -@pytest.mark.regression -class TestCoreIssue984: - def test_define_subversion_ignored(self): - # Run the command in the terminal - command = [ - f"{get_python_executable()}", - "-m", - "core", - "validate", - "-s", - "sdtmig", - "-r", - "CORE-000007", - "-v", - "3.4", - "-dp", - os.path.join( - "tests", - "resources", - "test_dataset.json", - ), - "-dxp", - os.path.join("tests", "resources", "CoreIssue984", "define.xml"), - "--output-format", - "json", - "-ps", - "1", - ] - subprocess.run(command, check=True) - - files = os.listdir() - json_files = [ - file - for file in files - if file.startswith("CORE-Report-") and file.endswith(".json") - ] - json_report_path = sorted(json_files)[-1] - # Open the JSON report file - json_report = json.load(open(json_report_path)) - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - - assert { - "Conformance_Details", - "Dataset_Details", - "Issue_Summary", - "Issue_Details", - "Rules_Report", - }.issubset(json_report.keys()) - assert json_report["Conformance_Details"]["Standard"] == "SDTMIG" - assert json_report["Conformance_Details"]["Define_XML_Version"] == "2.1.5" - - if os.path.exists(json_report_path): - os.remove(json_report_path) +import os +import subprocess + +import json +from conftest import get_python_executable + + +class TestCoreIssue984: + def test_define_subversion_ignored(self): + """Test that a Define-XML sub-version string doesn't break standard/ + version matching (Conformance_Details still reports SDTMIG / 2.1.5). + """ + # Run the command in the terminal + command = [ + f"{get_python_executable()}", + "-m", + "core", + "validate", + "-s", + "sdtmig", + "-lr", + os.path.join("tests", "resources", "library_rules", "CORE-000007.json"), + "-v", + "3.4", + "-dp", + os.path.join( + "tests", + "resources", + "test_dataset.json", + ), + "-dxp", + os.path.join("tests", "resources", "CoreIssue984", "define.xml"), + "--output-format", + "json", + "-ps", + "1", + ] + subprocess.run(command, check=True) + + files = os.listdir() + json_files = [ + file + for file in files + if file.startswith("CORE-Report-") and file.endswith(".json") + ] + json_report_path = sorted(json_files)[-1] + # Open the JSON report file + json_report = json.load(open(json_report_path)) + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + + assert { + "Conformance_Details", + "Dataset_Details", + "Issue_Summary", + "Issue_Details", + "Rules_Report", + }.issubset(json_report.keys()) + assert json_report["Conformance_Details"]["Standard"] == "SDTMIG" + assert json_report["Conformance_Details"]["Define_XML_Version"] == "2.1.5" + + if os.path.exists(json_report_path): + os.remove(json_report_path) diff --git a/tests/QARegressionTests/test_core/test_list_dataset_metadata.py b/tests/QARegressionTests/test_core/test_list_dataset_metadata.py index e094cf8e5..64cdb09ab 100644 --- a/tests/QARegressionTests/test_core/test_list_dataset_metadata.py +++ b/tests/QARegressionTests/test_core/test_list_dataset_metadata.py @@ -11,6 +11,8 @@ def setUp(self): self.runner = CliRunner() def test_list_dataset_metadata_with_valid_paths(self): + """Test that list-dataset-metadata on a valid -dp file returns the + expected metadata JSON (domain EX, correct filename).""" result = self.runner.invoke( list_dataset_metadata, ["-dp", os.path.join("tests", "resources", "test_dataset.xpt")], @@ -23,6 +25,8 @@ def test_list_dataset_metadata_with_valid_paths(self): self.assertIn(expected_output, result.output) def test_list_dataset_metadata_with_invalid_paths(self): + """Test that a nonexistent -dp path fails (exit code 1) and does not + produce dataset metadata output.""" result = self.runner.invoke(list_dataset_metadata, ["-dp", "invalid_path"]) expected_output = """[ { @@ -34,6 +38,8 @@ def test_list_dataset_metadata_with_invalid_paths(self): self.assertNotIn(expected_output, result.output) def test_list_dataset_metadata_with_no_paths(self): + """Test that omitting the required -dp option fails with a + missing-option error.""" result = self.runner.invoke(list_dataset_metadata) expected_output = """Error: Missing option '-dp' / '--dataset-path'""" self.assertEqual(result.exit_code, 2) diff --git a/tests/QARegressionTests/test_core/test_list_rule_sets.py b/tests/QARegressionTests/test_core/test_list_rule_sets.py index c248e43bc..693f26584 100644 --- a/tests/QARegressionTests/test_core/test_list_rule_sets.py +++ b/tests/QARegressionTests/test_core/test_list_rule_sets.py @@ -9,28 +9,25 @@ class TestListRuleSets(unittest.TestCase): def setUp(self): self.runner = CliRunner() - def test_list_rule_sets_valid_cache_path(self): - result = self.runner.invoke( - list_rule_sets, ["-c", os.path.join("resources", "cache")] - ) - self.assertIn("sdtmig, 3-2", result.output) - self.assertIn("sdtmig, 3-3", result.output) - self.assertIn("sdtmig, 3-4", result.output) - self.assertIn("sendig, 3-1", result.output) + def test_list_rule_sets_lists_expected_standard_version_pairs(self): + """Test that list-rule-sets lists the expected standard/version pairs + both with an explicit valid -c/--cache-path and with -c omitted + entirely (falls back to the default cache path). + """ + expected_pairs = ["sdtmig, 3-2", "sdtmig, 3-3", "sdtmig, 3-4", "sendig, 3-1"] + for args in (["-c", os.path.join("resources", "cache")], []): + result = self.runner.invoke(list_rule_sets, args) + for pair in expected_pairs: + self.assertIn(pair, result.output) def test_list_rule_sets_invalid_cache_path(self): + """Test that an invalid/incomplete --cache-path (a dir with no rule + cache files) fails with a non-zero exit code.""" result = self.runner.invoke( list_rule_sets, ["--cache-path", os.path.join("resources")] ) self.assertNotEqual(result.exit_code, 0) - def test_list_rule_sets_no_cache_path(self): - result = self.runner.invoke(list_rule_sets, []) - self.assertIn("sdtmig, 3-2", result.output) - self.assertIn("sdtmig, 3-3", result.output) - self.assertIn("sdtmig, 3-4", result.output) - self.assertIn("sendig, 3-1", result.output) - def tearDown(self): tearDown() diff --git a/tests/QARegressionTests/test_core/test_list_rules.py b/tests/QARegressionTests/test_core/test_list_rules.py index 06002b65c..8586935a5 100644 --- a/tests/QARegressionTests/test_core/test_list_rules.py +++ b/tests/QARegressionTests/test_core/test_list_rules.py @@ -12,6 +12,7 @@ def setUp(self): self.runner = CliRunner() def test_list_rules_all_options_provided(self): + """Test that list-rules with -c, -s and -v all provided succeeds.""" result = self.runner.invoke( list_rules, ["-c", os.path.join("resources", "cache"), "-s", "sdtmig", "-v", "3.4"], @@ -19,22 +20,24 @@ def test_list_rules_all_options_provided(self): self.assertEqual(result.exit_code, 0) def test_list_rules_standard_option_provided(self): + """Test that list-rules with only -s provided succeeds.""" result = self.runner.invoke(list_rules, ["-s", "sdtmig"]) self.assertEqual(result.exit_code, 0) def test_list_rules_version_option_provided(self): + """Test that list-rules with only -v provided succeeds.""" result = self.runner.invoke(list_rules, ["-v", "3.4"]) self.assertEqual(result.exit_code, 0) def test_list_rules_no_option_provided(self): + """Test that list-rules with no options at all succeeds (lists all + rules). + """ result = self.runner.invoke(list_rules) self.assertEqual(result.exit_code, 0) - def test_list_rules_required_options_provided(self): - result = self.runner.invoke(list_rules, ["-s", "sdtmig", "-v", "3.4"]) - self.assertEqual(result.exit_code, 0) - def test_list_rules_output_format(self): + """Test that list-rules output is a JSON list of rule dicts.""" result = self.runner.invoke(list_rules) output = json.loads(result.output) self.assertIsInstance(output, list) diff --git a/tests/QARegressionTests/test_core/test_utils.py b/tests/QARegressionTests/test_core/test_utils.py index 7a1f7c65b..0033bec13 100644 --- a/tests/QARegressionTests/test_core/test_utils.py +++ b/tests/QARegressionTests/test_core/test_utils.py @@ -1,36 +1,36 @@ -import subprocess -from platform import system -from os import environ, listdir, remove - - -def run_command(args, shell): - env = environ.copy() - env["PYTHONUNBUFFERED"] = "1" - env["PYTHONUTF8"] = "1" - - try: - completed_process = subprocess.run( - args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - check=True, - encoding="utf8", - shell=shell or system() == "Windows", - env=env, - ) - return ( - completed_process.returncode, - completed_process.stdout.lower(), - completed_process.stderr.lower(), - ) - except subprocess.CalledProcessError as e: - return e.returncode, e.stdout.lower(), e.stderr.lower() - - -def tearDown(): - for file_name in listdir("."): - if file_name not in ("host.json", "local.settings.json") and ( - file_name.endswith(".xlsx") or file_name.endswith(".json") - ): - remove(file_name) +import subprocess +from platform import system +from os import environ, listdir, remove + + +def run_command(args, shell): + env = environ.copy() + env["PYTHONUNBUFFERED"] = "1" + env["PYTHONUTF8"] = "1" + + try: + completed_process = subprocess.run( + args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + encoding="utf8", + shell=shell or system() == "Windows", + env=env, + ) + return ( + completed_process.returncode, + completed_process.stdout.lower(), + completed_process.stderr.lower(), + ) + except subprocess.CalledProcessError as e: + return e.returncode, e.stdout.lower(), e.stderr.lower() + + +def tearDown(): + for file_name in listdir("."): + if file_name not in ("host.json", "local.settings.json") and ( + file_name.endswith(".xlsx") or file_name.endswith(".json") + ): + remove(file_name) diff --git a/tests/QARegressionTests/test_core/test_validate.py b/tests/QARegressionTests/test_core/test_validate.py index b3e6229bc..28f4714f4 100644 --- a/tests/QARegressionTests/test_core/test_validate.py +++ b/tests/QARegressionTests/test_core/test_validate.py @@ -35,6 +35,7 @@ def check_issue_summary_tab_empty(self): return True def test_validate_required_s_option_missing(self): + """Test that omitting the required -s/--standard option fails.""" args = [ "python", "core.py", @@ -49,24 +50,8 @@ def test_validate_required_s_option_missing(self): self.assertNotEqual(exit_code, 0) self.assertNotEqual(stderr, "", "Error Not raised for invalid command") - def test_validate_required_s_option_present(self): - args = [ - "python", - "core.py", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-dp", - os.path.join("tests", "resources", "test_dataset.xpt"), - ] - exit_code, stdout, stderr = run_command(args, False) - - self.assertEqual(exit_code, 0) - self.assertFalse(self.error_keyword in stdout) - def test_validate_required_v_option_missing(self): + """Test that omitting the required -v/--version option fails.""" args = [ "python", "core.py", @@ -81,7 +66,10 @@ def test_validate_required_v_option_missing(self): self.assertNotEqual(exit_code, 0) self.assertNotEqual(stderr, "", "Error Not raised for invalid command") - def test_validate_required_v_option_present(self): + def test_validate_with_minimum_required_options(self): + """Test that supplying just the 3 required options (-s, -v, -dp), + with no other flags, succeeds without producing errors. + """ args = [ "python", "core.py", @@ -99,25 +87,9 @@ def test_validate_required_v_option_present(self): self.assertFalse(self.error_keyword in stdout) self.assertNotIn("error", stderr.lower()) - def test_validate_with_all_required_options(self): - args = [ - "python", - "core.py", - "validate", - "-dp", - os.path.join("tests", "resources", "test_dataset.xpt"), - "-s", - "sdtmig", - "-v", - "3.4", - ] - exit_code, stdout, stderr = run_command(args, False) - - self.assertEqual(exit_code, 0) - self.assertFalse(self.error_keyword in stdout) - self.assertNotIn("error", stderr.lower()) - def test_validate_without_all_required_options(self): + """Test that passing only -d (no -s/-v) fails with a missing-option + error.""" args = [ "python", "core.py", @@ -131,6 +103,11 @@ def test_validate_without_all_required_options(self): self.assertIn("error: missing option", stderr.lower()) def test_validate_all_options(self): + """Test that nearly every validate flag can be supplied together + without crashing. Deliberately passes both -d and -dp (mutually + exclusive), so the expected outcome is a non-empty stderr; this does + not assert anything about the rule/report content itself. + """ args = [ "python", "core.py", @@ -162,7 +139,7 @@ def test_validate_all_options(self): os.path.join("tests", "resources", "dictionaries", "whodrug"), "--meddra", os.path.join("tests", "resources", "dictionaries", "meddra"), - "-r", + "-lr", os.path.join("tests", "resources", "Rule-CG0027.json"), "-lr", os.path.join("tests", "resources", "CG0272.yml"), @@ -173,6 +150,9 @@ def test_validate_all_options(self): self.assertNotEqual(stderr, "") def test_validate_local_rule(self): + """Test that -r filters which rule(s) within a -lr local rules + directory run. + """ args = [ "python", "core.py", @@ -194,6 +174,8 @@ def test_validate_local_rule(self): self.assertFalse(self.error_keyword in stdout) def test_validate_local_exclude_rule(self): + """Test that -er excludes a rule within a -lr local rules directory + (counterpart to test_validate_local_rule's include-filter case).""" args = [ "python", "core.py", @@ -217,6 +199,8 @@ def test_validate_local_exclude_rule(self): self.assertFalse(self.error_keyword in stdout) def test_validate_include_exclude(self): + """Test that passing both -r and -er together is rejected with exit + code 2, regardless of whether the referenced rules exist locally.""" args = [ "python", "core.py", @@ -242,31 +226,19 @@ def test_validate_include_exclude(self): "cannot use both --rules and --exclude-rules flags together.", stderr ) - def test_validate_minimum_options(self): - args = [ - "python", - "core.py", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-dp", - os.path.join("tests", "resources", "test_dataset.xpt"), - ] - exit_code, stdout, stderr = run_command(args, False) - - self.assertEqual(exit_code, 0) - self.assertNotIn("error", stderr.lower()) - self.assertFalse(self.error_keyword in stdout) - def test_validate_less_than_minimum_options(self): + """Test that -s alone (no -v, no dataset) fails with a specific + "missing option -v" error message.""" args = ["python", "core.py", "validate", "-s", "sdtmig"] exit_code, stdout, stderr = run_command(args, False) self.assertNotEqual(exit_code, 0) - self.assertIn("error: missing option", stderr) + self.assertIn( + "\n\nerror: missing option '-v' / '--version'.\n", + stderr, + ) def test_validate_output_format_json(self): + """Test that -of json produces a successful run.""" args = [ "python", "core.py", @@ -287,6 +259,7 @@ def test_validate_output_format_json(self): self.assertNotIn("error", stderr.lower()) def test_validate_output_format_excel(self): + """Test that -of xlsx produces a successful run.""" args = [ "python", "core.py", @@ -307,6 +280,7 @@ def test_validate_output_format_excel(self): self.assertNotIn("error", stderr.lower()) def test_validate_with_invalid_output_format(self): + """Test that an unrecognized -of value fails.""" args = [ "python", "core.py", @@ -328,6 +302,7 @@ def test_validate_with_invalid_output_format(self): self.assertNotEqual(stderr, "") def test_validate_with_log_level_disabled(self): + """Test that -l disabled produces a successful run.""" args = [ "python", "core.py", @@ -348,6 +323,9 @@ def test_validate_with_log_level_disabled(self): self.assertNotIn("error", stderr.lower()) def test_validate_with_log_level_info(self): + """Test that -l info logging doesn't affect exit code/output when a + specific rule is selected. + """ args = [ "python", "core.py", @@ -360,14 +338,17 @@ def test_validate_with_log_level_info(self): os.path.join("tests", "resources", "test_dataset.xpt"), "-l", "info", + "-lr", + os.path.join("tests", "resources", "library_rules", "CORE-000237.json"), ] exit_code, stdout, stderr = run_command(args, False) self.assertEqual(exit_code, 0) self.assertFalse(self.error_keyword in stdout) - self.assertIn("warning", stderr) def test_validate_with_log_level_error(self): + """Test that -l error produces a successful run (with some stderr + output, since "error" level still surfaces log lines here).""" args = [ "python", "core.py", @@ -388,6 +369,7 @@ def test_validate_with_log_level_error(self): self.assertNotEqual(stderr, "") def test_validate_with_log_level_critical(self): + """Test that -l critical produces a successful run.""" args = [ "python", "core.py", @@ -408,6 +390,8 @@ def test_validate_with_log_level_critical(self): self.assertNotIn("error", stderr.lower()) def test_validate_with_log_level_warn(self): + """Test that -l warn produces a successful run with no "warning" text + in stderr.""" args = [ "python", "core.py", @@ -427,6 +411,7 @@ def test_validate_with_log_level_warn(self): self.assertNotIn("warning", stderr) def test_validate_with_invalid_log_level(self): + """Test that an unrecognized -l value fails.""" args = [ "python", "core.py", @@ -445,25 +430,9 @@ def test_validate_with_invalid_log_level(self): self.assertNotEqual(exit_code, 0) self.assertNotEqual(stderr, "") - def test_validate_with_no_log_level(self): - args = [ - "python", - "core.py", - "validate", - "-s", - "sdtmig", - "-v", - "3.4", - "-dp", - os.path.join("tests", "resources", "test_dataset.xpt"), - ] - exit_code, stdout, stderr = run_command(args, False) - - self.assertEqual(exit_code, 0) - self.assertFalse(self.error_keyword in stdout) - self.assertNotIn("error", stderr.lower()) - def test_validate_high_value_ps(self): + """Test that a -ps (pool size) value smaller than the CPU count + doesn't break validation.""" args = [ "python", "core.py", @@ -484,6 +453,8 @@ def test_validate_high_value_ps(self): self.assertNotIn("error", stderr.lower()) def test_validate_define_xml_path(self): + """Test that a valid -dxp/--define-xml-path produces a successful + run.""" args = [ "python", "core.py", @@ -503,6 +474,8 @@ def test_validate_define_xml_path(self): self.assertNotIn("error", stderr.lower()) def test_validate_dummy_with_all_options_one_data_source(self): + """Test the shell-string command-invocation variant (shell=True) with + a single -dp data source and a full option set succeeds.""" args = ( f"python core.py validate " f"-ca {os.path.join('resources', 'cache')} " @@ -522,6 +495,9 @@ def test_validate_dummy_with_all_options_one_data_source(self): self.assertNotIn("error", stderr.lower()) def test_validate_dummy_with_all_options(self): + """Test that passing both -dp and -d (mutually exclusive) via the + shell-string command variant fails with the expected error message + format.""" args = ( f"python core.py validate " f"-ca {os.path.join('resources', 'cache')} " @@ -553,6 +529,8 @@ def test_validate_dummy_with_all_options(self): self.assertTrue(re.match(expected_pattern, stderr), error_msg) def test_validate_dummy_without_dataset_path(self): + """Test that passing neither -dp nor -d fails with the expected + "you must pass one of" error message format.""" args = ( f"python core.py validate " f"-ca {os.path.join('resources', 'cache')} " @@ -575,6 +553,8 @@ def test_validate_dummy_without_dataset_path(self): self.assertTrue(re.match(expected_pattern, stderr), error_msg) def test_validate_dummy_with_default_cache_path(self): + """Test that omitting -ca/--cache-path falls back to the default + cache location and still succeeds.""" args = ( f"python core.py validate " f"-s sdtmig " @@ -588,6 +568,8 @@ def test_validate_dummy_with_default_cache_path(self): self.assertNotIn("error", stderr.lower()) def test_validate_dummy_without_whodrug_and_meddra(self): + """Test that omitting the optional --whodrug/--meddra dictionary + paths still succeeds.""" args = ( f"python core.py validate " f"-s sdtmig " @@ -602,6 +584,7 @@ def test_validate_dummy_without_whodrug_and_meddra(self): self.assertNotIn("error", stderr.lower()) def test_validate_dummy_with_invalid_whodrug_and_meddra(self): + """Test that invalid --whodrug/--meddra dictionary paths fail.""" args = ( f"python core.py validate " f"-ca {os.path.join('resources', 'cache')} " @@ -615,6 +598,7 @@ def test_validate_dummy_with_invalid_whodrug_and_meddra(self): self.assertNotEqual(stderr, "") def test_validate_dummy_with_vx_as_no(self): + """Test that -vx no disables Define-XML validation without error.""" args = ( f"python core.py validate " f"-s sendig " @@ -627,6 +611,8 @@ def test_validate_dummy_with_vx_as_no(self): self.assertNotIn("error", stdout) def test_validate_dummy_with_vx_as_yes(self): + """Test that -vx y (default) enables Define-XML validation and still + succeeds.""" args = ( f"python core.py validate " f"-s sendig " diff --git a/tests/resources/CoreIssue357/SENDIG_266_rule.json b/tests/resources/CoreIssue357/SENDIG_266_rule.json index 996f76f94..dd48b8ebd 100644 --- a/tests/resources/CoreIssue357/SENDIG_266_rule.json +++ b/tests/resources/CoreIssue357/SENDIG_266_rule.json @@ -20,7 +20,8 @@ ], "Core": { "Id": "CDISC.SDTMIG.CG0013", - "Version": "1" + "Version": "1", + "Status": "Draft" }, "Description": "Trigger when variable cannot be found in the SDTM model (v.1.7 for SENDIG-3.1)", "Operations": [ diff --git a/tests/resources/library_rules/CORE-000007.json b/tests/resources/library_rules/CORE-000007.json new file mode 100644 index 000000000..69b7c67d3 --- /dev/null +++ b/tests/resources/library_rules/CORE-000007.json @@ -0,0 +1,376 @@ +{ + "core_id": "CORE-000007", + "author": "CDISC", + "reference": [ + [ + { + "Citations": [ + { + "Cited_Guidance": "DTHDTC[The date or date and time of death, represented in a standardized character format.] DTHFL[An indication that the subject died.]", + "Document": "Model v2.0", + "Item": "DTHDTC|DTHFL", + "Section": "Demographics" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0435", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "A value of 'Y' indicates the subject died. Should be Y or null. Should be populated even when the death date is unknown.", + "Document": "Model v1.4", + "Section": "2.2.6" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0435", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "A value of \"Y\" indicates the subject died. Should be \"Y\" or null. Should be populated even when the death date is unknown.", + "Document": "Model v1.7", + "Section": "Table 2.2.6.1" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0435", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "DTHDTC[The date or date and time of death, represented in a standardized character format.]|DTHFL[An indication that the subject died.]", + "Document": "SDTM v2.1", + "Item": "DTHDTC|DTHFL", + "Section": "3.2.1 Demographics" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0587", + "Version": "1" + }, + "Version": "1.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "All death information should be populated for subjects that died during the study including any post treatment follow-up.", + "Document": "FDA", + "Section": "FDAB006" + } + ], + "Origin": "FDA Business Rules", + "Rule_Identifier": { + "Id": "FB0606", + "Version": "1" + }, + "Version": "1.5" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "All death information should be populated for subjects that died during the study including any post treatment follow-up.", + "Document": "FDA", + "Section": "FDAB006" + } + ], + "Origin": "FDA Business Rules", + "Rule_Identifier": { + "Id": "FB0606", + "Version": "1" + }, + "Version": "1.5" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "All death information should be populated for subjects that died during the study including any post treatment follow-up.", + "Document": "FDA", + "Section": "FDAB006" + } + ], + "Origin": "FDA Business Rules", + "Rule_Identifier": { + "Id": "FB0606", + "Version": "1" + }, + "Version": "1.5" + } + ] + ], + "sensitivity": "Record", + "executability": "fully executable", + "description": "When Date/Time of Death (DTHDTC) in the DM dataset is populated then death flag (DTHFL) should be populated as 'Y' in the DM dataset for the corresponding subject.", + "authorities": [ + { + "Organization": "CDISC", + "Standards": [ + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "DTHDTC[The date or date and time of death, represented in a standardized character format.] DTHFL[An indication that the subject died.]", + "Document": "Model v2.0", + "Item": "DTHDTC|DTHFL", + "Section": "Demographics" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0435", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.4" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "A value of 'Y' indicates the subject died. Should be Y or null. Should be populated even when the death date is unknown.", + "Document": "Model v1.4", + "Section": "2.2.6" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0435", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.2" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "A value of \"Y\" indicates the subject died. Should be \"Y\" or null. Should be populated even when the death date is unknown.", + "Document": "Model v1.7", + "Section": "Table 2.2.6.1" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0435", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.3" + }, + { + "Name": "TIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "DTHDTC[The date or date and time of death, represented in a standardized character format.]|DTHFL[An indication that the subject died.]", + "Document": "SDTM v2.1", + "Item": "DTHDTC|DTHFL", + "Section": "3.2.1 Demographics" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0587", + "Version": "1" + }, + "Version": "1.0" + } + ], + "Substandard": "SDTM", + "Version": "1.0" + } + ] + }, + { + "Organization": "FDA", + "Standards": [ + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "All death information should be populated for subjects that died during the study including any post treatment follow-up.", + "Document": "FDA", + "Section": "FDAB006" + } + ], + "Origin": "FDA Business Rules", + "Rule_Identifier": { + "Id": "FB0606", + "Version": "1" + }, + "Version": "1.5" + } + ], + "Version": "3.2" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "All death information should be populated for subjects that died during the study including any post treatment follow-up.", + "Document": "FDA", + "Section": "FDAB006" + } + ], + "Origin": "FDA Business Rules", + "Rule_Identifier": { + "Id": "FB0606", + "Version": "1" + }, + "Version": "1.5" + } + ], + "Version": "3.3" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "All death information should be populated for subjects that died during the study including any post treatment follow-up.", + "Document": "FDA", + "Section": "FDAB006" + } + ], + "Origin": "FDA Business Rules", + "Rule_Identifier": { + "Id": "FB0606", + "Version": "1" + }, + "Version": "1.5" + } + ], + "Version": "3.4" + } + ] + } + ], + "standards": [ + { + "Name": "SDTMIG", + "Version": "3.4", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.2", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.3", + "Substandard": null + }, + { + "Name": "TIG", + "Version": "1.0", + "Substandard": "SDTM" + }, + { + "Name": "SDTMIG", + "Version": "3.2", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.3", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.4", + "Substandard": null + } + ], + "classes": { + "Include": [ + "SPECIAL PURPOSE" + ] + }, + "domains": { + "Include": [ + "DM" + ] + }, + "entities": null, + "rule_type": "Record Data", + "conditions": { + "all": [ + { + "name": "get_dataset", + "operator": "non_empty", + "value": { + "comparator": null, + "target": "DTHDTC" + } + }, + { + "name": "get_dataset", + "operator": "not_equal_to", + "value": { + "comparator": "Y", + "target": "DTHFL" + } + } + ] + }, + "actions": [ + { + "name": "generate_dataset_error_objects", + "params": { + "message": "DTHDTC is populated but DTHFL in DM dataset is not \"Y\"." + } + } + ], + "use_case": "INDH", + "data_structures": null, + "status": "Published", + "output_variables": [ + "DTHDTC", + "DTHFL" + ] +} \ No newline at end of file diff --git a/tests/resources/library_rules/CORE-000237.json b/tests/resources/library_rules/CORE-000237.json new file mode 100644 index 000000000..873c1dcf7 --- /dev/null +++ b/tests/resources/library_rules/CORE-000237.json @@ -0,0 +1,504 @@ +{ + "core_id": "CORE-000237", + "author": "CDISC", + "reference": [ + [ + { + "Citations": [ + { + "Cited_Guidance": "A further grouping or classification of the category for the topic of the finding, event, or intervention. The category is in --CAT.", + "Document": "Model v2.0", + "Item": "--SCAT", + "Section": "Interventions" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0027", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Used to define a further categorization of --CAT values.", + "Document": "Model v1.7", + "Item": "--SCAT", + "Section": "2.2.1" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0027", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Used to define a further categorization of --CAT values.", + "Document": "Model v1.4", + "Item": "Table 2.2.1, --SCAT", + "Section": "2.2.1" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0027", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.0", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "A further grouping or classification of the category for the topic of the finding, event, or intervention. The category is in --CAT.", + "Document": "SDTM v2.1", + "Item": "--SCAT", + "Section": "3.1.4 Identifiers for All Classes" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0309", + "Version": "1" + }, + "Version": "1.0" + } + ] + ], + "sensitivity": "Record", + "executability": "fully executable", + "description": "Trigger error when --SCAT is not null and --SCAT is equal to --CAT", + "authorities": [ + { + "Organization": "CDISC", + "Standards": [ + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "A further grouping or classification of the category for the topic of the finding, event, or intervention. The category is in --CAT.", + "Document": "Model v2.0", + "Item": "--SCAT", + "Section": "Interventions" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0027", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.4" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Used to define a further categorization of --CAT values.", + "Document": "Model v1.7", + "Item": "--SCAT", + "Section": "2.2.1" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0027", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.3" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Used to define a further categorization of --CAT values.", + "Document": "Model v1.4", + "Item": "Table 2.2.1, --SCAT", + "Section": "2.2.1" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0027", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.2" + }, + { + "Name": "SENDIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.0", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "3.0" + }, + { + "Name": "SENDIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "3.1" + }, + { + "Name": "SENDIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "3.1.1" + }, + { + "Name": "SENDIG-GENETOX", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "1.0" + }, + { + "Name": "SENDIG-DART", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "1.1" + }, + { + "Name": "SENDIG-DART", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Values for --CAT and --SCAT should not be redundant with the domain or dictionary classification provided by --DECOD and --BODSYS.", + "Document": "IG v.3.1.1", + "Section": "4.2.6" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND43", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "1.2" + }, + { + "Name": "TIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "A further grouping or classification of the category for the topic of the finding, event, or intervention. The category is in --CAT.", + "Document": "SDTM v2.1", + "Item": "--SCAT", + "Section": "3.1.4 Identifiers for All Classes" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0309", + "Version": "1" + }, + "Version": "1.0" + } + ], + "Substandard": "SDTM", + "Version": "1.0" + } + ] + } + ], + "standards": [ + { + "Name": "SDTMIG", + "Version": "3.4", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.3", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.2", + "Substandard": null + }, + { + "Name": "SENDIG", + "Version": "3.0", + "Substandard": null + }, + { + "Name": "SENDIG", + "Version": "3.1", + "Substandard": null + }, + { + "Name": "SENDIG", + "Version": "3.1.1", + "Substandard": null + }, + { + "Name": "SENDIG-GENETOX", + "Version": "1.0", + "Substandard": null + }, + { + "Name": "SENDIG-DART", + "Version": "1.1", + "Substandard": null + }, + { + "Name": "SENDIG-DART", + "Version": "1.2", + "Substandard": null + }, + { + "Name": "TIG", + "Version": "1.0", + "Substandard": "SDTM" + } + ], + "classes": { + "Include": [ + "ALL" + ] + }, + "domains": { + "Include": [ + "ALL" + ] + }, + "entities": null, + "rule_type": "Record Data", + "conditions": { + "all": [ + { + "name": "get_dataset", + "operator": "non_empty", + "value": { + "comparator": null, + "target": "--SCAT" + } + }, + { + "name": "get_dataset", + "operator": "equal_to", + "value": { + "comparator": "--CAT", + "target": "--SCAT" + } + } + ] + }, + "actions": [ + { + "name": "generate_dataset_error_objects", + "params": { + "message": "--SCAT is equal to --CAT" + } + } + ], + "use_case": "INDH, PROD", + "data_structures": null, + "status": "Published", + "output_variables": [ + "--CAT", + "--SCAT" + ] +} \ No newline at end of file diff --git a/tests/resources/library_rules/CORE-000354.json b/tests/resources/library_rules/CORE-000354.json new file mode 100644 index 000000000..4bcd56660 --- /dev/null +++ b/tests/resources/library_rules/CORE-000354.json @@ -0,0 +1,260 @@ +{ + "core_id": "CORE-000354", + "author": "CDISC", + "reference": [ + [ + { + "Citations": [ + { + "Cited_Guidance": "The permissible Study Day variables (--DY, --STDY, and --ENDY) . . . are determined by comparing the date portion of the respective date/time variables (--DTC, --STDTC, and --ENDTC) to the date portion of the Subject Reference Start Date (RFSTDTC from the Demographics domain)", + "Document": "SDTMIG v3.4", + "Section": "4.4.4" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0007", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "The permissible Study Day variables (--DY, --STDY, and --ENDY) . . . are determined by comparing the date portion of the respective date/time variables (--DTC, --STDTC, and --ENDTC) to the date portion of the Subject Reference Start Date (RFSTDTC from the Demographics domain).", + "Document": "SDTMIG v3.3", + "Section": "4.4.4" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0007", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "The permissible Study Day variables (--DY, --STDY, and --ENDY) . . . are determined by comparing the date portion of the respective date/time variables (--DTC, --STDTC, and --ENDTC) to the date portion of the Subject Reference Start Date (RFSTDTC from the Demographics domain).", + "Document": "SDTMIG v3.2", + "Section": "4.1.4.4" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0007", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Variables --DY, --STDY, and --ENDY represent the timing of observations in days relative to the reference start date in RFSTDTC where the value of --DY is the relative day of the date populated in --DTC for the observation,--STDTC is the relative day of the date populated in --STDTC for the observation; an--ENDY is the relative day of the of the date populated in --ENDTC for the observation.", + "Document": "TIG 1.0", + "Section": "2.8.7.6" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0292", + "Version": "1" + }, + "Version": "1.0" + } + ] + ], + "sensitivity": "Record", + "executability": "fully executable", + "description": "Raise an error when the date portion of --DTC is an incomplete date or the date portion of DM.RFSTDTC is an incomplete date, but --DY is not empty", + "authorities": [ + { + "Organization": "CDISC", + "Standards": [ + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "The permissible Study Day variables (--DY, --STDY, and --ENDY) . . . are determined by comparing the date portion of the respective date/time variables (--DTC, --STDTC, and --ENDTC) to the date portion of the Subject Reference Start Date (RFSTDTC from the Demographics domain)", + "Document": "SDTMIG v3.4", + "Section": "4.4.4" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0007", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.4" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "The permissible Study Day variables (--DY, --STDY, and --ENDY) . . . are determined by comparing the date portion of the respective date/time variables (--DTC, --STDTC, and --ENDTC) to the date portion of the Subject Reference Start Date (RFSTDTC from the Demographics domain).", + "Document": "SDTMIG v3.3", + "Section": "4.4.4" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0007", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.3" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "The permissible Study Day variables (--DY, --STDY, and --ENDY) . . . are determined by comparing the date portion of the respective date/time variables (--DTC, --STDTC, and --ENDTC) to the date portion of the Subject Reference Start Date (RFSTDTC from the Demographics domain).", + "Document": "SDTMIG v3.2", + "Section": "4.1.4.4" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0007", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.2" + }, + { + "Name": "TIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Variables --DY, --STDY, and --ENDY represent the timing of observations in days relative to the reference start date in RFSTDTC where the value of --DY is the relative day of the date populated in --DTC for the observation,--STDTC is the relative day of the date populated in --STDTC for the observation; an--ENDY is the relative day of the of the date populated in --ENDTC for the observation.", + "Document": "TIG 1.0", + "Section": "2.8.7.6" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0292", + "Version": "1" + }, + "Version": "1.0" + } + ], + "Substandard": "SDTM", + "Version": "1.0" + } + ] + } + ], + "standards": [ + { + "Name": "SDTMIG", + "Version": "3.4", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.3", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.2", + "Substandard": null + }, + { + "Name": "TIG", + "Version": "1.0", + "Substandard": "SDTM" + } + ], + "classes": { + "Include": [ + "ALL" + ] + }, + "domains": { + "Include": [ + "ALL" + ] + }, + "entities": null, + "rule_type": "Record Data", + "conditions": { + "all": [ + { + "name": "get_dataset", + "operator": "non_empty", + "value": { + "comparator": null, + "target": "--DY" + } + }, + { + "any": [ + { + "name": "get_dataset", + "operator": "is_incomplete_date", + "value": { + "comparator": null, + "target": "--DTC" + } + }, + { + "name": "get_dataset", + "operator": "is_incomplete_date", + "value": { + "comparator": null, + "target": "RFSTDTC" + } + } + ] + } + ] + }, + "actions": [ + { + "name": "generate_dataset_error_objects", + "params": { + "message": "The date portion of --DTC is not complete date or the date portion of DM.RFSTDTC is not complete date, but --DY is not empty" + } + } + ], + "use_case": "INDH", + "data_structures": null, + "status": "Published", + "datasets": [ + { + "domain_name": "DM", + "match_key": [ + "USUBJID" + ], + "wildcard": "**" + } + ], + "output_variables": [ + "--DY", + "--DTC", + "RFSTDTC" + ] +} \ No newline at end of file diff --git a/tests/resources/library_rules/CORE-000356.json b/tests/resources/library_rules/CORE-000356.json new file mode 100644 index 000000000..ac341e02f --- /dev/null +++ b/tests/resources/library_rules/CORE-000356.json @@ -0,0 +1,558 @@ +{ + "core_id": "CORE-000356", + "author": "CDISC", + "reference": [ + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.2", + "Section": "4.1.1.5" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0014", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.3", + "Section": "4.1.5" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0014", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.4", + "Section": "4.1.5" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0014", + "Version": "1" + }, + "Version": "2.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.0", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "All Required and Expected tabulation variables are included as columns in the dataset. Required tabulation variables are populated for all records in the dataset.", + "Document": "TIG 1.0", + "Item": "Table 2 Num 6", + "Section": "2.5" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0299", + "Version": "1" + }, + "Version": "1.0" + } + ], + [ + { + "Citations": [ + { + "Cited_Guidance": "Req for variables which are Required and must be included in the resulting dataset and cannot be null for any record. Such variables are basic to the identification of a data record or are necessary to make the record meaningful.", + "Document": "TIG 1.0", + "Item": "Text", + "Section": "2.8.1 How to Read Domain Specifications" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0057", + "Version": "1" + }, + "Version": "1.0" + } + ] + ], + "sensitivity": "Record", + "executability": "fully executable", + "description": "Part B: Raise an error when a Required variable is null.", + "authorities": [ + { + "Organization": "CDISC", + "Standards": [ + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.2", + "Section": "4.1.1.5" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0014", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.2" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.3", + "Section": "4.1.5" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0014", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.3" + }, + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.4", + "Section": "4.1.5" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0014", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.4" + }, + { + "Name": "SENDIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.0", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "3.0" + }, + { + "Name": "SENDIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "3.1" + }, + { + "Name": "SENDIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "3.1.1" + }, + { + "Name": "SENDIG-DART", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "1.1" + }, + { + "Name": "SENDIG-DART", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "1.2" + }, + { + "Name": "SENDIG-GENETOX", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Required variables must always be included in the dataset and cannot be null for any record.", + "Document": "IG v3.1.1", + "Section": "4.1.3" + } + ], + "Origin": "SEND Conformance Rules", + "Rule_Identifier": { + "Id": "SEND12", + "Version": "1" + }, + "Version": "5.0" + } + ], + "Version": "1.0" + }, + { + "Name": "TIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "All Required and Expected tabulation variables are included as columns in the dataset. Required tabulation variables are populated for all records in the dataset.", + "Document": "TIG 1.0", + "Item": "Table 2 Num 6", + "Section": "2.5" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0299", + "Version": "1" + }, + "Version": "1.0" + } + ], + "Substandard": "SDTM", + "Version": "1.0" + }, + { + "Name": "TIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "Req for variables which are Required and must be included in the resulting dataset and cannot be null for any record. Such variables are basic to the identification of a data record or are necessary to make the record meaningful.", + "Document": "TIG 1.0", + "Item": "Text", + "Section": "2.8.1 How to Read Domain Specifications" + } + ], + "Origin": "TIG Conformance Rules", + "Rule_Identifier": { + "Id": "TIG0057", + "Version": "1" + }, + "Version": "1.0" + } + ], + "Substandard": "SEND", + "Version": "1.0" + } + ] + } + ], + "standards": [ + { + "Name": "SDTMIG", + "Version": "3.2", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.3", + "Substandard": null + }, + { + "Name": "SDTMIG", + "Version": "3.4", + "Substandard": null + }, + { + "Name": "SENDIG", + "Version": "3.0", + "Substandard": null + }, + { + "Name": "SENDIG", + "Version": "3.1", + "Substandard": null + }, + { + "Name": "SENDIG", + "Version": "3.1.1", + "Substandard": null + }, + { + "Name": "SENDIG-DART", + "Version": "1.1", + "Substandard": null + }, + { + "Name": "SENDIG-DART", + "Version": "1.2", + "Substandard": null + }, + { + "Name": "SENDIG-GENETOX", + "Version": "1.0", + "Substandard": null + }, + { + "Name": "TIG", + "Version": "1.0", + "Substandard": "SDTM" + }, + { + "Name": "TIG", + "Version": "1.0", + "Substandard": "SEND" + } + ], + "classes": { + "Include": [ + "ALL" + ] + }, + "domains": { + "Include": [ + "ALL" + ] + }, + "entities": null, + "rule_type": "Value Check with Dataset Metadata", + "conditions": { + "all": [ + { + "name": "get_dataset", + "operator": "exists", + "value": { + "comparator": null, + "target": "$required_variables" + } + }, + { + "name": "get_dataset", + "operator": "is_contained_by", + "value": { + "comparator": "$required_variables", + "target": "variable_name" + } + }, + { + "name": "get_dataset", + "operator": "empty", + "value": { + "comparator": null, + "target": "variable_value" + } + } + ] + }, + "actions": [ + { + "name": "generate_dataset_error_objects", + "params": { + "message": "At least one Required variable has a null value" + } + } + ], + "use_case": "INDH, PROD, NONCLIN", + "data_structures": null, + "status": "Published", + "operations": [ + { + "id": "$required_variables", + "operator": "required_variables" + } + ], + "output_variables": [ + "variable_name", + "variable_value" + ] +} \ No newline at end of file diff --git a/tests/resources/library_rules/CORE-000766.json b/tests/resources/library_rules/CORE-000766.json new file mode 100644 index 000000000..287ab32e3 --- /dev/null +++ b/tests/resources/library_rules/CORE-000766.json @@ -0,0 +1,116 @@ +{ + "core_id": "CORE-000766", + "author": "CDISC", + "reference": [ + [ + { + "Citations": [ + { + "Cited_Guidance": "When findings are collected about an event or intervention, and the name of the event or intervention is collected in an \"Other, specify\" CRF field, the value in --OBJ variable depends on whether the FA record has a parent record and whether the \"Other, specify\" value was coded.", + "Document": "IG v3.4", + "Section": "4.2.7.4" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0601", + "Version": "1" + }, + "Version": "2.0" + } + ] + ], + "sensitivity": "Record", + "executability": "fully executable", + "description": "When findings are collected, and the Events parent record exists and --DECOD is null, FAOBJ should not be equal to --TERM", + "authorities": [ + { + "Organization": "CDISC", + "Standards": [ + { + "Name": "SDTMIG", + "References": [ + { + "Citations": [ + { + "Cited_Guidance": "When findings are collected about an event or intervention, and the name of the event or intervention is collected in an \"Other, specify\" CRF field, the value in --OBJ variable depends on whether the FA record has a parent record and whether the \"Other, specify\" value was coded.", + "Document": "IG v3.4", + "Section": "4.2.7.4" + } + ], + "Origin": "SDTM and SDTMIG Conformance Rules", + "Rule_Identifier": { + "Id": "CG0601", + "Version": "1" + }, + "Version": "2.0" + } + ], + "Version": "3.4" + } + ] + } + ], + "standards": [ + { + "Name": "SDTMIG", + "Version": "3.4", + "Substandard": null + } + ], + "classes": { + "Include": [ + "EVENTS" + ] + }, + "domains": { + "Include": [ + "ALL" + ] + }, + "entities": null, + "rule_type": "Record Data", + "conditions": { + "all": [ + { + "name": "get_dataset", + "operator": "empty", + "value": { + "comparator": null, + "target": "--DECOD" + } + }, + { + "name": "get_dataset", + "operator": "not_equal_to", + "value": { + "comparator": "RELREC.FAOBJ", + "target": "--TERM" + } + } + ] + }, + "actions": [ + { + "name": "generate_dataset_error_objects", + "params": { + "message": "Related record is present in the parent domain dataset and \"Other, specify\" value was coded." + } + } + ], + "use_case": null, + "data_structures": null, + "status": "Published", + "datasets": [ + { + "domain_name": "RELREC", + "match_key": [], + "wildcard": "FA" + } + ], + "output_variables": [ + "--DECOD", + "--TERM", + "RELREC.FAOBJ" + ] +} \ No newline at end of file