-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
238 lines (215 loc) · 11.1 KB
/
Copy pathtesting.yml
File metadata and controls
238 lines (215 loc) · 11.1 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
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Testing
on:
push:
branches:
- master
- development
pull_request:
branches:
- master
- development
# Cancel any in-progress or queued Testing run on the same source
# branch when a new push arrives. Collapses duplicate push + PR runs
# from the same commit into a single active run, so rapid iteration
# does not stack up stale jobs.
#
# The group key uses github.head_ref (populated on PRs with the
# source branch) falling back to github.ref_name (populated on pushes
# with the branch short name), so a push to development and a PR
# with development as the source end up in the same group. The
# pull_request.head.repo.full_name fallback namespaces the group by
# fork identity, so two different forks with the same branch name
# do not collide.
#
# testing-cron.yml intentionally does not use concurrency because
# each scheduled cron trigger is an independent run.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }}
cancel-in-progress: true
jobs:
build:
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
# macOS is pinned to a single Python version because Apple Silicon
# runners were intermittently not finishing the non-torch test suite
# within the job timeout on 3.10-3.13 even though 3.9 passes
# reliably. Ubuntu and Windows cover the full Python matrix, so the
# single macOS cell is a smoke-test canary for Apple Silicon rather
# than full per-version coverage.
# TODO: re-enable 3.10-3.13 on macOS once the Apple Silicon slowness
# is understood (profile via pytest --durations=20 first).
include:
- os: macos-latest
python-version: "3.9"
steps:
- uses: actions/checkout@v4
- name: Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install libomp for macOS
if: startsWith(matrix.os, 'macos')
run: brew install libomp
- name: Install dependencies (non-macOS, full deps including torch)
if: ${{ !startsWith(matrix.os, 'macos') }}
run: |
python -m pip install --upgrade pip
pip install -r docs/requirements.txt
pip install pytest
pip install coverage
pip install coveralls
# macOS CI deliberately skips torch and torch_geometric because of
# the upstream PyTorch NNPACK slowdown on Apple Silicon
# (https://github.com/pytorch/pytorch/issues/107534). Conv-heavy
# torch tests run ~36x slower on Apple Silicon and exhaust any
# reasonable job timeout. Torch-dependent tests are auto-skipped
# by pyod/test/conftest.py when torch is absent.
# TODO: remove this macOS-specific step and the conftest shim once
# upstream PyTorch fixes NNPACK on ARM and a fixed wheel is released.
- name: Install dependencies (macOS, skip torch and torch_geometric)
if: startsWith(matrix.os, 'macos')
run: |
python -m pip install --upgrade pip
grep -vE '^(torch|torch_geometric)$' docs/requirements.txt > /tmp/mac-reqs.txt
pip install -r /tmp/mac-reqs.txt
pip install pytest
pip install coverage
pip install coveralls
- name: Test with pytest
run: |
coverage run --source=pyod -m pytest
- name: Coverage report
shell: bash
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_SERVICE_NAME: github-actions
run: |
if ! coveralls --service=github; then
echo "Coveralls upload failed; retrying once in 15s for best effort."
sleep 15
coveralls --service=github || true
fi
packaging-smoke-test:
name: packaging smoke test (sdist + wheel from a clean build)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install build tooling
run: python -m pip install --upgrade pip build twine
- name: Build sdist and wheel
run: python -m build
- name: Inspect sdist contents
run: |
set -euo pipefail
sdist=$(ls dist/*.tar.gz | head -1)
echo "Inspecting $sdist"
tar tzf "$sdist" > /tmp/sdist-listing.txt
knowledge_count=$(grep -c 'pyod/utils/knowledge/.*\.json' /tmp/sdist-listing.txt || true)
analysis_count=$(grep -c 'pyod/utils/model_analysis_jsons/.*\.json' /tmp/sdist-listing.txt || true)
echo "knowledge JSONs in sdist: $knowledge_count"
echo "model_analysis JSONs in sdist: $analysis_count"
test "$knowledge_count" -ge 4 || { echo "ERROR: expected >=4 knowledge JSONs in sdist"; exit 1; }
test "$analysis_count" -ge 10 || { echo "ERROR: expected >=10 model_analysis JSONs in sdist"; exit 1; }
- name: Inspect wheel contents
run: |
set -euo pipefail
wheel=$(ls dist/*.whl | head -1)
echo "Inspecting $wheel"
python -m zipfile -l "$wheel" > /tmp/wheel-listing.txt
knowledge_count=$(grep -c 'pyod/utils/knowledge/.*\.json' /tmp/wheel-listing.txt || true)
analysis_count=$(grep -c 'pyod/utils/model_analysis_jsons/.*\.json' /tmp/wheel-listing.txt || true)
skill_count=$(grep -c 'pyod/skills/od_expert/SKILL\.md' /tmp/wheel-listing.txt || true)
test_count=$(grep -c 'pyod/test/' /tmp/wheel-listing.txt || true)
docs_count=$(grep -cE '^docs/' /tmp/wheel-listing.txt || true)
echo "knowledge JSONs in wheel: $knowledge_count"
echo "model_analysis JSONs in wheel: $analysis_count"
echo "od_expert SKILL.md in wheel: $skill_count"
echo "test files in wheel: $test_count"
echo "docs files in wheel: $docs_count"
test "$knowledge_count" -ge 4 || { echo "ERROR: expected >=4 knowledge JSONs in wheel"; exit 1; }
test "$analysis_count" -ge 10 || { echo "ERROR: expected >=10 model_analysis JSONs in wheel"; exit 1; }
test "$skill_count" -eq 1 || { echo "ERROR: expected exactly 1 od_expert SKILL.md in wheel"; exit 1; }
test "$test_count" -eq 0 || { echo "ERROR: wheel contains $test_count pyod/test files; should be 0"; exit 1; }
test "$docs_count" -eq 0 || { echo "ERROR: wheel contains $docs_count docs/ files; should be 0"; exit 1; }
- name: Validate metadata with twine
run: twine check dist/*
- name: Fresh-venv install from sdist and import smoke test
run: |
set -euo pipefail
python -m venv /tmp/pyod-smoke
/tmp/pyod-smoke/bin/python -m pip install --upgrade pip
/tmp/pyod-smoke/bin/python -m pip install dist/*.tar.gz
/tmp/pyod-smoke/bin/python - <<'PY'
import pyod
from pyod.models.iforest import IForest
from pyod.utils.ad_engine import ADEngine
print("pyod version:", pyod.__version__)
assert pyod.__version__.startswith("3."), f"unexpected version {pyod.__version__}"
engine = ADEngine()
detectors = engine.list_detectors()
assert len(detectors) > 0, "ADEngine knowledge base loaded 0 detectors"
print("ADEngine detectors loaded:", len(detectors))
import numpy as np
rng = np.random.default_rng(42)
X = rng.standard_normal((50, 5))
clf = IForest(contamination=0.1, random_state=42)
clf.fit(X)
print("IForest fit OK; decision_scores_ shape:", clf.decision_scores_.shape)
PY
- name: Verify unified pyod CLI end-to-end
run: |
set -euo pipefail
/tmp/pyod-smoke/bin/pyod --help
/tmp/pyod-smoke/bin/pyod info
/tmp/pyod-smoke/bin/pyod install skill --help
/tmp/pyod-smoke/bin/pyod install skill --list
rm -rf /tmp/pyod-cli-skill-test
/tmp/pyod-smoke/bin/pyod install skill --target /tmp/pyod-cli-skill-test
test -f /tmp/pyod-cli-skill-test/od-expert/SKILL.md \
|| { echo "ERROR: pyod install skill did not create SKILL.md"; exit 1; }
# v3.2.0: verify references/ subdir was copied alongside SKILL.md
test -d /tmp/pyod-cli-skill-test/od-expert/references \
|| { echo "ERROR: references/ subdir not installed"; exit 1; }
test -f /tmp/pyod-cli-skill-test/od-expert/references/workflow.md \
|| { echo "ERROR: references/workflow.md not installed"; exit 1; }
test -f /tmp/pyod-cli-skill-test/od-expert/references/pitfalls.md \
|| { echo "ERROR: references/pitfalls.md not installed"; exit 1; }
test -f /tmp/pyod-cli-skill-test/od-expert/references/tabular.md \
|| { echo "ERROR: references/tabular.md not installed"; exit 1; }
test -f /tmp/pyod-cli-skill-test/od-expert/references/time_series.md \
|| { echo "ERROR: references/time_series.md not installed"; exit 1; }
test -f /tmp/pyod-cli-skill-test/od-expert/references/graph.md \
|| { echo "ERROR: references/graph.md not installed"; exit 1; }
test -f /tmp/pyod-cli-skill-test/od-expert/references/text_image.md \
|| { echo "ERROR: references/text_image.md not installed"; exit 1; }
# v3.2.0: __pycache__ and __init__.py must NOT be installed
test ! -e /tmp/pyod-cli-skill-test/od-expert/__pycache__ \
|| { echo "ERROR: __pycache__ leaked into install"; exit 1; }
test ! -e /tmp/pyod-cli-skill-test/od-expert/__init__.py \
|| { echo "ERROR: __init__.py leaked into install"; exit 1; }
# Regression guard: `import pyod.mcp_server` in a smoke venv
# that deliberately does NOT have pyod[mcp] must not sys.exit.
/tmp/pyod-smoke/bin/python -c "import importlib.util; assert importlib.util.find_spec('mcp') is None, 'smoke venv unexpectedly has mcp'"
/tmp/pyod-smoke/bin/python -c "import pyod.mcp_server"
# Canonical-name assertion: underscore input → hyphen output
rm -rf /tmp/pyod-cli-underscore-test
/tmp/pyod-smoke/bin/pyod install skill --skill od_expert \
--target /tmp/pyod-cli-underscore-test \
| tee /tmp/pyod-cli-underscore.log
grep -q "Installed od-expert skill" /tmp/pyod-cli-underscore.log \
|| { echo "ERROR: canonical name not used in output"; exit 1; }
test -f /tmp/pyod-cli-underscore-test/od-expert/SKILL.md
echo "unified pyod CLI works end-to-end"