diff --git a/.readthedocs.yml b/.readthedocs.yml index b46910d..2022bb2 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,7 +5,7 @@ build: apt_packages: - graphviz tools: - python: "3.10" + python: "3.11" sphinx: configuration: docs/conf.py diff --git a/pyproject.toml b/pyproject.toml index 8fb6ef0..85fad98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,10 +13,9 @@ authors = [{name = "The NiPreps Developers", email = "nipreps@gmail.com"}] classifiers = [ "Development Status :: 2 - Pre-Alpha", "Programming Language :: Python", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] dependencies = [ "fmriprep", diff --git a/src/fmripost_aroma/data/tests/config.toml b/src/fmripost_aroma/data/tests/config.toml index 1a93dfb..b698976 100644 --- a/src/fmripost_aroma/data/tests/config.toml +++ b/src/fmripost_aroma/data/tests/config.toml @@ -9,7 +9,7 @@ templateflow_version = "0.4.2" version = "20.0.1" [execution] -bids_dir = "ds000005/" +bids_dir = "/data" bids_description_hash = "5d42e27751bbc884eca87cb4e62b9a0cca0cd86f8e578747fe89b77e6c5b21e5" boilerplate_only = false fs_license_file = "/opt/freesurfer/license.txt" diff --git a/src/fmripost_aroma/tests/conftest.py b/src/fmripost_aroma/tests/conftest.py new file mode 100644 index 0000000..5b6a773 --- /dev/null +++ b/src/fmripost_aroma/tests/conftest.py @@ -0,0 +1,84 @@ +"""Fixtures for the CircleCI tests.""" + +import base64 +import os + +import pytest + + +def pytest_addoption(parser): + """Collect pytest parameters for running tests.""" + parser.addoption( + "--working_dir", + action="store", + default=( + "/usr/local/miniconda/lib/python3.11/site-packages/fmripost_aroma/fmripost_aroma/" + "tests/data/test_data/run_pytests/work" + ), + ) + parser.addoption( + "--data_dir", + action="store", + default=( + "/usr/local/miniconda/lib/python3.11/site-packages/fmripost_aroma/fmripost_aroma/" + "tests/data/test_data" + ), + ) + parser.addoption( + "--output_dir", + action="store", + default=( + "/usr/local/miniconda/lib/python3.11/site-packages/fmripost_aroma/fmripost_aroma/" + "tests/data/test_data/run_pytests/out" + ), + ) + + +# Set up the commandline options as fixtures +@pytest.fixture(scope="session") +def data_dir(request): + """Grab data directory.""" + return request.config.getoption("--data_dir") + + +@pytest.fixture(scope="session") +def working_dir(request): + """Grab working directory.""" + workdir = request.config.getoption("--working_dir") + os.makedirs(workdir, exist_ok=True) + return workdir + + +@pytest.fixture(scope="session") +def output_dir(request): + """Grab output directory.""" + outdir = request.config.getoption("--output_dir") + os.makedirs(outdir, exist_ok=True) + return outdir + + +@pytest.fixture(scope="session", autouse=True) +def fslicense(working_dir): + """Set the FreeSurfer license as an environment variable.""" + FS_LICENSE = os.path.join(working_dir, "license.txt") + os.environ["FS_LICENSE"] = FS_LICENSE + LICENSE_CODE = ( + "bWF0dGhldy5jaWVzbGFrQHBzeWNoLnVjc2IuZWR1CjIwNzA2CipDZmVWZEg1VVQ4clkKRlNCWVouVWtlVElDdwo=" + ) + with open(FS_LICENSE, "w") as f: + f.write(base64.b64decode(LICENSE_CODE).decode()) + + +@pytest.fixture(scope="session") +def base_config(): + from fmripost_aroma.tests.tests import mock_config + + return mock_config + + +@pytest.fixture(scope="session") +def base_parser(): + from argparse import ArgumentParser + + parser = ArgumentParser(description="Test parser") + return parser diff --git a/src/fmripost_aroma/tests/run_local_tests.py b/src/fmripost_aroma/tests/run_local_tests.py new file mode 100644 index 0000000..3486be1 --- /dev/null +++ b/src/fmripost_aroma/tests/run_local_tests.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Run tests locally by calling Docker.""" +import argparse +import os +import subprocess + + +def _get_parser(): + """Parse command line inputs for tests. + + Returns + ------- + parser.parse_args() : argparse dict + """ + parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument( + "-k", + dest="test_regex", + metavar="PATTERN", + type=str, + help="Test pattern.", + required=False, + default=None, + ) + parser.add_argument( + "-m", + dest="test_mark", + metavar="LABEL", + type=str, + help="Test mark label.", + required=False, + default=None, + ) + return parser + + +def run_command(command, env=None): + """Run a given shell command with certain environment variables set. + + Keep this out of the real xcp_d code so that devs don't need to install ASLPrep to run tests. + """ + merged_env = os.environ + if env: + merged_env.update(env) + + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, + env=merged_env, + ) + while True: + line = process.stdout.readline() + line = str(line, "utf-8")[:-1] + print(line) + if line == "" and process.poll() is not None: + break + + if process.returncode != 0: + raise RuntimeError( + f"Non zero return code: {process.returncode}\n" f"{command}\n\n{process.stdout.read()}" + ) + + +def run_tests(test_regex, test_mark): + """Run the tests.""" + local_patch = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + mounted_code = "/usr/local/miniconda/lib/python3.11/site-packages/fmripost_aroma" + run_str = "docker run --rm -ti " + run_str += f"-v {local_patch}:{mounted_code} " + run_str += "--entrypoint pytest " + run_str += "nipreps/fmripost_aroma:unstable " + run_str += ( + f"{mounted_code}/fmripost_aroma " + f"--data_dir={mounted_code}/fmripost_aroma/tests/test_data " + f"--output_dir={mounted_code}/fmripost_aroma/tests/pytests/out " + f"--working_dir={mounted_code}/fmripost_aroma/tests/pytests/work " + ) + if test_regex: + run_str += f"-k {test_regex} " + elif test_mark: + run_str += f"-rP -o log_cli=true -m {test_mark} " + + run_command(run_str) + + +def _main(argv=None): + """Run the tests.""" + options = _get_parser().parse_args(argv) + kwargs = vars(options) + run_tests(**kwargs) + + +if __name__ == "__main__": + _main() diff --git a/src/fmripost_aroma/workflows/tests/test_base.py b/src/fmripost_aroma/tests/test_base.py similarity index 100% rename from src/fmripost_aroma/workflows/tests/test_base.py rename to src/fmripost_aroma/tests/test_base.py diff --git a/src/fmripost_aroma/workflows/tests/__init__.py b/src/fmripost_aroma/workflows/tests/__init__.py deleted file mode 100644 index 74d2074..0000000 --- a/src/fmripost_aroma/workflows/tests/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -# -# Copyright The NiPreps Developers -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# We support and encourage derived works from this project, please read -# about our expectations at -# -# https://www.nipreps.org/community/licensing/ -# -"""Utilities and mocks for testing and documentation building.""" - -import os -import shutil -from contextlib import contextmanager -from pathlib import Path -from tempfile import mkdtemp - -from toml import loads - -from ... import data - - -@contextmanager -def mock_config(bids_dir=None): - """Create a mock config for documentation and testing purposes.""" - from ... import config - - _old_fs = os.getenv('FREESURFER_HOME') - if not _old_fs: - os.environ['FREESURFER_HOME'] = mkdtemp() - - settings = loads(data.load.readable('tests/config.toml').read_text()) - for sectionname, configs in settings.items(): - if sectionname != 'environment': - section = getattr(config, sectionname) - section.load(configs, init=False) - config.nipype.omp_nthreads = 1 - config.nipype.init() - config.loggers.init() - config.init_spaces() - - bids_dir = bids_dir or data.load('tests/ds000005').absolute() - - config.execution.work_dir = Path(mkdtemp()) - config.execution.bids_dir = bids_dir - config.execution.fmriprep_dir = Path(mkdtemp()) - config.execution.bids_database_dir = None - config.execution._layout = None - config.execution.init() - - yield - - shutil.rmtree(config.execution.work_dir) - shutil.rmtree(config.execution.fmriprep_dir) - - if not _old_fs: - del os.environ['FREESURFER_HOME']