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

Testing is good #19

Merged
merged 13 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
26 changes: 26 additions & 0 deletions .github/workflows/petdeface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: petdeface
on:
workflow_call:
workflow_dispatch:
inputs:
debug_enabled:
type: boolean
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false

jobs:
test_petdeface:
runs-on: ubuntu-latest
name: Test petdeface
steps:
- uses: actions/checkout@v3
- name: Install Poetry
run: pipx install poetry
- uses: actions/setup-python@v4
with:
python-version: 3.11
cache: poetry
- run: poetry install
- name: Run All Tests
run: poetry run make testall
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,13 @@ scratch.txt
# ignore scratch folder
scratch/

# ignore scripts folder, for now
scripts/

# integration testing scripts using unpublishable dicoms
integration_testing.sh

petdeface/Dockerfile
petdeface/README.md
petdeface/pyproject.toml
petdeface/README.md
bendhouseart marked this conversation as resolved.
Show resolved Hide resolved
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Quickly run black on all python files in this repository, local version of the pre-commit hook
black:
for file in `find . -name "*.py"`; do \
black $$file; \
done

testlayoutsverbose:
pytest tests/test_dir_layouts.py -s -vv

testlayouts:
pytest tests/test_dir_layouts.py

# run all tests
testall: testlayouts
.PHONY: testall

# install python dependencies
pythondeps:
pip install --upgrade pip && pip install -e .
10 changes: 7 additions & 3 deletions petdeface/petdeface.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,11 @@ def init_single_subject_wf(subject_id: str, bids_dir: str) -> Workflow:
t1w_wf = Workflow(name="t1w_wf")
unique_t1w_best_matches = sorted(set(t1w_best_matches))
for j, t1w_file in enumerate(unique_t1w_best_matches):
ses_id = re.search("/ses-(.+?)/", t1w_file).group(1)
ses_id = re.search("/ses-(.+?)/", t1w_file)
if ses_id:
ses_id = f".ses-{ses_id.group(1)}_"
bendhouseart marked this conversation as resolved.
Show resolved Hide resolved
else:
ses_id = ""
deface_t1w = Node(
Mideface(in_file=t1w_file),
name=f"deface_t1w{j}",
Expand All @@ -264,10 +268,10 @@ def init_single_subject_wf(subject_id: str, bids_dir: str) -> Workflow:
deface_t1w,
datasink,
[
("out_file", f"sub-{subject_id}.ses-{ses_id}.anat"),
("out_file", f"sub-{subject_id}{ses_id}.anat"),
(
"out_facemask",
f"sub-{subject_id}.ses-{ses_id}.anat.@facemask",
f"sub-{subject_id}{ses_id}.anat.@facemask",
),
],
),
Expand Down
667 changes: 406 additions & 261 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ black = "^23.7.0"
flake8 = "^6.1.0"
isort = "^5.12.0"
pre-commit = "^3.3.3"
pytest = "^7.4.2"

[tool.isort]
profile = "black"
Expand Down
46 changes: 46 additions & 0 deletions scripts/integration_testing.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#! /usr/bin/env bash

SCRIPT_PATH=$(dirname $(realpath -s $0))
REPO_PATH=$(dirname $SCRIPT_PATH)

while getopts 'r' flag
do
case "$flag" in
r) rebuildimage=true ;;
esac

case "$flag" in
o) outputdir=${OPTARG} ;;
esac

done

if [[ -n "$rebuildimage" ]]
#if $rebuildimage
then
echo "rebuilding docker image"
pushd $REPO_PATH
docker build . -t petdeface
else
echo "Using existing petdeface docker image"
fi

shift $(($OPTIND - 1))
INPUT_DIR=$1
OUTPUT_DIR=$2

echo Removing contents of existing output directory at $OUTPUT_DIR
mkdir -p $OUTPUT_DIR
rm -rf $OUTPUT_DIR/*

echo running the following command:
echo python3 petdeface/petdeface.py $INPUT_DIR \
--output $OUTPUT_DIR \
--n_procs 16 \
--skip_bids_validator \


python3 petdeface/petdeface.py $INPUT_DIR \
--output $OUTPUT_DIR \
--n_procs 16 \
--skip_bids_validator \
120 changes: 120 additions & 0 deletions tests/test_dir_layouts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import pytest
from pathlib import Path
import shutil
import bids
Copy link
Contributor

Choose a reason for hiding this comment

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

L1 and L4 can be deleted as these imports are not being used

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think pytest might be as I use a fixture from it, and bids will be used to validate that the outputs look correct.

from petdeface.petdeface import PetDeface
from os import cpu_count
from bids.layout import BIDSLayout

import tempfile
Copy link
Contributor

Choose a reason for hiding this comment

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

pytest has temp dir fixtures (https://docs.pytest.org/en/6.2.x/tmpdir.html); not sure if that would be more advantageous to use than the tempfile based implementation

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, pytest has everything, but despite my owning a book on it I'm not wholly familiar. I think we could move to using exclusively pytest, but for the moment it works and I'm able to easily inspect the temp dir if something fails.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay, this is getting out of hand. I've gone ahead and created a petutils library that includes the sort of directory tree's we're intent on testing on. It has it's own fixtures that use pytest's tmpdir. Going to just merge this and switch over directory creation to the fixtures contained in in openneuropet/petutils for further development.


# collect test bids dataset from data directory
data_dir = Path(__file__).parent.parent / "data"
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we consider downsampling the test data to speed up the tests?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

From my understanding it's a registration issue with the current test data, either way I think we should optimize later and push through with it now.


# get number of cores, use all but one
nthreads = cpu_count() - 1

layout = BIDSLayout(data_dir, validate=True)

if layout:
pass


def test_anat_in_first_session_folder():
# create a temporary directory to copy the existing dataset into
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(data_dir, Path(tmpdir) / "anat_in_first_session_folder")

# run petdeface on the copied dataset
petdeface = PetDeface(
Path(tmpdir) / "anat_in_first_session_folder",
output_dir=Path(tmpdir)
/ "anat_in_first_session_folder_defaced"
/ "derivatives"
/ "petdeface",
n_procs=nthreads,
)
petdeface.run()


def test_anat_in_each_session_folder():
# create a temporary directory to copy the existing dataset into
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(data_dir, Path(tmpdir) / "anat_in_each_session_folder")

# create a second session
second_session_folder = (
Path(tmpdir) / "anat_in_each_session_folder" / "sub-01" / "ses-second"
)
second_session_folder.mkdir(parents=True, exist_ok=True)

shutil.copytree(
Path(tmpdir) / "anat_in_each_session_folder" / "sub-01" / "ses-baseline",
second_session_folder,
dirs_exist_ok=True,
)

# replace the ses- entities in the files in the newly created second session folder
for file in second_session_folder.glob("pet/*"):
shutil.move(
file,
second_session_folder
/ "pet"
/ file.name.replace("ses-baseline_", "ses-second_"),
)

for file in second_session_folder.glob("anat/*"):
shutil.move(
file,
second_session_folder
/ "anat"
/ file.name.replace("ses-baseline_", "ses-second_"),
)

# run petdeface on the copied dataset
petdeface = PetDeface(
Path(tmpdir) / "anat_in_each_session_folder",
output_dir=Path(tmpdir)
/ "anat_in_each_session_folder_defaced"
/ "derivatives"
/ "petdeface",
n_procs=nthreads,
)
petdeface.run()

Copy link
Contributor

Choose a reason for hiding this comment

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

It would be good to check if the defaced PET file(s) is/are created.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

See my comment about the mask files, we're going to need to slightly modify the pipeline to output nifti masks instead of mgz, but that's irrelevant for the purposes of this branch/PR. I'd like to add all the tests even if it means everything is failing.


def test_anat_in_subject_folder():
# create a temporary directory to copy the existing dataset into
with tempfile.TemporaryDirectory() as tmpdir:
shutil.copytree(data_dir, Path(tmpdir) / "anat_in_subject_folder")

original_anat_folder = (
Path(tmpdir) / "anat_in_subject_folder" / "sub-01" / "ses-baseline" / "anat"
)
subject_folder = Path(tmpdir) / "anat_in_subject_folder" / "sub-01"
# now we move the anatomical folder in the first session of our test data into the subject level folder
shutil.move(original_anat_folder, subject_folder)

# and next remove the ses- entities from the files in the newly created anat folder
for file in Path(tmpdir).glob(
"anat_in_subject_folder/sub-01/anat/sub-01_ses-baseline_*"
):
shutil.move(
file,
Path(tmpdir)
/ "anat_in_subject_folder"
/ "sub-01"
/ "anat"
/ file.name.replace("ses-baseline_", ""),
)

# run petdeface on the copied dataset
petdeface = PetDeface(
Path(tmpdir) / "anat_in_subject_folder",
output_dir=Path(tmpdir)
/ "anat_in_subject_folder_defaced"
/ "derivatives"
/ "petdeface",
n_procs=nthreads,
)
petdeface.run()