Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions .github/workflows/test-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
# workflow_call from release-native.yml with `release: true`: all four platforms, Python 3.11 to 3.14
# This workflow never publishes: it has no id-token permission and no environment.
#
# Beside the full parity runs, the `pyver` job installs the already built linux-x86_64 wheel (py3-none: no rebuild) into the Pythons the full
# runs of the event leave out (a pull request: 3.11, 3.13, 3.14; main, nightly, dispatch: 3.11, 3.13; a release: none) and into the free-threaded
# 3.14t, and runs a small subset of the tests there (about 20 s of tests per Python). It is informational: release-native.yml does not need it.
# scripts/select_native_matrix.py decides its legs.
#
# One wheel per platform, py3-none-<platform tag>:
# linux-x86_64 manylinux_2_34 container manylinux_2_34_x86_64 libarcadedb.so + libjvector-x86_64.so
# linux-aarch64 manylinux_2_34 aarch64 container manylinux_2_34_aarch64 libarcadedb.so
Expand Down Expand Up @@ -106,6 +111,7 @@ jobs:
final: ${{ steps.ver.outputs.final }}
build: ${{ steps.plan.outputs.build }}
test: ${{ steps.plan.outputs.test }}
pyver: ${{ steps.plan.outputs.pyver }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down Expand Up @@ -524,6 +530,7 @@ jobs:
run: |
set -euo pipefail
tsv="$T/parity/parity-wheel.tsv"
# the pyver job has a copy of this awk: keep the two in sync
awk -F'\t' '
NR == 1 { next }
$1 == "TOTAL" { passed = $2; failed = $3; err = $4; notyet = $5; na = $6; skipped = $7; tot = 1; next }
Expand Down Expand Up @@ -562,3 +569,124 @@ jobs:
${{ runner.temp }}/parity/parity-wheel-xml/
if-no-files-found: warn
retention-days: 30

# The cheap Python-version coverage of the wheel of linux-x86_64 (py3-none: the same file for every Python, nothing is rebuilt): a subset of
# the test files, not the whole suite. The legs are the supported Pythons the full parity runs of this event leave out, plus the
# free-threaded 3.14t (scripts/select_native_matrix.py).
# INFORMATIONAL for now: release-native.yml (verify and the publish jobs) reads only the native-passed-* markers and the native-wheel-* wheels,
# never an artifact of this job, so a red leg never blocks a release. Promote it once it has been green for a while.
# 3.14t runs without polars, and so without test_dataframe_ingest.py (it imports polars at module level): polars ships abi3 wheels only
# (polars and polars-runtime-32 have no cp314t wheel, and an abi3 wheel cannot be installed on a free-threaded build). The other
# dependencies of the subset (numpy, pandas, pyarrow, jpype1, requests) all have cp314t wheels. polars is an optional extra of the wheel.
pyver:
name: Python ${{ matrix.python }} on the wheel (linux-x86_64)
needs: [plan, build]
# The linux-x86_64 build may have failed: then there is no wheel to download and this shows red, as the test job does.
if: ${{ !cancelled() && needs.plan.outputs.run == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
shell: bash
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.plan.outputs.pyver) }}
env:
KEY: linux-x86_64
PY: ${{ matrix.python }}
WITH_POLARS: ${{ matrix.polars }}
FREE_THREADED: ${{ matrix.freethreaded }}
# The subset: ctypes layer and database lifecycle (test_core, test_native_udf), queries and results (test_resultset), parameters
# (test_parameter_binding), type conversion (test_type_conversion), transactions (test_transaction_config), async (test_async_executor),
# threads (test_concurrency, test_free_threading), vectors (test_vector), numpy (test_numpy_support), arrow (test_resultset_arrow), pandas
# and arrow readers (test_columnar_readers). test_dataframe_ingest (pandas, polars and arrow ingest) is added where polars installs.
SUBSET: test_core.py,test_resultset.py,test_type_conversion.py,test_parameter_binding.py,test_transaction_config.py,test_async_executor.py,test_vector.py,test_numpy_support.py,test_resultset_arrow.py,test_columnar_readers.py,test_native_udf.py,test_concurrency.py,test_free_threading.py
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Paths
run: echo "T=$RUNNER_TEMP" >> "$GITHUB_ENV"

- name: Install uv
run: |
installer="$(mktemp)"
curl -LsSf --retry 5 --retry-all-errors --retry-delay 5 -o "$installer" "https://astral.sh/uv/${UV_VERSION}/install.sh"
sh "$installer"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"

# The wheel of the build job, not a rebuild: the very file the test job installs.
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: native-wheel-linux-x86_64
path: ${{ runner.temp }}/wheel

- name: Run the subset against the installed wheel
run: |
set -euo pipefail
w="$(ls "$T"/wheel/*.whl)"
mkdir -p "$T/parity"
files="$SUBSET"
with_polars=()
if [ "$WITH_POLARS" = true ]; then
files="$files,test_dataframe_ingest.py"
with_polars=(--with polars)
fi
echo "python $PY, files: $files"
cores="$(nproc)"
uv run --no-project --python "$PY" --with "$w" --with pytest --with numpy --with requests --with jpype1 --with pyarrow \
--with pandas ${with_polars[@]+"${with_polars[@]}"} \
python native/wheel/parity_wheel.py --repo "$GITHUB_WORKSPACE" --files "$files" \
--out "$T/parity/parity-wheel.tsv" --cores "0-$((cores - 1))" | tee "$T/parity/parity-wheel.run.log"

# A free-threaded leg only means something if the GIL stays off once the wheel and the extension modules are loaded.
- name: Report whether the GIL is enabled
run: |
set -euo pipefail
w="$(ls "$T"/wheel/*.whl)"
uv run --no-project --python "$PY" --with "$w" --with numpy --with pandas --with pyarrow --with jpype1 python - <<'PY' | tee -a "$GITHUB_STEP_SUMMARY"
import os
import sys

import arcadedb # noqa: F401
import jpype # noqa: F401
import numpy # noqa: F401
import pandas # noqa: F401
import pyarrow # noqa: F401

gil = sys._is_gil_enabled() if hasattr(sys, "_is_gil_enabled") else True
print(f"python {sys.version.split()[0]}, GIL enabled after loading the wheel and its dependencies: {gil}")
if os.environ["FREE_THREADED"] == "true" and gil:
print("::warning::the free-threaded leg re-enabled the GIL: it does not test free threading")
PY

- name: "Gate: no failure, error, crash, timeout or uncounted skip"
run: |
set -euo pipefail
tsv="$T/parity/parity-wheel.tsv"
# the same awk as the gate of the test job: keep the two in sync
awk -F'\t' '
NR == 1 { next }
$1 == "TOTAL" { passed = $2; failed = $3; err = $4; notyet = $5; na = $6; skipped = $7; tot = 1; next }
$8 != "" { bad = bad " " $1 "(" $8 ")" }
END {
if (!tot) { print "no TOTAL row" > "/dev/stderr"; exit 1 }
printf "parity %s py%s: passed=%s failed=%s error=%s notyet=%s na_jvm=%s skipped=%s\n", ENVIRON["KEY"], ENVIRON["PY"], passed, failed, err, notyet, na, skipped
if (bad != "") { print "files with a status:" bad > "/dev/stderr"; exit 1 }
if (passed + 0 == 0 || failed + 0 != 0 || err + 0 != 0) exit 1
# this repository accepts no skips; NOTYET and NA-JVM are the two counted kinds of the native build
if (skipped + 0 != 0) { print "skipped tests that are neither NOTYET nor NA-JVM: " skipped > "/dev/stderr"; exit 1 }
}' "$tsv" | tee -a "$GITHUB_STEP_SUMMARY"

- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
if: ${{ !cancelled() }}
with:
name: native-parity-pyver-py${{ matrix.python }}
path: |
${{ runner.temp }}/parity/parity-wheel.tsv
${{ runner.temp }}/parity/parity-wheel.run.log
${{ runner.temp }}/parity/parity-wheel-xml/
if-no-files-found: warn
retention-days: 30
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/development/ci.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ The workflow `test-native.yml` builds the native image of the `arcadedb` package

* A pull request runs it only when native paths change: `native/**`, `src/arcadedb/_native/**`, the native tests, or `test-native.yml` itself. It then runs on `linux-x86_64` with Python 3.12 only.
* A push to `main`, the nightly run, a manual run (dispatch) run all four platforms: `linux-x86_64`, `linux-aarch64`, `macos-arm64` and `windows-x64` (Python 3.12 and 3.14). A release (`release-native.yml` calls the workflow with `release: true`) runs the same four platforms on all four Python versions, 16 legs.
* The `pyver` job installs the already built `linux-x86_64` wheel (no rebuild) into the Pythons that the full parity runs of the event leave out (3.11, 3.13 and 3.14 on a pull request; 3.11 and 3.13 on a push to `main`, the nightly run and a dispatch; none on a release) and into the free-threaded 3.14t, and runs 13 test files there (14 where polars installs), about 20 seconds of tests per Python. It is informational: `release-native.yml` reads only the `native-passed-*` markers and the `native-wheel-*` wheels, so a red `pyver` leg never blocks a release. The 3.14t leg has no polars (no free-threaded wheel exists), so it skips `test_dataframe_ingest.py`. `scripts/select_native_matrix.py` decides its legs.
* A native image build takes about 1 to 2.5 hours for each platform. The organisation shares one runner limit with upstream ArcadeDB, so do not add runs lightly. Change the paths or the matrix of `test-native.yml` only for a reason that you can write down.

== Change the selection
Expand Down
28 changes: 25 additions & 3 deletions scripts/select_native_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

usage: select_native_matrix.py EVENT EVENT: pull_request | push | schedule | workflow_dispatch | release

Prints `build=<json>` and `test=<json>` lines (GITHUB_OUTPUT format). Publishing is not decided here: release-native.yml
does it. The GraalVM CE archives are pinned by sha256 per platform (GraalVM Community Edition only, never Oracle GraalVM).
Prints `build=<json>`, `test=<json>` and `pyver=<json>` lines (GITHUB_OUTPUT format). Publishing is not decided here:
release-native.yml does it. `pyver` is the cheap Python-version job (see PYVER_NOTE): a subset of the tests against the built
linux-x86_64 wheel on the Pythons the full parity run of the event does not cover, and always on the free-threaded 3.14t. It never
gates a release. The GraalVM CE archives are pinned by sha256 per platform (GraalVM Community Edition only, never Oracle GraalVM).
"""

from __future__ import annotations
Expand Down Expand Up @@ -87,11 +89,30 @@
PRERELEASE_REQUIRED = ["linux-x86_64", "linux-aarch64", "macos-arm64"]
ALL_PLATFORMS = list(PLATFORMS)

# The `pyver` job of test-native.yml installs the already built linux-x86_64 wheel (py3-none, no rebuild) into every Python below that the
# full parity run of the event does not cover, and runs a subset of the test files there. 3.14t is the free-threaded build of 3.14:
# it is not a classifier of the wheel and has no polars (polars ships abi3 wheels only, which a free-threaded Python cannot install).
FREE_THREADED_PYTHONS = ["3.14t"]
PYVER_NOTE = "informational: release-native.yml does not read it"

PULL_REQUEST_PLATFORMS = ["linux-x86_64"]
PULL_REQUEST_PYTHONS = ["3.12"]
MAIN_PYTHONS = ["3.12", "3.14"]


def pyver_legs(covered: list[str]) -> list[dict]:
"""The legs of the cheap Python-version job: every supported Python that `covered` (the full parity run) leaves out."""
return [
{
"python": py,
"freethreaded": py in FREE_THREADED_PYTHONS,
"polars": py not in FREE_THREADED_PYTHONS,
}
for py in ALL_PYTHONS + FREE_THREADED_PYTHONS
if py not in covered
]


def select(event: str) -> dict:
if event == "pull_request":
platforms, pythons = PULL_REQUEST_PLATFORMS, PULL_REQUEST_PYTHONS
Expand All @@ -112,7 +133,7 @@ def select(event: str) -> dict:
for k in platforms
for py in pythons
]
return {"build": build, "test": test}
return {"build": build, "test": test, "pyver": pyver_legs(pythons)}


def main(argv: list[str] | None = None) -> int:
Expand All @@ -125,6 +146,7 @@ def main(argv: list[str] | None = None) -> int:
chosen = select(a.event)
print(f"build={json.dumps(chosen['build'], separators=(',', ':'))}")
print(f"test={json.dumps(chosen['test'], separators=(',', ':'))}")
print(f"pyver={json.dumps(chosen['pyver'], separators=(',', ':'))}")
return 0


Expand Down
60 changes: 59 additions & 1 deletion tests/test_select_native_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,63 @@ def test_the_script_prints_github_output_lines():
text=True,
check=True,
).stdout.splitlines()
assert [line.split("=", 1)[0] for line in out] == ["build", "test"]
assert [line.split("=", 1)[0] for line in out] == ["build", "test", "pyver"]
assert json.loads(out[0].split("=", 1)[1])[0]["key"] == "linux-x86_64"
assert [leg["python"] for leg in json.loads(out[2].split("=", 1)[1])] == [
"3.11",
"3.13",
"3.14",
"3.14t",
]


def _pyver(event):
return [
(leg["python"], leg["freethreaded"], leg["polars"])
for leg in sel.select(event)["pyver"]
]


def test_the_python_version_job_covers_what_the_full_parity_run_leaves_out():
# a pull request tests 3.12 only: the other three Pythons and the free-threaded one run the cheap subset
assert _pyver("pull_request") == [
("3.11", False, True),
("3.13", False, True),
("3.14", False, True),
("3.14t", True, False),
]
# main, nightly and dispatch test 3.12 and 3.14 in full
for event in ("push", "schedule", "workflow_dispatch"):
assert _pyver(event) == [
("3.11", False, True),
("3.13", False, True),
("3.14t", True, False),
]
# a release tests 3.11 to 3.14 in full: only the free-threaded build is left
assert _pyver("release") == [("3.14t", True, False)]


def test_the_python_version_job_never_repeats_a_full_parity_leg_and_has_no_polars_on_a_free_threaded_python():
for event in ("pull_request", "push", "schedule", "workflow_dispatch", "release"):
chosen = sel.select(event)
full = {t["python"] for t in chosen["test"]}
assert not full & {leg["python"] for leg in chosen["pyver"]}, event
assert any(leg["freethreaded"] for leg in chosen["pyver"]), event
for leg in chosen["pyver"]:
assert leg["polars"] is (not leg["freethreaded"]), leg


def test_the_release_does_not_need_the_python_version_job():
workflows = ROOT / ".github" / "workflows"
test_native = (workflows / "test-native.yml").read_text()
release = (workflows / "release-native.yml").read_text()
assert re.search(r"^ pyver:\n", test_native, re.M)
# no job of either workflow waits for it
for text in (test_native, release):
for needs in re.findall(r"^ needs: (.*)$", text, re.M):
assert "pyver" not in needs, needs
# release-native.yml reads the markers and the wheels only, never the artifacts of the pyver job
patterns = re.findall(r"^\s+pattern: (.*)$", release, re.M)
assert patterns and all(
p in ("native-passed-*", "native-wheel-*") for p in patterns
), patterns
Loading