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
164 changes: 164 additions & 0 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
name: Install

# `pip install zudb`, on a machine that has nothing else on it.
#
# Every other job in this repository runs on a hosted image, and a
# hosted image is the least representative computer in the world: it
# has a compiler, a Rust toolchain, a git, a Python built by somebody
# who knew what was going to be built against it, and a hundred
# libraries that a wheel can quietly link to and get away with. The
# failures that only a user's machine sees are the ones nothing here
# looks for. A file left out of the wheel and read out of the checkout
# instead. A stub or a py.typed that the sdist has and the wheel does
# not. An extension linked against a symbol version the build image had
# and a slim image has not. A dependency that arrived because something
# else in the job had pulled it in.
#
# So this builds the wheel the release would build and installs it in a
# container that holds an interpreter, a package manager and nothing
# else, with the index turned off so that nothing can arrive to cover
# for a mistake. tools/smoke.py is the program it runs there: standard
# library only, no pytest, no fixtures, no checkout.
#
# Nightly rather than on every push, because what it catches is drift
# in things outside this repository. A base image whose glibc moved, a
# manylinux policy that went forward, a pip that changed how it reads a
# tag. None of that is in a diff anybody here writes, and all of it
# arrives on its own schedule.

on:
schedule:
# Late enough that the day's merges are in, and not on the hour,
# where every scheduled job on the service is queued behind every
# other one.
- cron: "23 5 * * *"
workflow_dispatch:
# The workflow and the program it runs are exercised on the pull
# request that changes them, since a nightly that broke is a nightly
# nobody reads for a week.
pull_request:
paths:
- .github/workflows/install.yml
- tools/smoke.py

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
CARGO_TERM_COLOR: always

jobs:
# The two Linux wheels that a container can install, built the way the
# release builds them: inside the pypa images, against the oldest
# interpreter the stable ABI covers. Release and not debug, because
# what is being installed has to be what would be published, and a
# debug extension is a different program with different link edges.
wheel:
name: ${{ matrix.libc }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- libc: manylinux
manylinux: "2_28"
image: quay.io/pypa/manylinux_2_28_x86_64
- libc: musllinux
manylinux: musllinux_1_2
image: quay.io/pypa/musllinux_1_2_x86_64
steps:
- uses: actions/checkout@v7
- uses: PyO3/maturin-action@v1
with:
target: x86_64
manylinux: ${{ matrix.manylinux }}
container: ${{ matrix.image }}
# Named rather than left to the toolchain file, which the
# build container does not read.
rust-toolchain: 1.97.1
args: --release --out dist -i /opt/python/cp311-cp311/bin/python
# A shared object cannot have a static C runtime linked into
# it, which is the musl default and a no-op everywhere else.
before-script-linux: |
export RUSTFLAGS="-C target-feature=-crt-static"
- uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.libc }}
path: dist/*.whl
if-no-files-found: error

# And the install, in a container that is the whole point of the job.
#
# docker run rather than a job container, because a job container has
# the runner's own Node mounted into it and half the reason to use a
# slim image is that nothing is mounted into it. This way the only
# things inside are the image, one wheel and one file.
#
# Three rows: the floor this package supports, the newest release, and
# musl, which is the platform where a wheel that was tagged wrongly
# installs anyway and then fails to import.
clean:
name: ${{ matrix.image }}
needs: wheel
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- image: python:3.11-slim
wheel: manylinux
- image: python:3.14-slim
wheel: manylinux
- image: python:3.14-alpine
wheel: musllinux
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v4
with:
name: wheel-${{ matrix.wheel }}
path: dist
- name: An install, on a machine with only the language runtime
run: |
docker run --rm \
-v "$PWD/dist:/dist:ro" \
-v "$PWD/tools/smoke.py:/smoke.py:ro" \
-w /tmp \
${{ matrix.image }} sh -c '
set -eu
# What the image is claimed to be, checked rather than
# believed, because the day a base image starts shipping a
# compiler is the day this job silently stops being about
# anything.
for tool in cc gcc clang rustc cargo make git; do
if command -v "$tool" >/dev/null 2>&1; then
echo "this image has $tool on it, so it is not the machine this job is about"
exit 1
fi
done
# --no-index is what makes the install a test. Without it
# a wheel that failed to build would be papered over by
# whatever the index has, and a dependency that crept in
# would arrive rather than fail.
python -m pip install --no-index --find-links /dist zudb
python -m pip check
python /smoke.py
'
# The gate is validated the only way a gate can be: the failure it
# exists to catch has to fail it. The same program in the same
# image with nothing installed, which is what a wheel that did not
# build, did not upload or did not install looks like from in
# here. It is also the check that this job is running the program
# at all, since a bind mount pointing at nothing and a container
# whose exit code went nowhere both look exactly like success.
- name: The failure the job is meant to catch, caught
run: |
set +e
docker run --rm \
-v "$PWD/tools/smoke.py:/smoke.py:ro" \
-w /tmp \
${{ matrix.image }} python /smoke.py
test $? -ne 0 || { echo "the smoke program passed without zudb installed"; exit 1; }
68 changes: 68 additions & 0 deletions tests/test_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""The clean-machine program, run here as well.

`tools/smoke.py` is what the nightly install job runs inside a container
that holds an interpreter, a package manager and nothing else, and that
container is the least convenient place in the world to find out that a
line of it went stale. So each piece runs here too, against the install
this suite already has, where a failure arrives with a traceback and a
name attached rather than as a red square once a day.

The one piece that cannot run here is the one that is about the machine
rather than about the client: this machine has pandas on it, because the
rest of the suite needs pandas.
"""

from __future__ import annotations

import importlib.util
from pathlib import Path

import pytest
import smoke
import zudb

# `maturin develop`, which is how this is worked on, puts the checkout
# on the path instead of installing anything, so the one check that is
# about where the package came from has nothing to say here. Every job
# in CI installs the wheel, which is where it does.
WHERE = Path(zudb.__file__).resolve()
INSTALLED = "site-packages" in WHERE.parts or "dist-packages" in WHERE.parts


@pytest.mark.skipif(not INSTALLED, reason=f"zudb here is a checkout at {WHERE}, not an install")
def test_the_package_under_test_is_an_install() -> None:
smoke.imported_from_an_install()


def test_the_quickstart_runs(tmp_path) -> None:
smoke.a_graph(tmp_path / "social.zu1")


def test_the_bulk_paths_run(tmp_path) -> None:
smoke.the_bulk_path(tmp_path / "roads.zu1")


def test_a_failure_is_a_failure(tmp_path) -> None:
path = tmp_path / "social.zu1"
smoke.a_graph(path)
smoke.a_failure(path)


def test_the_dbapi_front_door_runs(tmp_path) -> None:
path = tmp_path / "social.zu1"
smoke.a_graph(path)
smoke.pep_249(path)


def test_the_event_loop_front_door_runs(tmp_path) -> None:
path = tmp_path / "social.zu1"
smoke.a_graph(path)
smoke.an_event_loop(path)


@pytest.mark.skipif(
importlib.util.find_spec("pandas") is not None,
reason="this check is about a machine with no pandas on it, and this one has pandas",
)
def test_nothing_arrived_with_the_wheel() -> None:
smoke.nothing_else_installed()
Loading
Loading