Repository navigation
386 lines (346 loc) · 14.2 KB
/
Copy pathtest.yml
File metadata and controls
386 lines (346 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
name: Tests
on:
push:
branches: [ master, develop ]
pull_request:
branches: [ master, develop ]
env:
# MATLAB batch licensing token (MathWorks pilot, license 1144083, expires
# 2027-01-15). It licenses matlab-batch only, not the Engine API for Python,
# so the parity tests reach MATLAB through tests/matlab/eegprep_batch_engine_serve.m
# running inside matlab-batch (client: tests/matlab_batch_engine.py).
MLM_LICENSE_TOKEN: ${{ secrets.MATLAB_BATCH_TOKEN }}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
pre-commit:
name: Pre-commit checks
runs-on: ubuntu-22.04
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Fetch base branch
run: |
git fetch --no-tags origin "${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}"
- name: Run pre-commit checks
run: |
./pre-commit.py --changed-from "origin/${{ github.base_ref }}"
lint:
name: Ruff and ty
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
# Fetch the vendored EEGLAB reference itself, but not EEGLAB's nested
# plugin submodules; upstream EEGLAB can temporarily point at plugin
# commits that are unavailable for shallow GitHub Actions checkout.
submodules: true
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install Python 3.12
run: |
uv python install 3.12
- name: Install dependencies
run: |
uv sync --python 3.12 --all-extras --group dev
- name: Run Ruff lint
run: |
uv run --no-sync ruff check .
- name: Run Ruff format check
run: |
uv run --no-sync ruff format --check .
- name: Run ty type check
run: |
uv run --no-sync ty check
test:
name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: lint
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04]
python-version: ["3.12", "3.13"]
include:
# Test on macOS and Windows for Python 3.12 only
- os: macos-latest
python-version: "3.12"
- os: windows-latest
python-version: "3.12"
steps:
- uses: actions/checkout@v4
with:
# Fetch the vendored EEGLAB reference itself, but not EEGLAB's nested
# plugin submodules; upstream EEGLAB can temporarily point at plugin
# commits that are unavailable for shallow GitHub Actions checkout.
submodules: true
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install Python ${{ matrix.python-version }}
run: |
uv python install ${{ matrix.python-version }}
- name: Install Linux GUI runtime libraries
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libegl1 \
libgl1 \
libxkbcommon-x11-0 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-shape0 \
libxcb-xinerama0
echo "QT_QPA_PLATFORM=offscreen" >> "$GITHUB_ENV"
- name: Determine MATLAB availability
id: matlab-availability
shell: bash
run: |
# Skip MATLAB on macOS and Windows, only enable on Ubuntu if token available
if [[ "${{ matrix.os }}" == "ubuntu-22.04" ]] && [[ -n "$MLM_LICENSE_TOKEN" ]]; then
available=true
else
available=false
fi
echo "available=${available}" >> "$GITHUB_OUTPUT"
echo "EEGPREP_SKIP_MATLAB=1" >> "$GITHUB_ENV"
- name: Install dependencies
run: |
uv sync --python ${{ matrix.python-version }} --all-extras --group dev
- name: Set up MATLAB
if: steps.matlab-availability.outputs.available == 'true'
uses: matlab-actions/setup-matlab@v2
with:
release: R2024b
# EEGLAB's reference functions need resample/firls/pwelch and the
# statistics functions (tcdf, ttest, kmeans, ...).
products: >-
MATLAB
Signal_Processing_Toolbox
Statistics_and_Machine_Learning_Toolbox
- name: Locate MATLAB root
if: steps.matlab-availability.outputs.available == 'true'
shell: bash
run: |
MATLAB_ROOT=$(python - <<'PY'
import os
from pathlib import Path
tool_cache = Path(os.environ.get("RUNNER_TOOL_CACHE", "")) / "MATLAB"
candidates = sorted(tool_cache.glob("*/x64"))
if not candidates:
raise SystemExit(f"No MATLAB installation found under {tool_cache}")
print(candidates[-1])
PY
)
echo "MATLAB_ROOT=$MATLAB_ROOT" >> $GITHUB_ENV
echo "$MATLAB_ROOT/bin" >> $GITHUB_PATH
- name: Fetch EEGLAB plugin submodules
if: steps.matlab-availability.outputs.available == 'true'
shell: bash
working-directory: src/eegprep/eeglab
run: |
# One plugin at a time: a missing upstream commit costs one plugin, not the lane.
for path in $(git config -f .gitmodules --get-regexp path | awk '{print $2}'); do
if ! git submodule update --init --recursive --depth 1 -- "$path"; then
echo "::warning title=EEGLAB plugin unavailable::Could not fetch $path; parity tests needing it will fail"
fi
done
- name: Start MATLAB through matlab-batch
id: matlab-batch
if: steps.matlab-availability.outputs.available == 'true'
shell: bash
run: |
BATCH_DIR="$RUNNER_TEMP/eegprep-matlab-batch"
mkdir -p "$BATCH_DIR"
nohup matlab-batch "addpath('$GITHUB_WORKSPACE/tests/matlab'); eegprep_batch_engine_serve('$BATCH_DIR')" \
> "$BATCH_DIR/server.log" 2>&1 &
echo $! > "$BATCH_DIR/server.pid"
for i in $(seq 1 180); do
[[ -f "$BATCH_DIR/ready" ]] && break
if ! kill -0 "$(cat "$BATCH_DIR/server.pid")" 2>/dev/null; then break; fi
sleep 2
done
if [[ ! -f "$BATCH_DIR/ready" ]]; then
echo "::error title=MATLAB did not start::see server.log below"
cat "$BATCH_DIR/server.log"
exit 1
fi
echo "EEGPREP_MATLAB_BATCH_DIR=$BATCH_DIR" >> "$GITHUB_ENV"
echo "EEGPREP_SKIP_MATLAB=0" >> "$GITHUB_ENV"
- name: Run Python tests
run: |
uv run --no-sync pytest tests
- name: Stop MATLAB
if: always() && steps.matlab-batch.outcome == 'success'
shell: bash
run: |
touch "$EEGPREP_MATLAB_BATCH_DIR/quit"
for i in $(seq 1 30); do
kill -0 "$(cat "$EEGPREP_MATLAB_BATCH_DIR/server.pid")" 2>/dev/null || break
sleep 1
done
echo "--- matlab-batch server log (tail)"
tail -n 60 "$EEGPREP_MATLAB_BATCH_DIR/server.log" | grep -v "$MLM_LICENSE_TOKEN" || true
# eegprep-lean is a second distribution in this repo (ADR 0069) and the root
# suite's `testpaths = ["tests"]` does not reach it, so without this step it ships
# with lint as its only gate. Run from its own directory so its pyproject supplies
# the `network` marker, with src on the path because the package is deliberately
# not installed into this environment: it must keep working with no dependencies.
# The network tier is excluded here and runs in lean-live.yml on a schedule, so a
# NEMAR outage cannot turn every unrelated pull request red.
- name: Run eegprep-lean tests
if: always()
working-directory: packages/eegprep-lean
env:
PYTHONPATH: src
shell: bash
run: |
uv run --no-sync --project ../.. pytest tests -m "not network"
# The store and window modules need the zarr extra, and skip without it, so the
# step above proves only that they skip. This installs it so they actually run.
# zarr is not added to the dev environment because the base install must keep
# working with nothing present, which the step above is what verifies.
- name: Run eegprep-lean tests with the zarr extra
if: always()
working-directory: packages/eegprep-lean
# bash on every OS, as the steps above do. The default shell on windows-latest
# is PowerShell, where a trailing backslash is not a line continuation but a
# command to run, and `Failed to spawn: \` is the only clue you get -- printed
# after a fully green test run.
shell: bash
run: |
uv run --isolated --with 'zarr==3.4.0' --with pytest \
python -m pytest tests -m "not network" -p no:cacheprovider
# And the plot extra on its own, not alongside zarr. window.py imports zarr inside
# read_window rather than at module scope precisely so this tier stands alone, and
# a stray module-scope import would otherwise only show up in a browser, where the
# zarr install is the awkward one.
- name: Run eegprep-lean tests with the plot extra
if: always()
working-directory: packages/eegprep-lean
shell: bash
run: |
uv run --isolated --with matplotlib --with pytest \
python -m pytest tests -m "not network" -p no:cacheprovider
# Both extras, which is the install a real session has. The three tiers above each
# prove a tier stands alone; this one is the only place the cross-extra paths run,
# such as reading a window and then plotting it.
- name: Run eegprep-lean tests with both extras
if: always()
working-directory: packages/eegprep-lean
shell: bash
run: |
uv run --isolated --with 'zarr==3.4.0' --with matplotlib --with pytest \
python -m pytest tests -m "not network" -p no:cacheprovider
- name: Display installed packages
if: always()
run: |
uv pip list
# No continue-on-error, so the job fails on any new failure or strict XPASS.
# Maintainers should mark it required in branch protection. Known Python gaps
# are individual strict xfails. GUI and slow contracts stay out because modal
# Qt dialogs hang under offscreen Qt and the slow set exceeds this lane.
# This lane deselects MATLAB-oracle unittest classes; they are not Python gaps.
eeglab-python:
name: EEGLAB Python backend
runs-on: ubuntu-22.04
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install Python 3.12
run: |
uv python install 3.12
- name: Install Linux GUI runtime libraries
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
git-lfs \
libegl1 \
libgl1 \
libxkbcommon-x11-0 \
libxcb-cursor0 \
libxcb-icccm4 \
libxcb-image0 \
libxcb-keysyms1 \
libxcb-randr0 \
libxcb-render-util0 \
libxcb-shape0 \
libxcb-xinerama0
echo "QT_QPA_PLATFORM=offscreen" >> "$GITHUB_ENV"
- name: Install dependencies
run: |
uv sync --python 3.12 --all-extras --group dev
- name: Read pinned EEGLAB test commits
id: pins
run: |
uv run --no-sync python - <<'PY'
import os
from tests.eeglab_tests import (
EEGLAB_TESTS_COMMIT,
EEGLAB_TESTS_EEGLAB_COMMIT,
EEGLAB_TESTS_REPOSITORY,
)
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as handle:
handle.write(f"suite-commit={EEGLAB_TESTS_COMMIT}\n")
handle.write(f"eeglab-commit={EEGLAB_TESTS_EEGLAB_COMMIT}\n")
handle.write(f"suite-repository={EEGLAB_TESTS_REPOSITORY}\n")
PY
- name: Checkout eeglab_tests
env:
SUITE_COMMIT: ${{ steps.pins.outputs.suite-commit }}
EEGLAB_COMMIT: ${{ steps.pins.outputs.eeglab-commit }}
SUITE_REPOSITORY: ${{ steps.pins.outputs.suite-repository }}
run: |
rm -rf eeglab_tests
git init eeglab_tests
git -C eeglab_tests remote add origin "$SUITE_REPOSITORY"
git -C eeglab_tests fetch --depth 1 origin "$SUITE_COMMIT"
git -C eeglab_tests checkout --detach FETCH_HEAD
if [[ "$(git -C eeglab_tests rev-parse HEAD)" != "$SUITE_COMMIT" ]]; then
echo "::error title=eeglab_tests pin mismatch::HEAD is not $SUITE_COMMIT"
exit 1
fi
git -C eeglab_tests submodule set-url eeglab https://github.com/sccn/eeglab.git
git -C eeglab_tests submodule sync -- eeglab
git -C eeglab_tests submodule update --init --depth 1 -- eeglab
if [[ "$(git -C eeglab_tests/eeglab rev-parse HEAD)" != "$EEGLAB_COMMIT" ]]; then
echo "::error title=EEGLAB pin mismatch::suite submodule is not $EEGLAB_COMMIT"
exit 1
fi
git -C eeglab_tests lfs install --local
git -C eeglab_tests lfs pull
# The working tree already holds the smudged files. The LFS cache copies them again.
rm -rf eeglab_tests/.git/lfs
- name: Run EEGLAB Python backend tests
run: |
uv run --no-sync pytest tests \
--eeglab-backend python \
--eeglab-suite-root "$GITHUB_WORKSPACE/eeglab_tests" \
-m "parity and not slow and not gui" \
--timeout=600 \
--timeout-method=signal