Skip to content

Commit 0bc2517

Browse files
committed
Align longitudinal integration test with test_all.py style, add -vv
- Mirror test_all.py: shared _rbc_exe / _run_rbc helpers, session-scoped fixtures own the subprocess invocations, the test itself only asserts on the resulting BIDS tree. Adds a longitudinal_template_output fixture so Stages 3-6 can reuse the template run without paying the cost again. - Pass -vv to every rbc subprocess so the styx runner logger runs at DEBUG and container stdout/stderr reach the captured subprocess streams. Without this, a tool that exits non-zero with its own error output leaves only a bare returncode to diagnose from (styxcache discards the staging stderr.gz on exception, so the post-hoc cache dump approach can't recover it either). - Keep the 7200s per-call timeout; bump the stderr capture window from 2000 to 4000 chars to accommodate DEBUG-level logs.
1 parent 57d0fbc commit 0bc2517

2 files changed

Lines changed: 79 additions & 64 deletions

File tree

tests/integration/longitudinal/conftest.py

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,61 @@
11
"""Fixtures for longitudinal integration tests.
22
3-
These fixtures own the ds000114 test dataset lifecycle (download + reuse)
4-
and run the cross-sectional anatomical stage once so that each
5-
longitudinal test can build on ``desc-brain`` T1w derivatives without
6-
paying the preprocessing cost per test.
3+
Mirrors the subprocess-driven style in ``tests/integration/test_all.py``:
4+
session-scoped fixtures run each ``rbc`` invocation once and return the
5+
output directory; tests just assert on the resulting BIDS tree.
76
"""
87

98
from __future__ import annotations
109

1110
import shutil
1211
import subprocess
1312
from pathlib import Path
13+
from typing import TYPE_CHECKING
1414

1515
import pytest
1616

17+
if TYPE_CHECKING:
18+
from collections.abc import Sequence
19+
1720
_REPO_ROOT = Path(__file__).resolve().parents[3]
1821
_DOWNLOAD_SCRIPT = _REPO_ROOT / "scripts" / "download_ds000114.py"
1922
_DATASET_DIR = _REPO_ROOT / "tests" / "data" / "ds000114"
2023
_DATASET_SENTINEL = (
2124
_DATASET_DIR / "sub-01" / "ses-test" / "anat" / "sub-01_ses-test_T1w.nii.gz"
2225
)
2326

27+
_SUB = "01"
28+
29+
30+
def _rbc_exe() -> str:
31+
exe = shutil.which("rbc")
32+
assert exe is not None, "rbc CLI not found on PATH"
33+
return exe
34+
35+
36+
def _run_rbc(
37+
args: Sequence[str], *, timeout: int = 7200
38+
) -> subprocess.CompletedProcess[str]:
39+
"""Invoke ``rbc`` with ``-vv`` so container output reaches the subprocess.
40+
41+
Without ``-vv`` the styx runner logger stays at WARNING and
42+
container-side failure output (e.g. lta_convert exiting non-zero with
43+
its own error text) never makes it into the captured stderr, which
44+
leaves the test with only a bare ``returncode=1`` to diagnose from.
45+
"""
46+
result = subprocess.run( # noqa: S603
47+
[_rbc_exe(), *args, "-vv"],
48+
capture_output=True,
49+
text=True,
50+
timeout=timeout,
51+
)
52+
assert result.returncode == 0, (
53+
f"rbc {args[0]} exited with code {result.returncode}\n"
54+
f"--- stdout ---\n{result.stdout[-4000:]}\n"
55+
f"--- stderr ---\n{result.stderr[-4000:]}"
56+
)
57+
return result
58+
2459

2560
@pytest.fixture(scope="session")
2661
def ds000114_dataset() -> Path:
@@ -56,16 +91,15 @@ def ds000114_dataset() -> Path:
5691

5792

5893
@pytest.fixture(scope="session")
59-
def runner_backend(request: pytest.FixtureRequest) -> str:
60-
"""Styx runner backend selected via ``--runner`` on the pytest CLI."""
94+
def _runner(request: pytest.FixtureRequest) -> str:
6195
return request.config.getoption("--runner")
6296

6397

6498
@pytest.fixture(scope="session")
6599
def ds000114_anat_derivatives(
66100
ds000114_dataset: Path,
67101
tmp_path_factory: pytest.TempPathFactory,
68-
runner_backend: str,
102+
_runner: str,
69103
) -> Path:
70104
"""Run ``rbc anatomical`` once against ds000114 sub-01 (both sessions).
71105
@@ -74,33 +108,44 @@ def ds000114_anat_derivatives(
74108
scope ensures we only pay the registration + brain extraction cost
75109
once across all longitudinal integration tests.
76110
"""
77-
rbc = shutil.which("rbc")
78-
if rbc is None:
79-
pytest.skip("rbc CLI not found on PATH")
80-
81111
out = tmp_path_factory.mktemp("ds000114_derivatives")
82-
result = subprocess.run( # noqa: S603
112+
_run_rbc(
83113
[
84-
rbc,
85114
"anatomical",
86115
str(ds000114_dataset),
87116
"-o",
88117
str(out),
89118
"--runner",
90-
runner_backend,
119+
_runner,
91120
"--participant-label",
92-
"01",
121+
_SUB,
93122
],
94-
capture_output=True,
95-
text=True,
96-
# Two-session anat (brain extraction + registration, twice) under
97-
# xdist contention on the self-hosted runner can push past an hour;
98-
# match the 7200s budget used by tests/integration/test_all.py.
99-
timeout=7200,
100-
)
101-
assert result.returncode == 0, (
102-
f"rbc anatomical exited with code {result.returncode}\n"
103-
f"--- stdout ---\n{result.stdout[-2000:]}\n"
104-
f"--- stderr ---\n{result.stderr[-2000:]}"
105123
)
106124
return out
125+
126+
127+
@pytest.fixture(scope="session")
128+
def longitudinal_template_output(
129+
ds000114_anat_derivatives: Path,
130+
_runner: str,
131+
) -> Path:
132+
"""Run ``rbc longitudinal template`` once and return the derivatives dir.
133+
134+
Writes the ``ses-longitudinal`` tree alongside the per-session
135+
cross-sectional anat outputs, the same layout downstream longitudinal
136+
stages will consume.
137+
"""
138+
_run_rbc(
139+
[
140+
"longitudinal",
141+
"template",
142+
str(ds000114_anat_derivatives),
143+
"-o",
144+
str(ds000114_anat_derivatives),
145+
"--runner",
146+
_runner,
147+
"--participant-label",
148+
_SUB,
149+
],
150+
)
151+
return ds000114_anat_derivatives

tests/integration/longitudinal/test_template.py

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
"""Integration test for ``rbc longitudinal template``.
22
33
Deferred from Stage 2 of the longitudinal refactor (tracker #301,
4-
Stage 2 landed in PR #306). Depends on the ds000114 multi-session test
5-
fixture; cross-sectional anatomical derivatives are produced by the
6-
session-scoped ``ds000114_anat_derivatives`` fixture so the template
7-
stage has ``desc-brain`` T1w volumes to consume.
4+
Stage 2 landed in PR #306). Depends on the ds000114 multi-session
5+
fixture and the cross-sectional anatomical pre-run that produces the
6+
``desc-brain`` T1w derivatives the template stage consumes.
87
"""
98

109
from __future__ import annotations
1110

12-
import shutil
13-
import subprocess
1411
from typing import TYPE_CHECKING
1512

1613
import pytest
@@ -21,37 +18,10 @@
2118

2219
@pytest.mark.slow
2320
def test_rbc_longitudinal_template_builds_bids_tree(
24-
ds000114_anat_derivatives: Path,
25-
runner_backend: str,
21+
longitudinal_template_output: Path,
2622
) -> None:
27-
"""Run ``rbc longitudinal template`` and verify the BIDS output tree."""
28-
rbc = shutil.which("rbc")
29-
assert rbc is not None, "rbc CLI not found on PATH"
30-
31-
result = subprocess.run( # noqa: S603
32-
[
33-
rbc,
34-
"longitudinal",
35-
"template",
36-
str(ds000114_anat_derivatives),
37-
"-o",
38-
str(ds000114_anat_derivatives),
39-
"--runner",
40-
runner_backend,
41-
"--participant-label",
42-
"01",
43-
],
44-
capture_output=True,
45-
text=True,
46-
timeout=7200,
47-
)
48-
assert result.returncode == 0, (
49-
f"rbc longitudinal template exited with code {result.returncode}\n"
50-
f"--- stdout ---\n{result.stdout[-2000:]}\n"
51-
f"--- stderr ---\n{result.stderr[-2000:]}"
52-
)
53-
54-
ses_long = ds000114_anat_derivatives / "sub-01" / "ses-longitudinal" / "anat"
23+
"""The ``ses-longitudinal`` BIDS tree contains the expected files."""
24+
ses_long = longitudinal_template_output / "sub-01" / "ses-longitudinal" / "anat"
5525
expected = [
5626
"sub-01_ses-longitudinal_T1w.nii.gz",
5727
"sub-01_ses-longitudinal_from-test_to-longitudinal_mode-image_xfm.txt",
@@ -60,8 +30,8 @@ def test_rbc_longitudinal_template_builds_bids_tree(
6030
missing = [name for name in expected if not (ses_long / name).is_file()]
6131
if missing:
6232
tree = sorted(
63-
str(p.relative_to(ds000114_anat_derivatives))
64-
for p in ds000114_anat_derivatives.rglob("*")
33+
str(p.relative_to(longitudinal_template_output))
34+
for p in longitudinal_template_output.rglob("*")
6535
if p.is_file()
6636
)
6737
pytest.fail(

0 commit comments

Comments
 (0)