256 lines
11 KiB
YAML
256 lines
11 KiB
YAML
name: Parallel Tests
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
test_file:
|
|
description: Run one discovered test file
|
|
required: true
|
|
default: archivebox/tests/test_cli_list.py
|
|
type: string
|
|
workflow_call:
|
|
inputs:
|
|
test_file:
|
|
description: Run one discovered test file; empty runs the complete matrix
|
|
required: false
|
|
default: ""
|
|
type: string
|
|
|
|
env:
|
|
PYTHONIOENCODING: utf-8
|
|
PYTHONLEGACYWINDOWSSTDIO: utf-8
|
|
USE_COLOR: False
|
|
UV_NO_SOURCES: "1"
|
|
CI_PYTHON_VERSION: "3.13"
|
|
|
|
jobs:
|
|
discover-tests:
|
|
name: Discover every test
|
|
# Selection must run on GitHub so an offline NAS cannot block the fallback.
|
|
runs-on: ubuntu-24.04
|
|
outputs:
|
|
tests: ${{ steps.discover.outputs.tests }}
|
|
runner: ${{ steps.runner.outputs.use-runner || '["ubuntu-24.04"]' }}
|
|
|
|
steps:
|
|
- name: Select available CI capacity
|
|
id: runner
|
|
if: vars.UGNAS_CI_ENABLED == 'true' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
|
|
uses: ArchiveBox/monorepo/.github/actions/select-ci-runner@main
|
|
with:
|
|
github-token: ${{ secrets.RUNNER_STATUS_TOKEN }}
|
|
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
with:
|
|
submodules: true
|
|
fetch-depth: 1
|
|
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
with:
|
|
python-version: ${{ env.CI_PYTHON_VERSION }}
|
|
|
|
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
with:
|
|
version: "0.11.3"
|
|
enable-cache: false
|
|
cache-dependency-glob: uv.lock
|
|
|
|
- name: Resolve discovery tools through abxpkg
|
|
env:
|
|
ABXPKG_LIB_DIR: ${{ runner.temp }}/abx-lib
|
|
run: |
|
|
set -Eeuo pipefail
|
|
ABXPKG_SPEC="$(uv run --no-cache --no-project python -c 'import tomllib; package = next(item for item in tomllib.load(open("uv.lock", "rb"))["package"] if item["name"] == "abxpkg"); wheel = package["wheels"][0]; print("abxpkg @ {}#{}".format(wheel["url"], wheel["hash"].replace(":", "=")))')"
|
|
test -n "$ABXPKG_SPEC"
|
|
mkdir -p "$ABXPKG_LIB_DIR/env/bin"
|
|
export PYTHON_BINARY="$pythonLocation/bin/python"
|
|
test -x "$PYTHON_BINARY"
|
|
uv run --no-cache --no-project --with "$ABXPKG_SPEC" abxpkg env \
|
|
--install \
|
|
--lib="$ABXPKG_LIB_DIR" \
|
|
--deps-from="$GITHUB_WORKSPACE/.github/configs/ci-tooling.json:ci_binaries"
|
|
{
|
|
echo "PYTHON_BINARY=$ABXPKG_LIB_DIR/env/bin/python"
|
|
echo "JQ_BINARY=$ABXPKG_LIB_DIR/env/bin/jq"
|
|
} >> "$GITHUB_ENV"
|
|
test -L "$ABXPKG_LIB_DIR/env/bin/python"
|
|
test -x "$ABXPKG_LIB_DIR/env/bin/python"
|
|
test -L "$ABXPKG_LIB_DIR/env/bin/jq"
|
|
test -x "$ABXPKG_LIB_DIR/env/bin/jq"
|
|
|
|
- name: Discover every test exactly once
|
|
id: discover
|
|
env:
|
|
TEST_FILE: ${{ inputs.test_file }}
|
|
UGNAS_CI_MAX_JOBS: ${{ vars.UGNAS_CI_MAX_JOBS || 3 }}
|
|
run: |
|
|
set -Eeuo pipefail
|
|
mapfile -t output < <("$PYTHON_BINARY" .github/scripts/discover_test_matrix.py)
|
|
test "${#output[@]}" -eq 2
|
|
if [[ -n "$TEST_FILE" ]]; then
|
|
output[1]="$("$JQ_BINARY" -ce '[.[] | select(.paths == [env.TEST_FILE])] | select(length == 1)' <<< "${output[1]}")"
|
|
fi
|
|
echo "tests=${output[1]}" >> "$GITHUB_OUTPUT"
|
|
echo "${output[0]}"
|
|
"$JQ_BINARY" -e 'length > 0 and length <= 256 and ([.[].paths[]] | length == (unique | length))' <<< "${output[1]}"
|
|
|
|
tests:
|
|
name: ${{ matrix.test.name }}
|
|
needs: discover-tests
|
|
# Opt in trusted branches only; PRs always use GitHub-hosted runners.
|
|
# Ordinary Linux tests are eligible; # ci-runner: hosted declares exceptions.
|
|
# The hosted discovery job falls back when workers are offline or the
|
|
# availability API fails. UGNAS_CI_ENABLED=false disables the pool entirely.
|
|
runs-on: ${{ fromJSON(matrix.test.ugnas && needs.discover-tests.outputs.runner || '["ubuntu-24.04"]') }}
|
|
env:
|
|
CHROME_HEADLESS: "true"
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
test: ${{ fromJson(needs.discover-tests.outputs.tests) }}
|
|
|
|
steps:
|
|
- name: Set job-local runtime paths
|
|
env:
|
|
CI_RUNNER_ENVIRONMENT: ${{ runner.environment }}
|
|
run: |
|
|
workers=2
|
|
cache_suffix=''
|
|
if [[ "$CI_RUNNER_ENVIRONMENT" == github-hosted ]]; then
|
|
workers="$(nproc)"
|
|
cache_suffix="-workers-$workers"
|
|
# Avoid the hosted image's cold Chromium executable page faults.
|
|
# abxpkg owns installation and reuse in the existing runtime cache.
|
|
echo "CHROME_BINPROVIDERS=playwright,puppeteer" >> "$GITHUB_ENV"
|
|
fi
|
|
{
|
|
echo "CI_TEST_WORKERS=$workers"
|
|
echo "ABXPKG_CACHE_WORKERS_SUFFIX=$cache_suffix"
|
|
echo "ABXPKG_LIB_DIR=$RUNNER_TEMP/archivebox-lib"
|
|
echo "DATA_DIR=$RUNNER_TEMP/archivebox-test-data"
|
|
echo "PERSONAS_DIR=$RUNNER_TEMP/abx-personas"
|
|
echo "CHROME_USER_DATA_DIR=$RUNNER_TEMP/abx-personas/Default/chrome_profile"
|
|
} >> "$GITHUB_ENV"
|
|
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
with:
|
|
submodules: true
|
|
fetch-depth: 1
|
|
|
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
with:
|
|
python-version: ${{ env.CI_PYTHON_VERSION }}
|
|
architecture: x64
|
|
id: python
|
|
|
|
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
|
|
with:
|
|
version: "0.11.3"
|
|
enable-cache: false
|
|
cache-dependency-glob: uv.lock
|
|
|
|
# Self-hosted tool storage survives jobs. Normal abxpkg validation and
|
|
# install locks allow reuse across files; cold-install tests use tmp_path.
|
|
- name: Reuse persistent runner tools
|
|
if: runner.environment == 'self-hosted'
|
|
env:
|
|
PERSISTENT_ABXPKG_LIB: ${{ runner.tool_cache }}/abxpkg/${{ github.repository }}/${{ steps.python.outputs.python-version }}
|
|
run: |
|
|
mkdir -p "$PERSISTENT_ABXPKG_LIB"
|
|
echo "ABXPKG_LIB_DIR=$PERSISTENT_ABXPKG_LIB" >> "$GITHUB_ENV"
|
|
|
|
# Restored binaries still pass normal abxpkg resolution/version checks.
|
|
# Installation and isolation tests explicitly choose fresh temporary libs.
|
|
- name: Identify runtime dependency cache
|
|
shell: bash
|
|
run: |
|
|
uv run --no-project python - <<'PYTHON' >> "$GITHUB_ENV"
|
|
import hashlib
|
|
import json
|
|
import tomllib
|
|
from pathlib import Path
|
|
|
|
packages = tomllib.loads(Path("uv.lock").read_text())["package"]
|
|
versions = {p["name"]: p["version"] for p in packages if p["name"] in {"abxpkg", "abx-plugins"} and "editable" not in p["source"]}
|
|
print("ABXPKG_CACHE_DEPS=" + hashlib.sha256(json.dumps(versions, sort_keys=True).encode()).hexdigest())
|
|
PYTHON
|
|
|
|
- name: Reuse validated runtime dependencies
|
|
if: runner.environment != 'self-hosted'
|
|
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
|
with:
|
|
path: ${{ runner.temp }}/archivebox-lib
|
|
key: abxpkg-v1-${{ runner.os }}-${{ runner.arch }}-${{ runner.environment }}-${{ steps.python.outputs.python-version }}-${{ matrix.test.name }}-${{ env.ABXPKG_CACHE_DEPS }}${{ env.ABXPKG_CACHE_WORKERS_SUFFIX }}
|
|
restore-keys: |
|
|
abxpkg-v1-${{ runner.os }}-${{ runner.arch }}-${{ runner.environment }}-${{ steps.python.outputs.python-version }}-${{ matrix.test.name }}-
|
|
|
|
- name: Resolve optional build dependencies through abxpkg
|
|
if: matrix.test.extra != '' || contains(matrix.test.paths_arg, 'test_postgres_backend')
|
|
env:
|
|
TEST_EXTRA: ${{ matrix.test.extra }}
|
|
TEST_PATHS: ${{ matrix.test.paths_arg }}
|
|
run: |
|
|
set -Eeuo pipefail
|
|
ABXPKG_SPEC="$(uv run --no-cache --no-project python -c 'import tomllib; package = next(item for item in tomllib.load(open("uv.lock", "rb"))["package"] if item["name"] == "abxpkg"); wheel = package["wheels"][0]; print("abxpkg @ {}#{}".format(wheel["url"], wheel["hash"].replace(":", "=")))')"
|
|
test -n "$ABXPKG_SPEC"
|
|
dependency_sources=()
|
|
if [[ -n "$TEST_EXTRA" ]]; then
|
|
dependency_sources+=(--deps-from="$GITHUB_WORKSPACE/.github/configs/ci-linux-build.json:required_binaries")
|
|
fi
|
|
if [[ "$TEST_PATHS" == *test_postgres_backend* ]]; then
|
|
dependency_sources+=(--deps-from="$GITHUB_WORKSPACE/.github/configs/ci-linux-build.json:postgres_binaries")
|
|
fi
|
|
uv run --no-cache --no-project --with "$ABXPKG_SPEC" abxpkg env \
|
|
--install \
|
|
--no-cache \
|
|
--json \
|
|
--lib="$ABXPKG_LIB_DIR" \
|
|
"${dependency_sources[@]}"
|
|
|
|
- name: Install test dependencies
|
|
env:
|
|
TEST_EXTRA: ${{ matrix.test.extra }}
|
|
run: |
|
|
set -Eeuo pipefail
|
|
extra_args=()
|
|
if [[ -n "$TEST_EXTRA" ]]; then
|
|
extra_args=(--extra "$TEST_EXTRA")
|
|
fi
|
|
uv sync --no-cache --locked --dev "${extra_args[@]}"
|
|
- name: Run ${{ matrix.test.name }}
|
|
env:
|
|
TEST_PATHS: ${{ matrix.test.paths_arg }}
|
|
CI_RUNNER_ENVIRONMENT: ${{ runner.environment }}
|
|
run: |
|
|
set -Eeuo pipefail
|
|
echo "Running pytest with $CI_TEST_WORKERS workers on $CI_RUNNER_ENVIRONMENT"
|
|
read -r -a test_paths <<< "$TEST_PATHS"
|
|
test_args=()
|
|
for test_path in "${test_paths[@]}"; do
|
|
test -f "$test_path"
|
|
test_args+=("$GITHUB_WORKSPACE/$test_path")
|
|
done
|
|
# Each test still gets isolated DB/filesystem state from conftest.
|
|
# Keep one worker pool per batch instead of starting it for every file.
|
|
mkdir -p "$DATA_DIR" "$PERSONAS_DIR" "$RUNNER_TEMP/pytest"
|
|
uv run --no-cache --no-sync --no-sources pytest -vs -n "$CI_TEST_WORKERS" "${test_args[@]}" --durations=20 \
|
|
--basetemp="$RUNNER_TEMP/pytest" \
|
|
--junitxml="$RUNNER_TEMP/test-results/results.xml"
|
|
- name: Save test results
|
|
if: always()
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: test-results-${{ strategy.job-index }}
|
|
path: ${{ runner.temp }}/test-results/*.xml
|
|
if-no-files-found: error
|
|
retention-days: 14
|
|
- name: Save failed runtime diagnostics
|
|
if: failure()
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: runtime-logs-${{ strategy.job-index }}
|
|
path: ${{ runner.temp }}/pytest/**/*.log
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|