Contributing
Contributions are welcome! Please report issues or submit pull requests on GitHub.
Development setup
Clone the repository:
git clone https://github.com/mathesong/petfit.git cd petfit
Install the package in development mode:
# Install dependencies remotes::install_deps() # Load the package for development devtools::load_all()
Open the RStudio project (
petfit.Rproj) for the best development experience.
Repository structure
petfit/
├── R/ # Package source code
│ ├── region_definition_app.R # Region definition Shiny app
│ ├── modelling_plasma_app.R # Plasma input modelling app
│ ├── modelling_ref_app.R # Reference tissue modelling app
│ ├── region_utils.R # Region processing utilities
│ ├── pipeline_core.R # Core pipeline execution
│ ├── docker_functions.R # Docker/container orchestration
│ ├── report_generation.R # Report template management
│ ├── subsetting_utils.R # Data subsetting
│ ├── ancillary_utils.R # Ancillary analysis folder utilities
│ ├── blood_utils.R # Blood data handling
│ ├── bids_utils.R # BIDS attribute parsing
│ ├── launch_apps.R # App launcher function
│ └── ...
├── man/ # Auto-generated roxygen2 documentation
├── inst/rmd/ # Parameterised report templates (Rmd)
├── tests/testthat/ # Unit and integration tests
├── docker/ # Docker configuration
├── apptainer/ # Apptainer configuration
├── docs/ # Sphinx documentation (this site)
├── DESCRIPTION # R package metadata
├── NAMESPACE # Exported functions
└── CLAUDE.md # Developer architecture guide
Coding standards
Tidyverse conventions
PETFit follows tidyverse conventions throughout:
Use
tibble()instead ofdata.frame()Use
purrrfunctions (map(),walk()) instead ofapply()familyUse
stringr(str_detect(),str_replace()) instead of base R string functionsUse
dplyrverbs (mutate(),filter(),select()) for data manipulationLoad
library(tidyverse)in report templates
File I/O
TSV/CSV files: Use
readr::read_tsv()andreadr::write_tsv()(not base Rread.table()/write.table())JSON files: Use
jsonlitewithauto_unbox = TRUEfor configuration filesWhy:
readrpreserves character types (subject"01"stays as"01") and handles column names with hyphens correctly
Spelling
Use British English: “visualisation” not “visualization”, “colour” not “color”, “analyse” not “analyze”.
Documentation
Functions use roxygen2 documentation. After modifying function documentation, regenerate with:
devtools::document()
Testing
PETFit has two categories of tests: unit tests (fast, no external data) and integration tests (use real PET data, disabled by default). Both use the testthat framework.
Running tests
Unit tests only (fast, no setup needed):
Rscript -e "devtools::test()"
Full integration battery (R-native, no containers):
PETFIT_INTEGRATION_TESTS=true \
Rscript -e "devtools::test()"
This runs all unit tests plus all R-native integration tests (region definition, plasma modelling, reference modelling, ancillary inheritance, parallel processing).
Full integration battery with Docker:
PETFIT_INTEGRATION_TESTS=true \
PETFIT_DOCKER_TESTS=true \
Rscript -e "devtools::test()"
Full integration battery with Apptainer:
PETFIT_INTEGRATION_TESTS=true \
PETFIT_APPTAINER_TESTS=true \
Rscript -e "devtools::test()"
Everything (R-native + Docker + Apptainer):
PETFIT_INTEGRATION_TESTS=true \
PETFIT_DOCKER_TESTS=true \
PETFIT_APPTAINER_TESTS=true \
Rscript -e "devtools::test()"
Single test file (useful during development):
# A specific unit test
Rscript -e "devtools::test(filter = 'bids_utils')"
# A specific integration test
PETFIT_INTEGRATION_TESTS=true \
Rscript -e "devtools::test(filter = 'integration-regiondef')"
Persistent cache (avoids re-extracting test data between runs):
PETFIT_INTEGRATION_TESTS=true \
PETFIT_INTEGRATION_CACHE=/tmp/petfit_cache \
Rscript -e "devtools::test()"
Unit tests
Unit tests are in tests/testthat/ with the naming convention test-<module>.R. They run by default with devtools::test() and require no external data or environment variables.
Current unit test files:
File |
What it tests |
|---|---|
|
BIDS attribute formatting, PET identifier extraction |
|
Blood file detection, status reporting, pattern matching |
|
Semicolon parsing, data filtering, individual TACs creation |
|
Ancillary folder validation, helper utilities |
|
Model-to-template mapping, reports summary generation |
|
Interactive app launcher validation |
|
Automatic pipeline launcher validation |
Unit test pattern
Each unit test follows this structure:
test_that("function does X correctly", {
# 1. Create isolated temp directory if needed
temp_dir <- withr::local_tempdir()
# 2. Set up test data
test_data <- tibble::tibble(
sub = c("01", "02"),
ses = c("01", "02"),
region = c("Frontal", "Temporal")
)
# 3. Call the function under test
result <- function_under_test(test_data)
# 4. Assert expectations
expect_type(result, "character")
expect_equal(length(result), 2)
expect_true(all(grepl("sub-", result)))
})
Key patterns:
Use
withr::local_tempdir()for temporary directories (automatically cleaned up).Use
tibble::tibble()for test data (notdata.frame()).Use
readr::write_tsv()/readr::read_tsv()for file I/O in tests.Test edge cases:
NULLinputs, empty strings, missing columns.
Writing a new unit test
Create a file named
test-<module>.Rintests/testthat/.Write
test_that()blocks following the pattern above.Run with
Rscript -e "devtools::test(filter = '<module>')".
Integration tests
Integration tests verify full PETFit pipelines end-to-end using real PET data from OpenNeuro ds004869 (COX-2 PET, 27 subjects, C-11 tracer). They are disabled by default and gated behind environment variables.
Current integration test files:
File |
What it tests |
|---|---|
|
Test data extraction, file counts, readability |
|
|
|
Plasma pipeline: datadef, weights, delay, 2TCM model |
|
Reference pipeline: datadef, reference TAC, SRTM model |
|
Ancillary folder inheritance: delay and k2prime |
|
Parallel processing (R-native) |
|
Docker container execution |
|
Apptainer container execution |
|
Parallel processing in Docker |
|
Parallel processing in Apptainer |
Environment variables
Variable |
Purpose |
|---|---|
|
Enable R-native integration tests |
|
Explicit path to |
|
Persistent cache directory for extracted data |
|
Enable Docker container tests |
|
Rebuild Docker image before testing |
|
Enable Apptainer tests |
|
Explicit path to |
Test data
The test data tarball (ds004869_testdata.tar.gz, ~2.7 MB) is committed to the repository at tests/testthat/fixtures/integration/. It contains real TSV/JSON files from OpenNeuro ds004869 with NIfTI files replaced by empty placeholders.
At test time, the ensure_testdata() helper extracts the tarball. It searches for it in order:
PETFIT_TESTDATA_PATHenvironment variableLocal fixtures directory (normal case)
GitHub Release download (fallback)
To regenerate the tarball from scratch (requires datalad):
cd tests/testthat/fixtures/integration && bash prepare_testdata.sh
Workspace isolation
Each integration test creates an isolated workspace via create_integration_workspace():
Symlinks
petprepderivatives as read-only source data.Creates a writable
derivatives/petfit/directory for outputs.Cleaned up automatically via
withr::defer(cleanup_workspace(ws)).
This ensures tests never modify the shared test data and cannot interfere with each other.
Config fixtures
Pre-built configuration files for testing are in tests/testthat/fixtures/integration/:
File |
Pipeline |
Model |
|---|---|---|
|
Region definition |
4 regions: Frontal, Temporal, Cerebellum, WhiteMatter |
|
Plasma input |
2TCM (2 subjects, 1TCM delay) |
|
Reference tissue |
SRTM (2 subjects, Cerebellum reference) |
Integration test pattern
Each integration test follows this structure:
# File header describing what this test covers
# Requires: PETFIT_INTEGRATION_TESTS=true
test_that("pipeline produces expected output", {
skip_if_no_integration()
# 1. Get test data (extracted once per session, cached)
dataset_dir <- ensure_testdata()
# 2. Create isolated workspace
ws <- create_integration_workspace(dataset_dir)
withr::defer(cleanup_workspace(ws))
# 3. Install config fixtures
setup_regiondef_config(ws)
# For modelling tests:
# setup_modelling_config(ws, "ds004869_plasma_config.json")
# 4. Run the pipeline
result <- petfit_regiondef_auto(
bids_dir = ws$bids_dir,
derivatives_dir = ws$derivatives_dir
)
# 5. Assert success
expect_true(result$success, info = paste(result$messages, collapse = "\n"))
# 6. Verify outputs
expect_true(file.exists(result$output_file))
data <- readr::read_tsv(result$output_file, show_col_types = FALSE)
expect_true("region" %in% names(data))
expect_equal(length(unique(data$region)), 4)
})
Key patterns:
Always start with
skip_if_no_integration().Always use
create_integration_workspace()+withr::defer(cleanup_workspace(ws)).Use
info = paste(result$messages, collapse = "\n")in assertions for debugging context.For modelling tests, run region definition first (or use
setup_modelling_config()which assumes regions already exist).
Writing a new integration test
Create a file named
test-integration-<name>.Rintests/testthat/.Add a file header comment describing what the test covers and which environment variable is required.
Create a JSON config fixture in
tests/testthat/fixtures/integration/if needed (copy from an existing one and modify).Write
test_that()blocks using the pattern above.Run with:
PETFIT_INTEGRATION_TESTS=true Rscript -e "devtools::test(filter = 'integration-<name>')"
Config fixture gotchas
BIDS description ordering:
petfit_regions.tsvdescription column must useseg-gtm_desc-preproc(notdesc-preproc_seg-gtm). Thecreate_bids_key_value_pairs()function givesseg/labelpriority, then sorts remaining keys alphabetically.Reference region must be in subsetting: If
ReferenceTAC.regionis"Cerebellum", thenSubsetting.Regionsmust include"Cerebellum".Delay set to zero: When
FitDelay.modelis"Set to zero...", the delay step is skipped but model reports independently load blood data from raw BIDS_blood.tsvfiles.
Test infrastructure
tests/testthat/helper-integration.R provides all the shared utilities:
Function |
Purpose |
|---|---|
|
Skip test if |
|
Skip test if Docker not available |
|
Skip test if Apptainer not available |
|
Extract test data tarball (cached per session) |
|
Create isolated workspace with symlinks |
|
Remove temporary workspace |
|
Copy |
|
Copy JSON config fixture to analysis folder |
|
Execute Docker container with proper mounts |
|
Execute Apptainer container with bind mounts |
tests/testthat/helper-setup.R loads the here package and sets the package root directory.
CI/CD
Integration tests run automatically on GitHub Actions (.github/workflows/integration-tests.yml) with three parallel jobs:
R-native — Uses test data from the repository, runs all integration tests.
Docker — Builds the Docker image with layer caching, runs container tests.
Apptainer — Installs Apptainer, builds from Docker image, runs Apptainer tests.
The workflow triggers on pushes to main, pull requests, and manual dispatch.
Configuration management
When adding new features to the modelling apps, always ensure backward compatibility with existing JSON configuration files:
Use null coalescing (
%||%) when accessing new config propertiesProvide sensible defaults for missing sections
Handle missing or invalid data gracefully
Add UI update logic for any new input fields
Building documentation
The documentation uses Sphinx with MyST (Markdown). See the docs/README.md for full instructions on editing and building the documentation locally.
Quick start:
pip install -r docs/requirements.txt
cd docs
make html
# Open _build/html/index.html in your browser
When making code changes, please update the relevant documentation pages as well.