Skip to content

Commit 526b05e

Browse files
ci: temporary campaign test matrix
1 parent 3b4bde4 commit 526b05e

1 file changed

Lines changed: 333 additions & 0 deletions

File tree

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
name: Campaign matrix
2+
3+
# Temporary test-only workflow: runs the fast test suite across operating
4+
# systems and Python versions, builds the FreshCore extension on every OS, and
5+
# checks each optional extra on Linux. It publishes nothing and writes nothing.
6+
7+
on:
8+
push:
9+
branches: [campaign/matrix]
10+
workflow_dispatch:
11+
12+
concurrency:
13+
group: campaign-matrix-${{ github.sha }}
14+
15+
permissions:
16+
contents: read
17+
18+
defaults:
19+
run:
20+
shell: bash
21+
22+
jobs:
23+
suite:
24+
name: suite (${{ matrix.os }}, py${{ matrix.python }})
25+
runs-on: ${{ matrix.os }}
26+
timeout-minutes: 90
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
os: [ubuntu-latest, macos-latest, windows-latest]
31+
python: ["3.9", "3.12", "3.13", "3.14"]
32+
include:
33+
- python: "3.9"
34+
pandas: "pandas>=1.5,<2"
35+
numpy: "numpy<2"
36+
env:
37+
PYTHONUTF8: "1"
38+
steps:
39+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
41+
with:
42+
python-version: ${{ matrix.python }}
43+
allow-prereleases: true
44+
cache: pip
45+
- name: Install
46+
run: |
47+
python -m pip install --upgrade pip
48+
if pip install -e ".[dev,ml]" pytest-timeout; then
49+
echo "EXTRA_INSTALLED=dev,ml"
50+
else
51+
echo "EXTRA_FALLBACK=dev"
52+
echo "::warning::.[dev,ml] failed to install; falling back to .[dev]"
53+
pip install -e ".[dev]" pytest-timeout
54+
fi
55+
if [ -n "${{ matrix.pandas }}" ]; then pip install "${{ matrix.pandas }}"; fi
56+
if [ -n "${{ matrix.numpy }}" ]; then pip install "${{ matrix.numpy }}"; fi
57+
python -c "import sys, platform, pandas, numpy; print('ENV', sys.version, platform.platform(), 'pandas', pandas.__version__, 'numpy', numpy.__version__)"
58+
pip freeze > pip-freeze.txt
59+
- name: Test (fast marker set)
60+
run: |
61+
python -m pytest -m "not online and not large" \
62+
-o addopts="--strict-markers -q -rfE" -p no:cacheprovider \
63+
--timeout 600 --junitxml=junit.xml
64+
- name: Upload results
65+
if: always()
66+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
67+
with:
68+
name: junit-suite-${{ matrix.os }}-py${{ matrix.python }}
69+
path: |
70+
junit.xml
71+
pip-freeze.txt
72+
if-no-files-found: warn
73+
74+
freshcore-native:
75+
name: freshcore-native (${{ matrix.os }})
76+
runs-on: ${{ matrix.os }}
77+
timeout-minutes: 60
78+
strategy:
79+
fail-fast: false
80+
matrix:
81+
os: [ubuntu-latest, macos-latest, windows-latest]
82+
env:
83+
RUSTUP_TOOLCHAIN: "1.98.1"
84+
PYTHONUTF8: "1"
85+
steps:
86+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
87+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
88+
with:
89+
python-version: "3.12"
90+
cache: pip
91+
- name: Install Rust toolchain
92+
run: |
93+
rustup toolchain install "$RUSTUP_TOOLCHAIN" --profile minimal
94+
rustc --version
95+
cargo --version
96+
- name: Install
97+
# maturin develop installs into a virtualenv, so create one in the
98+
# workspace and put it first on PATH for the remaining steps.
99+
run: |
100+
python -m venv .venv
101+
if [ "$RUNNER_OS" = "Windows" ]; then
102+
VENV_DIR="$(cygpath -w "$PWD/.venv")"
103+
VENV_BIN="$(cygpath -w "$PWD/.venv/Scripts")"
104+
VPY=".venv/Scripts/python.exe"
105+
else
106+
VENV_DIR="$PWD/.venv"
107+
VENV_BIN="$PWD/.venv/bin"
108+
VPY=".venv/bin/python"
109+
fi
110+
echo "VIRTUAL_ENV=$VENV_DIR" >> "$GITHUB_ENV"
111+
echo "$VENV_BIN" >> "$GITHUB_PATH"
112+
"$VPY" -m pip install --upgrade pip
113+
"$VPY" -m pip install -e ".[dev,freshcore]"
114+
- name: Show interpreter
115+
run: |
116+
which python
117+
python -c "import sys; print(sys.executable, sys.prefix)"
118+
- name: Rust unit tests
119+
run: cargo test --locked --manifest-path crates/freshcore/Cargo.toml
120+
- name: Build the extension
121+
run: maturin develop --manifest-path crates/freshcore/Cargo.toml --features extension-module
122+
- name: FreshCore tests
123+
run: |
124+
python -c "import freshdata_freshcore; print(freshdata_freshcore.__file__)"
125+
python -m pytest tests/test_execution -k freshcore -o addopts="--strict-markers" -q \
126+
-p no:cacheprovider --junitxml=junit.xml -rs
127+
- name: Upload results
128+
if: always()
129+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
130+
with:
131+
name: junit-freshcore-${{ matrix.os }}
132+
path: junit.xml
133+
if-no-files-found: warn
134+
135+
extras-linux:
136+
name: extras-linux (${{ matrix.extra }})
137+
runs-on: ubuntu-latest
138+
timeout-minutes: 90
139+
strategy:
140+
fail-fast: false
141+
matrix:
142+
extra: [spark, airflow, dagster, dbt, kafka, flight, semantic, privacy, cleanlab, viz, notebook, domains, excel, entity-resolution, outofcore, all]
143+
env:
144+
EXTRA: ${{ matrix.extra }}
145+
steps:
146+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
147+
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
148+
with:
149+
python-version: "3.12"
150+
cache: pip
151+
- name: Set up Java (spark)
152+
if: matrix.extra == 'spark' || matrix.extra == 'all'
153+
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
154+
with:
155+
distribution: temurin
156+
java-version: "17"
157+
- name: Resolve extra plan
158+
run: |
159+
case "$EXTRA" in
160+
spark)
161+
MODS="pyspark"
162+
FD="from freshdata.execution._lazy import has_pyspark, require_pyspark; assert has_pyspark(); require_pyspark(); import freshdata.execution.backends._spark"
163+
TESTS="tests/test_execution/test_spark_engine.py tests/test_execution/test_spark_engine_fixes.py" ;;
164+
airflow)
165+
MODS="airflow"
166+
FD="from freshdata.integrations.airflow import FreshDataCleanOperator; print(FreshDataCleanOperator.__mro__)"
167+
TESTS="tests/test_integrations/test_airflow.py tests/test_integrations/test_imports.py" ;;
168+
dagster)
169+
MODS="dagster"
170+
FD="from freshdata.integrations.dagster import FreshDataResource, freshdata_asset_check; print(FreshDataResource)"
171+
TESTS="tests/test_integrations/test_dagster.py tests/test_integrations/test_imports.py" ;;
172+
dbt)
173+
MODS="dbt.version sqlalchemy"
174+
FD="from freshdata.integrations.dbt import FreshDataDbtTransform; print(FreshDataDbtTransform)"
175+
TESTS="tests/test_integrations/test_dbt.py tests/test_integrations/test_dbt_tests_exporter.py tests/test_integrations/test_dbt_tests_exporter_yaml.py tests/test_integrations/test_dbt_transform_fail_audit.py tests/test_integrations/test_core.py" ;;
176+
kafka)
177+
MODS="kafka"
178+
FD="from freshdata.streaming._connectors import kafka_batches; print(kafka_batches)"
179+
TESTS="tests/test_streaming_cleaner.py tests/test_streaming_reports.py tests/test_streaming_cli.py" ;;
180+
flight)
181+
MODS="pyarrow.flight"
182+
FD="from freshdata.streaming._connectors import flight_batches; print(flight_batches)"
183+
TESTS="tests/test_streaming_cleaner.py tests/test_streaming_reports.py tests/test_streaming_cli.py" ;;
184+
semantic)
185+
MODS="onnxruntime tokenizers"
186+
FD="from freshdata.models._lazy import has_semantic_extra; assert has_semantic_extra()"
187+
TESTS="tests/test_semantic_extra_optional.py tests/test_models_runtime.py tests/test_models_download_robustness.py tests/test_models_registry.py tests/test_semantic_backends.py tests/test_cli_models.py" ;;
188+
privacy)
189+
MODS="presidio_analyzer presidio_anonymizer pyffx"
190+
FD="import freshdata.enterprise.privacy as p; print(p.__name__)"
191+
TESTS="tests/test_enterprise_privacy.py tests/test_privacy_adversarial.py tests/test_privacy_keyless_defaults.py tests/test_privacy_missing_and_labels.py tests/test_privacy_policy.py tests/test_privacy_policy_classification.py" ;;
192+
cleanlab)
193+
MODS="cleanlab sklearn"
194+
FD="import freshdata.enterprise.cleaner as c; print(c.__name__)"
195+
TESTS="tests/test_enterprise_cleaner.py" ;;
196+
viz)
197+
MODS="itables plotly great_tables"
198+
FD="from freshdata.render import require; [require(p) for p in ('itables', 'plotly', 'great_tables')]"
199+
TESTS="tests/test_render_html.py tests/test_peel_view.py tests/test_peel_notebook.py tests/test_report_rendering_fixes.py" ;;
200+
notebook)
201+
MODS="itables plotly great_tables anywidget IPython"
202+
FD="from freshdata.render import require; [require(p) for p in ('itables', 'plotly', 'great_tables', 'anywidget', 'IPython')]; import freshdata.render.notebook"
203+
TESTS="tests/test_render_html.py tests/test_peel_view.py tests/test_peel_notebook.py tests/test_report_rendering_fixes.py" ;;
204+
domains)
205+
MODS="yaml"
206+
FD="import freshdata.domains as d; print(d.__name__)"
207+
TESTS="tests/domains tests/test_domain_rules_transport_icpn.py" ;;
208+
excel)
209+
MODS="openpyxl"
210+
FD="import freshdata as fd; assert callable(fd.clean_excel)"
211+
TESTS="tests/test_clean_excel.py tests/test_csv_formula_sanitize.py" ;;
212+
entity-resolution)
213+
MODS="duckdb"
214+
FD="import freshdata.enterprise.entity_resolution as er; print(er.__name__)"
215+
TESTS="tests/test_enterprise_entity_resolution.py tests/test_enterprise_entity_resolution_dedupe.py tests/test_link.py tests/test_er_comparators.py tests/test_er_review_loop.py" ;;
216+
outofcore)
217+
MODS="polars duckdb pyarrow"
218+
FD="from freshdata.execution._lazy import require_polars, require_duckdb, require_pyarrow; require_polars(); require_duckdb(); require_pyarrow()"
219+
TESTS="tests/test_execution tests/test_polars_adapter.py" ;;
220+
all)
221+
MODS="polars pyarrow sklearn yaml cleanlab duckdb presidio_analyzer presidio_anonymizer pyffx itables plotly great_tables anywidget IPython openpyxl"
222+
FD="import freshdata.enterprise.privacy, freshdata.enterprise.cleaner, freshdata.render.notebook, freshdata.domains"
223+
TESTS="tests" ;;
224+
*) echo "unknown extra $EXTRA"; exit 1 ;;
225+
esac
226+
{
227+
echo "MODS=$MODS"
228+
echo "FD_SMOKE=$FD"
229+
echo "TESTS=$TESTS"
230+
} >> "$GITHUB_ENV"
231+
- name: Extra-only install (no dev) and import smoke
232+
# Checks the extra is self-sufficient: the dev extra already ships some
233+
# of these packages, which would hide a missing dependency.
234+
continue-on-error: true
235+
run: |
236+
python -m venv /tmp/xonly
237+
/tmp/xonly/bin/python -m pip install --upgrade pip
238+
/tmp/xonly/bin/pip install -e ".[$EXTRA]"
239+
/tmp/xonly/bin/python - <<'PY'
240+
import importlib, os, sys
241+
bad = []
242+
for m in os.environ["MODS"].split():
243+
try:
244+
importlib.import_module(m)
245+
print("EXTRA_ONLY_IMPORT_OK", m)
246+
except Exception as exc:
247+
print("EXTRA_ONLY_IMPORT_FAIL", m, type(exc).__name__, exc)
248+
bad.append(m)
249+
try:
250+
exec(os.environ["FD_SMOKE"])
251+
print("EXTRA_ONLY_FD_SMOKE_OK")
252+
except Exception as exc:
253+
print("EXTRA_ONLY_FD_SMOKE_FAIL", type(exc).__name__, exc)
254+
bad.append("freshdata-smoke")
255+
sys.exit(1 if bad else 0)
256+
PY
257+
- name: Install (dev + extra)
258+
run: |
259+
python -m pip install --upgrade pip
260+
if ! pip install -e ".[dev,$EXTRA]" pytest-timeout; then
261+
echo "EXTRA_INSTALL_FAIL=$EXTRA"
262+
echo "::error::pip install -e .[dev,$EXTRA] failed"
263+
exit 1
264+
fi
265+
echo "EXTRA_INSTALL_OK=$EXTRA"
266+
pip freeze > pip-freeze.txt
267+
- name: Import smoke
268+
run: |
269+
python - <<'PY'
270+
import importlib, os, sys
271+
bad = []
272+
for m in os.environ["MODS"].split():
273+
try:
274+
mod = importlib.import_module(m)
275+
print("IMPORT_OK", m, getattr(mod, "__version__", ""))
276+
except Exception as exc:
277+
print("IMPORT_FAIL", m, type(exc).__name__, exc)
278+
bad.append(m)
279+
try:
280+
exec(os.environ["FD_SMOKE"])
281+
print("FD_SMOKE_OK")
282+
except Exception as exc:
283+
print("FD_SMOKE_FAIL", type(exc).__name__, exc)
284+
bad.append("freshdata-smoke")
285+
sys.exit(1 if bad else 0)
286+
PY
287+
- name: Related tests
288+
run: |
289+
# shellcheck disable=SC2086
290+
python -m pytest $TESTS -m "not online and not large" \
291+
-o addopts="--strict-markers -q -rs" -p no:cacheprovider \
292+
--timeout 600 --junitxml=junit.xml
293+
- name: Count skips for missing modules
294+
if: always()
295+
run: |
296+
python - <<'PY'
297+
import os, re, xml.etree.ElementTree as ET
298+
if not os.path.exists("junit.xml"):
299+
print("NO_JUNIT"); raise SystemExit(0)
300+
root = ET.parse("junit.xml").getroot()
301+
pat = re.compile(r"No module named|could not import|not installed|unavailable|requires? .{0,40}install", re.I)
302+
total = passed = failed = skipped = missing = 0
303+
reasons = {}
304+
for tc in root.iter("testcase"):
305+
total += 1
306+
kids = {c.tag: c for c in tc}
307+
if "failure" in kids or "error" in kids:
308+
failed += 1
309+
elif "skipped" in kids:
310+
skipped += 1
311+
msg = kids["skipped"].get("message", "") or (kids["skipped"].text or "")
312+
if pat.search(msg):
313+
missing += 1
314+
reasons[msg[:160]] = reasons.get(msg[:160], 0) + 1
315+
else:
316+
passed += 1
317+
line = (f"EXTRA_RESULT extra={os.environ['EXTRA']} total={total} passed={passed} "
318+
f"failed={failed} skipped={skipped} skipped_missing_module={missing}")
319+
print(line)
320+
for r, n in sorted(reasons.items(), key=lambda kv: -kv[1]):
321+
print(f"SKIP_MISSING [{n}] {r}")
322+
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as fh:
323+
fh.write(line + "\n")
324+
PY
325+
- name: Upload results
326+
if: always()
327+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
328+
with:
329+
name: junit-extra-${{ matrix.extra }}
330+
path: |
331+
junit.xml
332+
pip-freeze.txt
333+
if-no-files-found: warn

0 commit comments

Comments
 (0)