From 94a56f69e3aec739d6414c6f4ff316f47ab03d2f Mon Sep 17 00:00:00 2001 From: Aviral Saxena Date: Sat, 28 Feb 2026 00:47:29 +0530 Subject: [PATCH 1/7] Added regression tests --- .github/workflows/backend-ci.yml | 43 +++++++++++++++++ apps/backend/tests/test_api_schema.py | 24 ++++++++++ apps/backend/tests/test_regression.py | 46 +++++++++++++++++++ apps/frontend/src/app/_home/UploadButtons.tsx | 2 +- .../frontend/src/app/_home/WhatIsThisTool.tsx | 4 +- .../src/app/_layout/navigation-data.ts | 4 +- .../src/app/convert/dcm-to-bdis/page.tsx | 2 +- docs/assets/Package-Arch.svg | 2 +- docs/development/architecture.mdx | 2 +- docs/user-guide/quick-start.mdx | 2 +- 10 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/backend-ci.yml create mode 100644 apps/backend/tests/test_api_schema.py create mode 100644 apps/backend/tests/test_regression.py diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml new file mode 100644 index 00000000..65801f3e --- /dev/null +++ b/.github/workflows/backend-ci.yml @@ -0,0 +1,43 @@ +name: Backend CI + +on: + push: + paths: + - "apps/backend/**" + - "package/**" + pull_request: + paths: + - "apps/backend/**" + - "package/**" + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + cd apps/backend + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest pytest-regressions httpx + + - name: Install package (editable) + run: | + cd package + pip install -e . + + - name: Run tests + run: | + cd apps/backend + pytest -v \ No newline at end of file diff --git a/apps/backend/tests/test_api_schema.py b/apps/backend/tests/test_api_schema.py new file mode 100644 index 00000000..bac9a562 --- /dev/null +++ b/apps/backend/tests/test_api_schema.py @@ -0,0 +1,24 @@ +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + + +def test_bids_endpoint_returns_expected_schema(): + response = client.post( + "/api/report/process/bids", + data={"modality": "ASL"} + ) + + assert response.status_code == 200 + data = response.json() + + expected_keys = { + "basic_report", + "extended_report", + "asl_parameters", + "errors", + "warnings", + } + + assert expected_keys.issubset(data.keys()) \ No newline at end of file diff --git a/apps/backend/tests/test_regression.py b/apps/backend/tests/test_regression.py new file mode 100644 index 00000000..e2a42f6b --- /dev/null +++ b/apps/backend/tests/test_regression.py @@ -0,0 +1,46 @@ +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + + +def normalize_response(data: dict) -> dict: + """ + Remove volatile fields to ensure deterministic regression. + """ + data.pop("nifti_slice_number", None) + return data + + +def test_root_endpoint_regression(data_regression): + response = client.get("/") + assert response.status_code == 200 + + data = response.json() + data_regression.check(data) + + +def test_bids_endpoint_regression(data_regression): + response = client.post( + "/api/report/process/bids", + data={"modality": "ASL"} + ) + + assert response.status_code == 200 + + data = normalize_response(response.json()) + data_regression.check(data) + + +def test_dicom_invalid_file_returns_500(tmp_path): + file_path = tmp_path / "invalid.txt" + file_path.write_text("not a dicom") + + with open(file_path, "rb") as f: + response = client.post( + "/api/report/process/dicom", # fixed /api prefix + files={"dcm_files": ("invalid.txt", f, "text/plain")}, + data={"modality": "ASL"}, + ) + + assert response.status_code == 500 \ No newline at end of file diff --git a/apps/frontend/src/app/_home/UploadButtons.tsx b/apps/frontend/src/app/_home/UploadButtons.tsx index 5c141393..40de33ca 100644 --- a/apps/frontend/src/app/_home/UploadButtons.tsx +++ b/apps/frontend/src/app/_home/UploadButtons.tsx @@ -126,7 +126,7 @@ const UploadButtons = () => { )} onClick={() => setActiveFileTypeOption(UploadDataType.BIDS)} > - BDIS + BIDS diff --git a/apps/frontend/src/app/_layout/navigation-data.ts b/apps/frontend/src/app/_layout/navigation-data.ts index 2a52bb75..583e8059 100644 --- a/apps/frontend/src/app/_layout/navigation-data.ts +++ b/apps/frontend/src/app/_layout/navigation-data.ts @@ -42,8 +42,8 @@ const NavData: { countType: "warnings", }, { - title: "Convert DICOM to BDIS", - url: "/convert/dcm-to-bdis", + title: "Convert DICOM to BIDS", + url: "/convert/dcm-to-bids", icon: IconTransform, showCount: false, } diff --git a/apps/frontend/src/app/convert/dcm-to-bdis/page.tsx b/apps/frontend/src/app/convert/dcm-to-bdis/page.tsx index f5f34d2e..7185100a 100644 --- a/apps/frontend/src/app/convert/dcm-to-bdis/page.tsx +++ b/apps/frontend/src/app/convert/dcm-to-bdis/page.tsx @@ -2,7 +2,7 @@ const Page = () => { return (
-

DCM to BDIS Conversion

+

DCM to BIDS Conversion

This feature is under development.

); diff --git a/docs/assets/Package-Arch.svg b/docs/assets/Package-Arch.svg index 62e00c51..819d25f2 100644 --- a/docs/assets/Package-Arch.svg +++ b/docs/assets/Package-Arch.svg @@ -1,4 +1,4 @@ -
Core
IO
Modalities
Sequences
Converters
Utils
CLI
ASL
DSC
Processors
Validators
Utils
Readers
Writers
PCASL_siemens
CASL_ucl
DICOM to BDIS
Desktop App
Web App
CLI App
Tests
\ No newline at end of file +
Core
IO
Modalities
Sequences
Converters
Utils
CLI
ASL
DSC
Processors
Validators
Utils
Readers
Writers
PCASL_siemens
CASL_ucl
DICOM to BIDS
Desktop App
Web App
CLI App
Tests
\ No newline at end of file diff --git a/docs/development/architecture.mdx b/docs/development/architecture.mdx index a4306b26..fc508fe7 100644 --- a/docs/development/architecture.mdx +++ b/docs/development/architecture.mdx @@ -95,7 +95,7 @@ modalities/ #### Sequence Management -This Handles the extraction of BDIS metadata (e.g. asl.json) from DICOM for vendors and organization implementation of modalities +This Handles the extraction of BIDS metadata (e.g. asl.json) from DICOM for vendors and organization implementation of modalities ``` sequences/ diff --git a/docs/user-guide/quick-start.mdx b/docs/user-guide/quick-start.mdx index b337ac0b..af3cb8bb 100644 --- a/docs/user-guide/quick-start.mdx +++ b/docs/user-guide/quick-start.mdx @@ -58,7 +58,7 @@ If you have DICOM files: - Or drag and drop files directly 2. **Select Data Type**: - - Choose "BDIS" for BIDS format + - Choose "BIDS" for BIDS format - Choose "DICOM" for DICOM files 3. **Select Modality**: From de7bd9db06cd887bfe715127932bc33695752098 Mon Sep 17 00:00:00 2001 From: Aviral Saxena Date: Sat, 28 Feb 2026 03:08:58 +0530 Subject: [PATCH 2/7] Added end to end tests --- apps/backend/app/routers/reports.py | 13 ++- apps/backend/app/utils/sample_nifti.nii.gz | Bin 0 -> 75 bytes .../fixtures/real_sample/sub-Sub1_asl.json | 37 +++++++ .../real_sample/sub-Sub1_aslcontext.tsv | 21 ++++ .../fixtures/real_sample/sub-Sub1_m0scan.json | 23 +++++ apps/backend/tests/test_api_schema.py | 16 +-- apps/backend/tests/test_fixture_regression.py | 43 ++++++++ .../test_real_asl_fixture_regression.yml | 96 ++++++++++++++++++ apps/backend/tests/test_regression.py | 11 +- .../test_bids_endpoint_regression.yml | 17 ++++ .../test_root_endpoint_regression.yml | 17 ++++ apps/backend/tests/test_report.py | 20 ++-- .../{modaliy_enum.py => modality_enum.py} | 0 13 files changed, 281 insertions(+), 33 deletions(-) create mode 100644 apps/backend/app/utils/sample_nifti.nii.gz create mode 100644 apps/backend/tests/fixtures/real_sample/sub-Sub1_asl.json create mode 100644 apps/backend/tests/fixtures/real_sample/sub-Sub1_aslcontext.tsv create mode 100644 apps/backend/tests/fixtures/real_sample/sub-Sub1_m0scan.json create mode 100644 apps/backend/tests/test_fixture_regression.py create mode 100644 apps/backend/tests/test_fixture_regression/test_real_asl_fixture_regression.yml create mode 100644 apps/backend/tests/test_regression/test_bids_endpoint_regression.yml create mode 100644 apps/backend/tests/test_regression/test_root_endpoint_regression.yml rename package/src/pyaslreport/enums/{modaliy_enum.py => modality_enum.py} (100%) diff --git a/apps/backend/app/routers/reports.py b/apps/backend/app/routers/reports.py index 5b80ab8e..046f3a60 100644 --- a/apps/backend/app/routers/reports.py +++ b/apps/backend/app/routers/reports.py @@ -12,6 +12,8 @@ from weasyprint import HTML from app.utils.report_template import render_report_html from app.utils.lib import default_serializer, save_upload, remove_dir +from starlette.background import BackgroundTask +import os report_router = APIRouter(prefix="/report") @@ -122,12 +124,15 @@ async def get_report_dicom( @report_router.post("/report-pdf") async def download_pdf(report_data: dict): - print("--------------------------------") - print(report_data["report_data"]["asl_parameters"]) - print("--------------------------------") html_content = render_report_html(report_data["report_data"]) + with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp: HTML(string=html_content).write_pdf(tmp.name) tmp_path = tmp.name - return FileResponse(tmp_path, media_type="application/pdf", filename="report.pdf") + return FileResponse( + tmp_path, + media_type="application/pdf", + filename="report.pdf", + background=BackgroundTask(os.unlink, tmp_path) + ) \ No newline at end of file diff --git a/apps/backend/app/utils/sample_nifti.nii.gz b/apps/backend/app/utils/sample_nifti.nii.gz new file mode 100644 index 0000000000000000000000000000000000000000..4a8e8c65007b16a02a8c3f055600f4ba13493bf2 GIT binary patch literal 75 zcmb2|=3oE;mjB&}DGFQ$#v2+Gm6=% 1000 \ No newline at end of file diff --git a/package/src/pyaslreport/enums/modaliy_enum.py b/package/src/pyaslreport/enums/modality_enum.py similarity index 100% rename from package/src/pyaslreport/enums/modaliy_enum.py rename to package/src/pyaslreport/enums/modality_enum.py From 87e1f7c583f0035899106a67c634ac9a27b78f1b Mon Sep 17 00:00:00 2001 From: Aviral Saxena Date: Sat, 28 Feb 2026 03:17:56 +0530 Subject: [PATCH 3/7] Minor change --- apps/backend/tests/test_regression.py | 7 +------ .../test_bids_endpoint_regression.yml | 17 ----------------- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 apps/backend/tests/test_regression/test_bids_endpoint_regression.yml diff --git a/apps/backend/tests/test_regression.py b/apps/backend/tests/test_regression.py index e4cb6ed9..a897a965 100644 --- a/apps/backend/tests/test_regression.py +++ b/apps/backend/tests/test_regression.py @@ -20,18 +20,13 @@ def test_root_endpoint_regression(data_regression): data_regression.check(data) -def test_bids_endpoint_regression(data_regression): - response = client.get("/") - assert response.status_code == 200 - data_regression.check(response.json()) - def test_dicom_invalid_file_returns_500(tmp_path): file_path = tmp_path / "invalid.txt" file_path.write_text("not a dicom") with open(file_path, "rb") as f: response = client.post( - "/api/report/process/dicom", # fixed /api prefix + "/api/report/process/dicom", files={"dcm_files": ("invalid.txt", f, "text/plain")}, data={"modality": "ASL"}, ) diff --git a/apps/backend/tests/test_regression/test_bids_endpoint_regression.yml b/apps/backend/tests/test_regression/test_bids_endpoint_regression.yml deleted file mode 100644 index a887b6bb..00000000 --- a/apps/backend/tests/test_regression/test_bids_endpoint_regression.yml +++ /dev/null @@ -1,17 +0,0 @@ -Specs: - Framework: FastAPI - Operating System: OS Independent - Programming Language: Python -authors: -- Ibrahim Abdelazim: ibrahim.abdelazim@fau.de -- Hanliang Xu: hxu110@jh.edu -description: This service provides an API for generating ASL methods parameters based - on user input. It is designed to be used in conjunction with the ASL Methods Parameter - Generator frontend application. -license: MIT -name: ASL Methods Parameter Generator API Service -organization: The ISMRM Open Science Initiative for Perfusion Imaging -supervisors: -- Jan Petr -- David Thomas -version: 0.0.1 From 4c57001b2d76f238841686cb1b8f0ae8aad9e5a4 Mon Sep 17 00:00:00 2001 From: Aviral Saxena Date: Mon, 2 Mar 2026 00:45:29 +0530 Subject: [PATCH 4/7] Added the test coverage in CI pipeline --- .github/workflows/backend-ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 65801f3e..3a0d2ab4 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -30,14 +30,15 @@ jobs: cd apps/backend python -m pip install --upgrade pip pip install -r requirements.txt - pip install pytest pytest-regressions httpx + pip install pytest pytest-regressions httpx pytest-cov - name: Install package (editable) run: | cd package pip install -e . - - name: Run tests + - name: Run tests with coverage run: | cd apps/backend - pytest -v \ No newline at end of file + # So this command generates terminal report with missing lines AND an XML report for future Codecov integration + pytest -v --cov=app --cov-report=term-missing --cov-report=xml \ No newline at end of file From 6bbc24cf336ec605f2256bed074eaf2af22d106a Mon Sep 17 00:00:00 2001 From: Aviral Saxena Date: Mon, 2 Mar 2026 00:53:26 +0530 Subject: [PATCH 5/7] Added the test coverage in CI pipeline --- .github/workflows/backend-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 3a0d2ab4..13684cbf 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -40,5 +40,4 @@ jobs: - name: Run tests with coverage run: | cd apps/backend - # So this command generates terminal report with missing lines AND an XML report for future Codecov integration pytest -v --cov=app --cov-report=term-missing --cov-report=xml \ No newline at end of file From ab1049d743463727d083515d0531e86c85b74f86 Mon Sep 17 00:00:00 2001 From: Aviral Saxena Date: Mon, 2 Mar 2026 01:23:16 +0530 Subject: [PATCH 6/7] Dropped the coverage file from git history --- apps/backend/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore index d19cfdf6..f04cf6c8 100644 --- a/apps/backend/.gitignore +++ b/apps/backend/.gitignore @@ -2,7 +2,7 @@ __pycache__/ *.py[cod] *$py.class - +coverage.xml # C extensions *.so From 2ba95b7a4e06ff8a1e823f2db568728898a37f24 Mon Sep 17 00:00:00 2001 From: Aviral Saxena Date: Tue, 3 Mar 2026 17:49:04 +0530 Subject: [PATCH 7/7] test: establish isolated package-level unit testing architecture --- .github/workflows/backend-ci.yml | 40 ++++++++++++++++++-- package/src/pyaslreport/enums/__init__.py | 2 +- package/src/pyaslreport/sequences/factory.py | 2 +- package/tests/__init__.py | 0 package/tests/unit/__init__.py | 0 package/tests/unit/test_config.py | 30 +++++++++++++++ package/tests/unit/test_modality_enum.py | 13 +++++++ package/tests/unit/test_registry.py | 22 +++++++++++ package/tests/unit/test_unit_converter.py | 25 ++++++++++++ 9 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 package/tests/__init__.py create mode 100644 package/tests/unit/__init__.py create mode 100644 package/tests/unit/test_config.py create mode 100644 package/tests/unit/test_modality_enum.py create mode 100644 package/tests/unit/test_registry.py create mode 100644 package/tests/unit/test_unit_converter.py diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 13684cbf..ee7fe5ca 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -11,8 +11,40 @@ on: - "package/**" jobs: - test: + + # Fast Package-Level Unit Tests + package-unit-tests: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package + run: | + cd package + python -m pip install --upgrade pip + pip install -e . + pip install pytest pytest-cov pyyaml + + - name: Run package unit tests + run: | + cd package + pytest tests/unit -v --cov=pyaslreport --cov-report=term-missing + + + + #Backend Integration Tests + backend-integration-tests: runs-on: ubuntu-latest + needs: package-unit-tests strategy: matrix: @@ -25,19 +57,19 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install backend dependencies run: | cd apps/backend python -m pip install --upgrade pip pip install -r requirements.txt - pip install pytest pytest-regressions httpx pytest-cov + pip install pytest pytest-regressions pytest-cov httpx - name: Install package (editable) run: | cd package pip install -e . - - name: Run tests with coverage + - name: Run backend integration tests run: | cd apps/backend pytest -v --cov=app --cov-report=term-missing --cov-report=xml \ No newline at end of file diff --git a/package/src/pyaslreport/enums/__init__.py b/package/src/pyaslreport/enums/__init__.py index 7b6db02c..8bda5003 100644 --- a/package/src/pyaslreport/enums/__init__.py +++ b/package/src/pyaslreport/enums/__init__.py @@ -1,3 +1,3 @@ -from pyaslreport.enums.modaliy_enum import ModalityTypeValues +from pyaslreport.enums.modality_enum import ModalityTypeValues __all__ = ["ModalityTypeValues"] diff --git a/package/src/pyaslreport/sequences/factory.py b/package/src/pyaslreport/sequences/factory.py index 173185e3..1d1d1c9d 100644 --- a/package/src/pyaslreport/sequences/factory.py +++ b/package/src/pyaslreport/sequences/factory.py @@ -1,4 +1,4 @@ -from pyaslreport.enums.modaliy_enum import ModalityTypeValues +from pyaslreport.enums.modality_enum import ModalityTypeValues from pyaslreport.sequences.ge.asl import GEBasicSinglePLD, GEMultiPLD from pyaslreport.sequences.ge.dsc import GEDSCSequence from pyaslreport.sequences.siemens.asl import SiemensBasicSinglePLD diff --git a/package/tests/__init__.py b/package/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/package/tests/unit/__init__.py b/package/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/package/tests/unit/test_config.py b/package/tests/unit/test_config.py new file mode 100644 index 00000000..b74aa2f7 --- /dev/null +++ b/package/tests/unit/test_config.py @@ -0,0 +1,30 @@ +import os +import tempfile +import yaml +from pyaslreport.core.config import Config + + +def test_config_load_with_minimal_yaml(): + with tempfile.TemporaryDirectory() as tmpdir: + # Create allowed_file_types.yaml + allowed_file_types_path = os.path.join(tmpdir, "allowed_file_types.yaml") + with open(allowed_file_types_path, "w") as f: + yaml.dump({ + "allowed_file_types": ["json"], + "paths": {"input": "in", "output": "out"} + }, f) + + # Create schemas directory + schemas_dir = os.path.join(tmpdir, "schemas") + os.makedirs(schemas_dir) + + schema_file = os.path.join(schemas_dir, "test_schema.yaml") + with open(schema_file, "w") as f: + yaml.dump({"TestSchema": {"field": "value"}}, f) + + config = Config(tmpdir) + loaded = config.load() + + assert "allowed_file_type" in loaded + assert "schemas" in loaded + assert "TestSchema" in loaded["schemas"] \ No newline at end of file diff --git a/package/tests/unit/test_modality_enum.py b/package/tests/unit/test_modality_enum.py new file mode 100644 index 00000000..34a2b000 --- /dev/null +++ b/package/tests/unit/test_modality_enum.py @@ -0,0 +1,13 @@ +import pytest +from pyaslreport.enums.modality_enum import ModalityTypeValues + + +def test_enum_values(): + assert ModalityTypeValues.ASL.value == "ASL" + assert ModalityTypeValues.DCE.value == "DCE" + assert ModalityTypeValues.DSC.value == "DSC" + + +def test_invalid_enum_raises(): + with pytest.raises(ValueError): + ModalityTypeValues("INVALID") \ No newline at end of file diff --git a/package/tests/unit/test_registry.py b/package/tests/unit/test_registry.py new file mode 100644 index 00000000..1788e850 --- /dev/null +++ b/package/tests/unit/test_registry.py @@ -0,0 +1,22 @@ +import pytest +from pyaslreport.modalities.registry import ( + register_modality, + get_processor, + MODALITY_REGISTRY, +) + + +class DummyProcessor: + pass + + +def test_register_and_get_processor(): + register_modality("test_modality", processor_cls=DummyProcessor) + + processor = get_processor("test_modality") + assert processor == DummyProcessor + + +def test_get_unknown_processor_raises(): + with pytest.raises(KeyError): + get_processor("unknown_modality") \ No newline at end of file diff --git a/package/tests/unit/test_unit_converter.py b/package/tests/unit/test_unit_converter.py new file mode 100644 index 00000000..3a538526 --- /dev/null +++ b/package/tests/unit/test_unit_converter.py @@ -0,0 +1,25 @@ +from pyaslreport.utils.unit_conversion_utils import UnitConverterUtils + + +def test_convert_single_value_to_milliseconds(): + assert UnitConverterUtils.convert_to_milliseconds(1) == 1000 + assert UnitConverterUtils.convert_to_milliseconds(0.5) == 500 + + +def test_convert_list_to_milliseconds(): + result = UnitConverterUtils.convert_to_milliseconds([1, 0.5]) + assert result == [1000, 500] + + +def test_convert_milliseconds_to_seconds(): + assert UnitConverterUtils.convert_milliseconds_to_seconds(1000) == 1.0 + assert UnitConverterUtils.convert_milliseconds_to_seconds([2000, 500]) == [2.0, 0.5] + + +def test_invalid_input_raises_type_error(): + try: + UnitConverterUtils.convert_to_milliseconds("invalid") + except TypeError: + assert True + else: + assert False \ No newline at end of file