Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: allow for parallel test execution #180

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,4 @@ data/**

test_data

*.lock
3 changes: 2 additions & 1 deletion config/coverage.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ omit = [
"src/imgtools/logging/**/*.py",
"src/imgtools/cli/**/*.py",
"src/imgtools/dicom/index/**/*.py",
"tests/**/*.py",
]

[tool.coverage.report]
[tool.coverage.report]
13 changes: 12 additions & 1 deletion config/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ addopts =
--showlocals
# Generate coverage report
# Tracks code coverage during test execution
--cov=imgtools
--cov
# Output coverage report in terminal
# Provides immediate feedback on coverage
--cov-report=term-missing
Expand All @@ -27,6 +27,17 @@ addopts =
# Point to coverage config file
# Allows customization of coverage report generation
--cov-config=config/coverage.toml
# Append coverage data from previous runs
# Ensures coverage data is appended
--cov-append
# numprocessors to use for xdist plugin
# Sets number of processors to use for parallel test execution
--numprocesses=auto
# max processes
# Sets maximum number of processes to use for parallel test execution
--maxprocesses=8
# group xdist
--dist=loadgroup

# Patterns for test discovery
# Defines which files are considered test files
Expand Down
12 changes: 10 additions & 2 deletions pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pytest-cov = "*"
pytest-xdist = "*"
pytest-mock = ">=3.14.0,<4"
sqlalchemy-stubs = ">=0.4,<0.5"
filelock = ">=3.16.1,<4"

[feature.test.pypi-dependencies]
med-imagetools = { path = ".", editable = true }
Expand All @@ -64,6 +65,10 @@ inputs = ["coverage-report/coverage.xml", "config/coverage.toml"]
depends-on = ["test"]
description = "Run pytest and generate coverage report"

[feature.test.tasks.clean_tests]
cmd = "rm -rf .pytest_cache ./data ./tests/temp"
description = "Clean up the test cache and data"

############################################## DOCS ###############################################
[feature.docs.dependencies]
mkdocs = "*"
Expand Down
42 changes: 41 additions & 1 deletion src/imgtools/ops/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,41 @@
from .ops import *
from .ops import (
BaseInput,
BaseOp,
BaseOutput,
CentreCrop,
ClipIntensity,
Crop,
HDF5Output,
ImageAutoInput,
ImageAutoOutput,
ImageStatistics,
MinMaxScale,
NumpyOutput,
Resample,
Resize,
StandardScale,
StructureSetToSegmentation,
WindowIntensity,
Zoom,
)

__all__ = [
"ImageAutoInput",
"ImageAutoOutput",
"StructureSetToSegmentation",
"BaseOp",
"BaseInput",
"BaseOutput",
"CentreCrop",
"ClipIntensity",
"Crop",
"HDF5Output",
"ImageStatistics",
"MinMaxScale",
"NumpyOutput",
"Resample",
"Resize",
"StandardScale",
"WindowIntensity",
"Zoom",
]
95 changes: 47 additions & 48 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,67 @@
from zipfile import ZipFile

import pytest
from filelock import FileLock
jjjermiah marked this conversation as resolved.
Show resolved Hide resolved

from imgtools.logging import logger


@pytest.fixture(scope='session')
@pytest.fixture(scope="package")
def curr_path():
return pathlib.Path(__file__).parent.parent.resolve().as_posix()
return pathlib.Path().cwd().resolve().absolute()


@pytest.fixture(scope='session')
def dataset_path(curr_path):
quebec_path = pathlib.Path(curr_path, 'data', 'Head-Neck-PET-CT')
@pytest.fixture(scope="session")
def prepare_dataset():
"""Prepares the dataset if not already downloaded."""
curr_path = pathlib.Path().cwd().resolve().absolute()
quebec_path = pathlib.Path(curr_path, "data", "Head-Neck-PET-CT").absolute()

if not (quebec_path.exists() and len(list(quebec_path.glob('*'))) == 2):
quebec_path.mkdir(parents=True, exist_ok=True)
# when running xdist, use lockfile to prevent all processors from trying to download the dataset
lock_path = quebec_path / ".dataset.lock"

# Download QC dataset
logger.info('Downloading the test dataset...')
quebec_data_url = (
'https://github.com/bhklab/tcia_samples/blob/main/Head-Neck-PET-CT.zip?raw=true'
with FileLock(lock_path):
logger.info(
"Checking if the test dataset is downloaded...",
curr_path=curr_path,
quebec_path=quebec_path,
)
quebec_zip_path = pathlib.Path(quebec_path, 'Head-Neck-PET-CT.zip').as_posix()
request.urlretrieve(quebec_data_url, quebec_zip_path)
with ZipFile(quebec_zip_path, 'r') as zipfile:
zipfile.extractall(quebec_path)
os.remove(quebec_zip_path)
else:
logger.info('Data already downloaded...')
if not (quebec_path.exists() and len(list(quebec_path.glob("*"))) == 2):
quebec_path.mkdir(parents=True, exist_ok=True)

output_path = pathlib.Path(curr_path, 'tests', 'temp').as_posix()
quebec_path = quebec_path.as_posix()
# Download QC dataset
logger.info("Downloading the test dataset...")
quebec_data_url = "https://github.com/bhklab/tcia_samples/blob/main/Head-Neck-PET-CT.zip?raw=true"
quebec_zip_path = pathlib.Path(
quebec_path, "Head-Neck-PET-CT.zip"
).as_posix()
request.urlretrieve(quebec_data_url, quebec_zip_path)
with ZipFile(quebec_zip_path, "r") as zipfile:
zipfile.extractall(quebec_path)
os.remove(quebec_zip_path)
else:
logger.info("Data already downloaded...")

# Dataset name
dataset_name = os.path.basename(quebec_path)
imgtools_path = pathlib.Path(os.path.dirname(quebec_path), '.imgtools')

# Defining paths for autopipeline and dataset component
crawl_path = pathlib.Path(imgtools_path, f'imgtools_{dataset_name}.csv').as_posix()
edge_path = pathlib.Path(imgtools_path, f'imgtools_{dataset_name}_edges.csv').as_posix()
# json_path = pathlib.Path(imgtools_path, f"imgtools_{dataset_name}.json").as_posix() # noqa: F841
yield quebec_path

yield quebec_path, output_path, crawl_path, edge_path
# Delete the lock file
if lock_path.exists():
lock_path.unlink()


@pytest.fixture(scope='session')
def modalities_path(curr_path):
qc_path = pathlib.Path(curr_path, 'data', 'Head-Neck-PET-CT', 'HN-CHUS-052')
assert qc_path.exists(), 'Dataset not found'
@pytest.fixture(scope="package")
def dataset_path(prepare_dataset):
"""Provides paths related to the dataset for tests."""
curr_path = pathlib.Path().cwd().resolve().absolute()
output_path = pathlib.Path(curr_path, "tests", "temp").as_posix()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use pytest's built-in temporary directory fixtures for better test isolation.

Instead of using a fixed 'temp' directory, consider using pytest's tmp_path or tmp_path_factory fixtures. This would provide better isolation for parallel test execution and automatic cleanup.

Example refactor:

-    output_path = pathlib.Path(curr_path, "tests", "temp").as_posix()
+    @pytest.fixture(scope="package")
+    def output_path(tmp_path_factory):
+        return tmp_path_factory.mktemp("output").as_posix()

Committable suggestion skipped: line range outside the PR's diff.


path = {}
path['CT'] = pathlib.Path(
qc_path, '08-27-1885-CA ORL FDG TEP POS TX-94629/3.000000-Merged-06362'
).as_posix()
path['RTSTRUCT'] = pathlib.Path(
qc_path,
'08-27-1885-OrophCB.0OrophCBTRTID derived StudyInstanceUID.-94629/Pinnacle POI-41418',
).as_posix()
path['RTDOSE'] = pathlib.Path(
qc_path,
'08-27-1885-OrophCB.0OrophCBTRTID derived StudyInstanceUID.-94629/11376',
).as_posix()
path['PT'] = pathlib.Path(
qc_path, '08-27-1885-CA ORL FDG TEP POS TX-94629/532790.000000-LOR-RAMLA-44600'
# Paths
quebec_path = prepare_dataset.as_posix()
dataset_name = os.path.basename(quebec_path)
imgtools_path = pathlib.Path(os.path.dirname(quebec_path), ".imgtools")
crawl_path = pathlib.Path(imgtools_path, f"imgtools_{dataset_name}.csv").as_posix()
edge_path = pathlib.Path(
imgtools_path, f"imgtools_{dataset_name}_edges.csv"
).as_posix()
return path

yield quebec_path, output_path, crawl_path, edge_path
Loading
Loading