Skip to content

Commit 8ab6d15

Browse files
authored
Fix tests, update workflows, support Python 3.12 tests (#237)
* update action versions in pypi workflow * simplify and speed-up push_pr / test workflow * add Python 3.12 to test matrix * fix syntax in sigpy tests * extend seq.plot() tests and run them for all test sequences * add coverage report
1 parent 4a25069 commit 8ab6d15

File tree

6 files changed

+140
-90
lines changed

6 files changed

+140
-90
lines changed

.github/workflows/publish-to-pypi.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,26 @@ jobs:
88
runs-on: ubuntu-latest
99

1010
steps:
11-
- uses: actions/checkout@v4
11+
- name: Checkout Repository
12+
uses: actions/checkout@v4
13+
1214
- name: Set up Python
13-
uses: actions/setup-python@v4
15+
uses: actions/setup-python@v5
1416
with:
1517
python-version: "3.x"
18+
1619
- name: Install pypa/build
1720
run: >-
1821
python3 -m
1922
pip install
2023
build
2124
--user
25+
2226
- name: Build a binary wheel and a source tarball
2327
run: python3 -m build
28+
2429
- name: Store the distribution packages
25-
uses: actions/upload-artifact@v3
30+
uses: actions/upload-artifact@v4
2631
with:
2732
name: python-package-distributions
2833
path: dist/
@@ -41,10 +46,11 @@ jobs:
4146

4247
steps:
4348
- name: Download all the dists
44-
uses: actions/download-artifact@v4.1.7
49+
uses: actions/download-artifact@v4
4550
with:
4651
name: python-package-distributions
4752
path: dist/
53+
4854
- name: Publish distribution to PyPI
4955
uses: pypa/gh-action-pypi-publish@release/v1
5056

@@ -62,12 +68,13 @@ jobs:
6268

6369
steps:
6470
- name: Download all the dists
65-
uses: actions/download-artifact@v4.1.7
71+
uses: actions/download-artifact@v4
6672
with:
6773
name: python-package-distributions
6874
path: dist/
75+
6976
- name: Sign the dists with Sigstore
70-
uses: sigstore/gh-action-sigstore-python@v1.2.3
77+
uses: sigstore/gh-action-sigstore-python@v3
7178
with:
7279
inputs: >-
7380
./dist/*.tar.gz

.github/workflows/push_pr.yml

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,83 @@
11
name: Run code tests on push and pull requests
22
on:
33
push:
4+
branches:
5+
- master
46
pull_request:
57
# Allows you to run this workflow manually from the Actions tab
68
workflow_dispatch:
79

10+
permissions:
11+
contents: read
12+
pull-requests: write
13+
814
jobs:
915
tests:
10-
name: Code tests (Python ${{ matrix.python-version }}, ${{ matrix.os }})
11-
runs-on: ${{ matrix.os }}
16+
name: Code tests
17+
runs-on: ubuntu-latest
18+
1219
strategy:
20+
fail-fast: false
1321
matrix:
14-
os: ["ubuntu-latest"]
15-
python-version: ["3.8", "3.9", "3.10", "3.11"]
22+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
23+
include-sigpy: [true, false]
1624

1725
steps:
18-
- uses: actions/checkout@v4
19-
with:
20-
submodules: true
21-
lfs: true
22-
- uses: conda-incubator/setup-miniconda@v2
26+
- name: Checkout Repository
27+
uses: actions/checkout@v4
28+
29+
- name: Cache pip dependencies
30+
uses: actions/cache@v3
2331
with:
24-
auto-update-conda: true
25-
python-version: ${{ matrix.python-version }}
26-
activate-environment: test
27-
environment-file: .github/requirements.yml
28-
auto-activate-base: false
29-
channels: conda-forge
30-
- shell: bash -l {0}
32+
path: ~/.cache/pip
33+
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
34+
restore-keys: |
35+
${{ runner.os }}-pip-
36+
37+
- name: Install PyPulseq and dependencies
3138
run: |
32-
conda info
33-
conda list
34-
# - name: Lint
35-
# shell: bash -l {0}
36-
# run: |
37-
# python -m flake8 pypulseq
38-
- name: Run pytest
39-
shell: bash -l {0}
39+
pip install --upgrade --upgrade-strategy eager .[test]
40+
if [ ${{ matrix.include-sigpy }} == "true" ]; then
41+
pip install sigpy
42+
fi
43+
44+
- name: Install PyTest GitHub Annotation Plugin
45+
run: pip install pytest-github-actions-annotate-failures
46+
47+
- name: Run PyTest and Generate Coverage Report
4048
run: |
41-
pip install .
42-
pytest -m "not slow and not sigpy"
43-
- name: Run pytest[sigpy]
44-
shell: bash -l {0}
49+
if [ ${{ matrix.include-sigpy }} == "true" ]; then
50+
pytest -m "not slow" --junitxml=pytest.xml \
51+
--cov-report=term-missing:skip-covered --cov=sequences | tee pytest-coverage.txt
52+
else
53+
pytest -m "not slow and not sigpy" --junitxml=pytest.xml \
54+
--cov-report=term-missing:skip-covered --cov=sequences | tee pytest-coverage.txt
55+
fi
56+
continue-on-error: ${{ matrix.include-sigpy }}
57+
58+
- name: Verify PyTest XML Output
4559
run: |
46-
pip install .[sigpy]
47-
pytest -m "not slow"
48-
continue-on-error: true
60+
if [ ! -f pytest.xml ]; then
61+
echo "PyTest XML report not found. Please check the previous 'Run PyTest' step for errors."
62+
exit 1
63+
fi
64+
65+
- name: Post PyTest Coverage Comment
66+
id: coverageComment
67+
uses: MishaKav/[email protected]
68+
with:
69+
pytest-coverage-path: ./pytest-coverage.txt
70+
junitxml-path: ./pytest.xml
71+
72+
- name: Set Pipeline Status Based on Test Results
73+
if: steps.coverageComment.outputs.errors != 0 || steps.coverageComment.outputs.failures != 0
74+
uses: actions/github-script@v7
75+
with:
76+
script: |
77+
core.setFailed("PyTest workflow failed with ${{ steps.coverageComment.outputs.errors }} errors and ${{ steps.coverageComment.outputs.failures }} failures.")
78+
79+
concurrency:
80+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
81+
82+
# Cancel in-progress runs when a new workflow with the same group name is triggered
83+
cancel-in-progress: true

examples/scripts/write_epi_se.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def main(plot: bool = False, write_seq: bool = False, seq_filename: str = "epi_s
7171

7272
# Refocusing pulse with spoiling gradients
7373
rf180 = pp.make_block_pulse(
74-
flip_angle=np.pi, system=system, duration=500e-6, use="refocusing"
74+
flip_angle=np.pi, delay=system.rf_dead_time,system=system, duration=500e-6, use="refocusing",
7575
)
7676
gz_spoil = pp.make_trapezoid(
7777
channel="z", system=system, area=gz.area * 2, duration=3 * pre_time

pyproject.toml

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ dependencies = [
2828

2929
[project.optional-dependencies]
3030
sigpy = ["sigpy>=0.1.26"]
31-
test = ["pytest", "pre-commit"]
31+
test = [
32+
"coverage",
33+
"codecov",
34+
"pre-commit",
35+
"pytest",
36+
"pytest-cov",
37+
"pytest-xdist",
38+
]
3239

3340
[project.urls]
3441
Homepage = "https://github.com/imr-framework/pypulseq"
@@ -120,11 +127,9 @@ exclude = ["examples/**"]
120127
# PyTest section
121128
[tool.pytest.ini_options]
122129
testpaths = ["tests"]
123-
filterwarnings = ["error",
124-
# Suppress error in debugpy due to mpl deprecation to debug tests.
125-
"ignore::matplotlib._api.deprecation.MatplotlibDeprecationWarning:pydev",
126-
]
127-
markers = [
128-
"slow: mark test as slow",
129-
"sigpy: tests that require sigpy",
130+
filterwarnings = [
131+
"error",
132+
# Suppress error in debugpy due to mpl deprecation to debug tests.
133+
"ignore::matplotlib._api.deprecation.MatplotlibDeprecationWarning:pydev",
130134
]
135+
markers = ["sigpy: tests that require sigpy"]

tests/test_sequence.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,16 @@ def test_save_expected(self, seq_func):
272272
seq_name = str(seq_func.__name__)
273273
TestSequence.seq.write(expected_output_path / (seq_name + '.seq'))
274274

275-
# Test whether a sequence can be plotted.
276-
@pytest.mark.slow
277-
def test_plot(self, seq_func): # noqa: ARG002
278-
with patch('matplotlib.pyplot.show'):
279-
TestSequence.seq.plot()
280-
TestSequence.seq.plot(show_blocks=True)
281-
plt.close('all')
275+
# Test sequence.plot() method
276+
def test_plot(self, seq_func):
277+
if seq_func.__name__ in ['seq1', 'seq2', 'seq3', 'seq4']:
278+
with patch('matplotlib.pyplot.show'):
279+
TestSequence.seq.plot()
280+
TestSequence.seq.plot(show_blocks=True)
281+
TestSequence.seq.plot(time_range=(0, 1e-3))
282+
TestSequence.seq.plot(time_disp='ms')
283+
TestSequence.seq.plot(grad_disp='mT/m')
284+
plt.close('all')
282285

283286
# Test whether the sequence is the approximately the same after writing a .seq
284287
# file and reading it back in.

tests/test_sigpy.py

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,12 @@ def test_sigpy_import():
2121

2222
@pytest.mark.sigpy
2323
def test_slr():
24-
import sigpy.mri.rf as rf
2524
from pypulseq.make_sigpy_pulse import sigpy_n_seq
2625

27-
print('Testing SLR design')
28-
29-
time_bw_product = 4
30-
slice_thickness = 3e-3 # Slice thickness
26+
slice_thickness = 3e-3
3127
flip_angle = np.pi / 2
32-
# Set system limits
28+
duration = 3e-3
29+
3330
system = Opts(
3431
max_grad=32,
3532
grad_unit='mT/m',
@@ -49,10 +46,10 @@ def test_slr():
4946
band_sep=20,
5047
phs_0_pt='None',
5148
)
52-
rfp, gz, _, pulse = sigpy_n_seq(
49+
rfp, _, _ = sigpy_n_seq( # type: ignore
5350
flip_angle=flip_angle,
5451
system=system,
55-
duration=3e-3,
52+
duration=duration,
5653
slice_thickness=slice_thickness,
5754
time_bw_product=4,
5855
return_gz=True,
@@ -61,32 +58,33 @@ def test_slr():
6158
delay=system.rf_dead_time,
6259
)
6360

64-
seq = pp.Sequence()
61+
seq = pp.Sequence(system=system)
6562
seq.add_block(rfp)
6663

67-
[a, b] = rf.sim.abrm(
68-
pulse,
69-
np.arange(-20 * time_bw_product, 20 * time_bw_product, 40 * time_bw_product / 2000),
70-
True,
71-
)
72-
mag_xy = 2 * np.multiply(np.conj(a), b)
73-
# pl.LinePlot(Mxy)
74-
# print(np.sum(np.abs(Mxy)))
75-
# peaks, dict = sis.find_peaks(np.abs(Mxy),threshold=0.5, plateau_size=40)
76-
plateau_widths = np.sum(np.abs(mag_xy) > 0.8)
77-
assert plateau_widths == 29
64+
assert rfp.signal.shape[0] == pytest.approx((duration + system.rf_ringdown_time) / system.rf_raster_time)
65+
66+
# [a, b] = rf.sim.abrm(
67+
# rfp.signal,
68+
# np.arange(-20 * time_bw_product, 20 * time_bw_product, 40 * time_bw_product / 2000),
69+
# True,
70+
# )
71+
# mag_xy = 2 * np.multiply(np.conj(a), b)
72+
# # pl.LinePlot(Mxy)
73+
# # print(np.sum(np.abs(Mxy)))
74+
# # peaks, dict = sis.find_peaks(np.abs(Mxy),threshold=0.5, plateau_size=40)
75+
# plateau_widths = np.sum(np.abs(mag_xy) > 0.8)
76+
# assert plateau_widths == 29
7877

7978

8079
@pytest.mark.sigpy
8180
def test_sms():
82-
import sigpy.mri.rf as rf
8381
from pypulseq.make_sigpy_pulse import sigpy_n_seq
8482

8583
print('Testing SMS design')
8684

87-
time_bw_product = 4
8885
slice_thickness = 3e-3 # Slice thickness
8986
flip_angle = np.pi / 2
87+
duration = 3e-3
9088
n_bands = 3
9189
# Set system limits
9290
system = Opts(
@@ -108,10 +106,10 @@ def test_sms():
108106
band_sep=20,
109107
phs_0_pt='None',
110108
)
111-
rfp, gz, _, pulse = sigpy_n_seq(
109+
rfp, _, _ = sigpy_n_seq( # type: ignore
112110
flip_angle=flip_angle,
113111
system=system,
114-
duration=3e-3,
112+
duration=duration,
115113
slice_thickness=slice_thickness,
116114
time_bw_product=4,
117115
return_gz=True,
@@ -120,18 +118,20 @@ def test_sms():
120118
delay=system.rf_dead_time,
121119
)
122120

123-
seq = pp.Sequence()
121+
seq = pp.Sequence(system=system)
124122
seq.add_block(rfp)
125123

126-
[a, b] = rf.sim.abrm(
127-
pulse,
128-
np.arange(-20 * time_bw_product, 20 * time_bw_product, 40 * time_bw_product / 2000),
129-
True,
130-
)
131-
mag_xy = 2 * np.multiply(np.conj(a), b)
132-
# pl.LinePlot(Mxy)
133-
# print(np.sum(np.abs(Mxy)))
134-
# peaks, dict = sis.find_peaks(np.abs(Mxy),threshold=0.5, plateau_size=40)
135-
plateau_widths = np.sum(np.abs(mag_xy) > 0.8)
136-
# if slr has 29 > 0.8, then sms with MB = n_bands
137-
assert (29 * n_bands) == plateau_widths
124+
assert rfp.signal.shape[0] == pytest.approx((duration + system.rf_ringdown_time) / system.rf_raster_time)
125+
126+
# [a, b] = rf.sim.abrm(
127+
# rfp.signal,
128+
# np.arange(-20 * time_bw_product, 20 * time_bw_product, 40 * time_bw_product / 2000),
129+
# True,
130+
# )
131+
# mag_xy = 2 * np.multiply(np.conj(a), b)
132+
# # pl.LinePlot(Mxy)
133+
# # print(np.sum(np.abs(Mxy)))
134+
# # peaks, dict = sis.find_peaks(np.abs(Mxy),threshold=0.5, plateau_size=40)
135+
# plateau_widths = np.sum(np.abs(mag_xy) > 0.8)
136+
# # if slr has 29 > 0.8, then sms with MB = n_bands
137+
# assert (29 * n_bands) == plateau_widths

0 commit comments

Comments
 (0)