|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# PyVista parity gate: run PyVista's OWN full test suite against the built fvtk |
| 4 | +# wheel, with `vtkmodules.*` redirected to `fvtk.*`. This proves the contract |
| 5 | +# downstream actually depends on — not just our internal bit-exact / pixel-exact |
| 6 | +# gates, but PyVista's thousands of behavioral + plotting assertions driving the |
| 7 | +# fvtk graphics + data stack. |
| 8 | +# |
| 9 | +# Sister to ci/run-bitexact.sh / ci/run-renderexact.sh, but instead of our own |
| 10 | +# vtkmodules-only scenes it installs PyVista (pinned to tests/pyvista/PYVISTA_REF) |
| 11 | +# and runs its suite. ONE venv: stock `vtk` is NEVER installed, so any import |
| 12 | +# that slips past the fvtk shim fails loud (ModuleNotFoundError) rather than |
| 13 | +# silently testing stock VTK — never a false green. |
| 14 | +# |
| 15 | +# Heavy by design (full suite, software GL). NOT on the PR fast path — see the |
| 16 | +# `pyvista` job in .github/workflows/ci.yml (merge queue / dispatch / labeled PR). |
| 17 | +# |
| 18 | +# Usage: ci/run-pyvista.sh <wheel-dir> [base-python] (base-python default python3) |
| 19 | +set -euxo pipefail |
| 20 | + |
| 21 | +WHEELDIR="${1:?usage: ci/run-pyvista.sh <wheel-dir> [base-python]}" |
| 22 | +BASE_PY="${2:-python3}" |
| 23 | +SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" |
| 24 | +HERE="$SRC/tests/pyvista" |
| 25 | + |
| 26 | +REF="$(tr -d '[:space:]' < "$HERE/PYVISTA_REF")" |
| 27 | +[ -n "$REF" ] || { echo "ERROR: tests/pyvista/PYVISTA_REF is empty"; exit 1; } |
| 28 | + |
| 29 | +OUT="${PYVISTA_OUTDIR:-/tmp/pv-out}" |
| 30 | +rm -rf "$OUT"; mkdir -p "$OUT" |
| 31 | + |
| 32 | +# Headless software GL via Xvfb — the SAME path PyVista's own CI uses to render |
| 33 | +# (and to generate its committed image_cache): an Xvfb virtual display + Mesa |
| 34 | +# llvmpipe over GLX. We deliberately do NOT force surfaceless EGL here (unlike |
| 35 | +# run-renderexact.sh, which drives raw vtkmodules): PyVista warns + segfault- |
| 36 | +# guards when no DISPLAY is present, runs filterwarnings=error, and its image |
| 37 | +# cache is GLX/llvmpipe — so matching that environment is what keeps the suite |
| 38 | +# green. We wrap each pytest invocation in xvfb-run below; here we only pin |
| 39 | +# software rendering and keep every Plotter offscreen. |
| 40 | +export LIBGL_ALWAYS_SOFTWARE=1 |
| 41 | +export GALLIUM_DRIVER=llvmpipe |
| 42 | +export PYVISTA_OFF_SCREEN=true |
| 43 | +# Provides a real DISPLAY (silences _warn_xserver) + a fresh server per call. |
| 44 | +XVFB=(xvfb-run -a -s "-screen 0 1280x1024x24") |
| 45 | + |
| 46 | +# --- fetch PyVista at the pinned SHA (shallow, single commit) ---------------- |
| 47 | +# Pinning (not floating main) keeps the gate deterministic; .github/workflows/ |
| 48 | +# pyvista-bump.yml advances the pin through a self-validating PR. The committed |
| 49 | +# image_cache that PyVista's plotting tests diff against rides along with the |
| 50 | +# checkout. |
| 51 | +PVDIR="${PYVISTA_SRC:-/tmp/pyvista-src}" |
| 52 | +if [ ! -e "$PVDIR/pyproject.toml" ]; then |
| 53 | + rm -rf "$PVDIR"; mkdir -p "$PVDIR" |
| 54 | + git -C "$PVDIR" init -q |
| 55 | + git -C "$PVDIR" remote add origin https://github.com/pyvista/pyvista |
| 56 | + git -C "$PVDIR" fetch -q --depth 1 origin "$REF" |
| 57 | + git -C "$PVDIR" checkout -q FETCH_HEAD |
| 58 | +fi |
| 59 | +echo ">>> PyVista @ $(git -C "$PVDIR" rev-parse --short HEAD) (pinned $REF)" |
| 60 | + |
| 61 | +# --- venv: PyVista (no deps) + test group + fvtk wheel + redirect shim ------- |
| 62 | +"$BASE_PY" -m venv /tmp/pv |
| 63 | +/tmp/pv/bin/pip -q install --upgrade pip |
| 64 | +# Non-editable: the clone dir is named `pyvista`, which an editable install |
| 65 | +# mistakes for a namespace package. --no-deps so PyVista does NOT drag stock |
| 66 | +# `vtk` into the venv (the shim must be the ONLY vtkmodules provider). |
| 67 | +/tmp/pv/bin/pip -q install --no-deps "$PVDIR" |
| 68 | +# PyVista's `test` dependency-group (pytest, pytest-xdist, pytest-pyvista, ...). |
| 69 | +# pytest-timeout is NOT in that group, so add it explicitly for --timeout. |
| 70 | +/tmp/pv/bin/pip -q install --group "$PVDIR/pyproject.toml:test" pytest-timeout |
| 71 | +# Built fvtk wheel. --find-links resolves `fvtk` from the local dir while PyPI |
| 72 | +# stays available for fvtk's own deps (matplotlib/numpy/...) — so NO --no-index. |
| 73 | +/tmp/pv/bin/pip -q install --find-links "$WHEELDIR" fvtk |
| 74 | +# vtkmodules.* -> fvtk.* redirect, active at interpreter startup via sitecustomize |
| 75 | +# (.pth). Same shim the bit-exact / PGO harnesses use. |
| 76 | +SP=$(/tmp/pv/bin/python -c 'import sysconfig;print(sysconfig.get_paths()["purelib"])') |
| 77 | +cp "$SRC/tools/fvtk_shim.py" "$SP/_fvtk_shim.py" |
| 78 | +echo "import _fvtk_shim" > "$SP/_fvtk_shim.pth" |
| 79 | +# Put the venv's bin on PATH: we invoke python by absolute path (no `activate`), |
| 80 | +# so without this the CLI tests that shell out to the bare `pyvista`/`pytest` |
| 81 | +# console-scripts get FileNotFoundError. |
| 82 | +export PATH="/tmp/pv/bin:$PATH" |
| 83 | + |
| 84 | +# --- identity: prove PyVista is driving fvtk through the shim ---------------- |
| 85 | +/tmp/pv/bin/python - <<'PY' |
| 86 | +import vtkmodules |
| 87 | +assert "fvtk" in (vtkmodules.__file__ or ""), \ |
| 88 | + f"vtkmodules NOT redirected to fvtk (got {vtkmodules.__file__!r}) -- shim failed" |
| 89 | +import fvtk, pyvista |
| 90 | +from vtkmodules.vtkCommonCore import vtkVersion |
| 91 | +print("vtkmodules ->", vtkmodules.__file__) |
| 92 | +print("fvtk ->", fvtk.__file__) |
| 93 | +print("VTK version->", vtkVersion.GetVTKVersion()) |
| 94 | +print("pyvista ->", pyvista.__version__, pyvista.__file__) |
| 95 | +PY |
| 96 | + |
| 97 | +# --- deselect list ----------------------------------------------------------- |
| 98 | +# Static triage entries from tests/pyvista/deselect.txt (one nodeid per line). |
| 99 | +DESELECT=() |
| 100 | +while IFS= read -r raw || [ -n "$raw" ]; do |
| 101 | + line="${raw%%#*}" # strip comment |
| 102 | + line="${line#"${line%%[![:space:]]*}"}" # ltrim |
| 103 | + line="${line%"${line##*[![:space:]]}"}" # rtrim |
| 104 | + [ -n "$line" ] && DESELECT+=(--deselect "$line") |
| 105 | +done < "$HERE/deselect.txt" |
| 106 | + |
| 107 | +# snake_case opt-out: if this fvtk wheel was built with VTK_DISABLE_PYTHON_ |
| 108 | +# PROPERTIES, VTK emits no `obj.snake_case` descriptors and PyVista's snake_case |
| 109 | +# tests fail BY DESIGN, not by regression. Probe at runtime and deselect only |
| 110 | +# then, so the gate still exercises them against a wheel built WITH properties. |
| 111 | +SNAKE_DISABLED="$(/tmp/pv/bin/python -c 'import vtkmodules.vtkCommonCore as m; print("0" if hasattr(m.vtkObject(), "global_warning_display") else "1")')" |
| 112 | +if [ "$SNAKE_DISABLED" = "1" ]; then |
| 113 | + echo ">>> snake_case API disabled in this wheel: deselecting PyVista snake_case tests" |
| 114 | + DESELECT+=( |
| 115 | + --deselect "tests/test_attributes.py::test_vtk_snake_case_api_is_disabled" |
| 116 | + --deselect "tests/test_attributes.py::test_dir_snake_case_visible_when_allowed" |
| 117 | + --deselect "tests/core/test_utilities.py::test_vtk_snake_case" |
| 118 | + --deselect "tests/core/test_utilities.py::test_is_vtk_attribute" |
| 119 | + # Same root cause: with no snake_case property descriptors, PyVista cannot |
| 120 | + # tell which CamelCase names are VTK-inherited, so its dir() filtering |
| 121 | + # (hide-inherited / opt-in-show) can't engage. Fails by design, not by |
| 122 | + # regression — bare nodeids deselect every parametrization. |
| 123 | + --deselect "tests/test_attributes.py::test_dir_hides_vtk_inherited_attributes" |
| 124 | + --deselect "tests/test_attributes.py::test_dir_show_vtk_api_opt_in" |
| 125 | + ) |
| 126 | +else |
| 127 | + echo ">>> snake_case API present in this wheel: running PyVista snake_case tests" |
| 128 | +fi |
| 129 | + |
| 130 | +cd "$PVDIR" |
| 131 | +# Two SEPARATE pytest invocations matching PyVista's tox layout (test-core, |
| 132 | +# test-plotting). One xdist run over the whole tree cross-pollutes core fixtures |
| 133 | +# into plotting workers and triggers spurious check_gc teardown errors. |
| 134 | +# -n auto : full xdist parallelism within each env |
| 135 | +# -m "not needs_download" : skip network-flaky tests (array element, not a |
| 136 | +# string — a quoted string splits into broken tokens) |
| 137 | +# --timeout=120 : hang guard per test |
| 138 | +SHARED=(-n auto --tb=short --no-header -ra --color=no -m "not needs_download" --timeout=120) |
| 139 | + |
| 140 | +set +e |
| 141 | +echo "=== core tests ===" |
| 142 | +"${XVFB[@]}" /tmp/pv/bin/python -m pytest "${SHARED[@]}" "${DESELECT[@]+"${DESELECT[@]}"}" \ |
| 143 | + --ignore=tests/plotting \ |
| 144 | + --ignore=tests/typing \ |
| 145 | + --test_downloads \ |
| 146 | + --junitxml="$OUT/junit-core.xml" \ |
| 147 | + tests/ 2>&1 | tee "$OUT/pytest-core.log" |
| 148 | +STATUS_CORE=${PIPESTATUS[0]} |
| 149 | + |
| 150 | +echo "=== plotting tests ===" |
| 151 | +"${XVFB[@]}" /tmp/pv/bin/python -m pytest "${SHARED[@]}" "${DESELECT[@]+"${DESELECT[@]}"}" \ |
| 152 | + --disallow_unused_cache \ |
| 153 | + --junitxml="$OUT/junit-plotting.xml" \ |
| 154 | + tests/plotting 2>&1 | tee "$OUT/pytest-plotting.log" |
| 155 | +STATUS_PLOT=${PIPESTATUS[0]} |
| 156 | +set -e |
| 157 | + |
| 158 | +echo ">>> logs: $OUT/pytest-{core,plotting}.log" |
| 159 | +echo ">>> junit: $OUT/junit-{core,plotting}.xml" |
| 160 | +exit $(( STATUS_CORE | STATUS_PLOT )) |
0 commit comments