From 82e76d06d54fa014d0fd0a07643d659d1989d717 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Tue, 18 Aug 2026 14:54:28 -0700 Subject: [PATCH 01/11] perf(ci): parallelize Python tests and shorten the macOS Rust job CI end-to-end time per commit is a median of 26.6 min and a p90 of 60.9 min, measured over the ~1000 PR-triggered runs in a two-day window. The Python workflow is the critical path on 74 of 132 commits and Rust on 51. Three of the largest contributors were single-threaded work on multi-core runners: - `Compatibility Tests` ran 292 tests single-process on a 4-core runner for 1283s, and was the last job to finish in 25 of 33 Python runs. The suite is subprocess-bound, and `nightly_run.yml` already runs part of it under xdist at `nproc * 4`. `COMPAT_TEMP_VENV` also disabled the persistent venv cache that `conftest.py` supports, so each run rebuilt ~18 venvs from PyPI -- leaving it unset lets xdist workers share one venv per version through the flock that `VenvFactory` already holds for exactly this purpose. - `make test` ran 1363 tests serially in 673s in five separate jobs. The Makefile grows a `PYTEST_WORKERS` knob, defaulted off so a local `make test` is unchanged, and the `run_tests` action passes `auto`. - `mac-build` used plain `cargo test`, one test binary at a time. macOS is the scarcest runner pool in CI (p90 queue 16.7 min against under 2 min for every other pool), so this job is now nextest and drops the bench compile-check that `build-no-lock` already performs on Linux. Doctests are unaffected: nextest does not run them, but `linux-arm` and `windows-build` still invoke `cargo test`. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/python.yml | 12 ++++++++---- .github/workflows/run_tests/action.yml | 6 +++++- .github/workflows/rust.yml | 16 +++++++++++----- python/Makefile | 9 +++++++++ python/pyproject.toml | 2 ++ python/uv.lock | 26 ++++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 10 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 98831ac6648..c69e181e5ad 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -185,7 +185,7 @@ jobs: compat: needs: linux-wheel timeout-minutes: 60 - runs-on: ubuntu-24.04 + runs-on: ubuntu-24.04-8x name: Compatibility Tests defaults: run: @@ -208,11 +208,15 @@ jobs: - name: Install dependencies run: | pip install $(ls target/wheels/pylance-*.whl)[tests,ray] + # Nearly every test here shells out to an old pylance in its own venv, so the + # job spends most of its time waiting on subprocesses rather than on CPU. + # Oversubscribing cores follows what nightly_run.yml already does with this + # suite. Leaving COMPAT_TEMP_VENV unset is what makes that pay off: the venvs + # then land in the shared cache directory, where VenvFactory's flock lets all + # workers reuse one venv per version instead of each building its own. - name: Run compatibility tests run: | - make compattest - env: - COMPAT_TEMP_VENV: 1 + make compattest PYTEST_WORKERS=$(( $(nproc) * 2 )) linux-arm: timeout-minutes: 45 diff --git a/.github/workflows/run_tests/action.yml b/.github/workflows/run_tests/action.yml index c5942a9c2ed..1a6786f84e2 100644 --- a/.github/workflows/run_tests/action.yml +++ b/.github/workflows/run_tests/action.yml @@ -13,6 +13,10 @@ inputs: required: false description: "Install and preload the prebuilt memtest library" default: "false" + pytest-workers: + required: false + description: "pytest-xdist worker count; 'auto' matches the runner's cores" + default: "auto" runs: using: "composite" steps: @@ -42,4 +46,4 @@ runs: - name: Run python tests shell: bash working-directory: python - run: make test + run: make test PYTEST_WORKERS=${{ inputs.pytest-workers }} diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8d2df7b0697..59160389f6e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -346,15 +346,21 @@ jobs: run: | rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 + - name: Install cargo-nextest + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # nextest + with: + tool: nextest + # macOS runner capacity is the scarcest in CI, so this job is kept as short + # as it can be. nextest runs each test in its own process and schedules + # across all cores, rather than one test binary at a time. Benchmarks are + # only compile-checked here, which build-no-lock already does on Linux; + # nothing about bench compilation is macOS-specific. - name: Build tests run: | - cargo test --profile ci --locked --features fp16kernels,cli,dynamodb,substrait --no-run + cargo nextest run --cargo-profile ci --locked --features fp16kernels,cli,dynamodb,substrait --no-run - name: Run tests run: | - cargo test --profile ci --features fp16kernels,cli,dynamodb,substrait - - name: Check benchmarks - run: | - cargo check --profile ci --benches --features fp16kernels,cli,dynamodb,substrait + cargo nextest run --cargo-profile ci --features fp16kernels,cli,dynamodb,substrait windows-build: runs-on: windows-latest-4x defaults: diff --git a/python/Makefile b/python/Makefile index 73a2945cfb9..d54b3fc53ae 100644 --- a/python/Makefile +++ b/python/Makefile @@ -18,6 +18,15 @@ ifeq ($(CI), true) PYTEST_ARGS += --durations=30 endif +# Number of pytest-xdist workers, e.g. `auto` or an explicit count. Empty (the +# default) runs serially; CI sets it per runner size. Much of the suite is spent +# waiting on IO rather than saturating the Rust core's own thread pools, so +# sharding across processes wins back time a single pytest process leaves idle. +PYTEST_WORKERS ?= +ifneq ($(strip $(PYTEST_WORKERS)),) + PYTEST_ARGS += -n $(PYTEST_WORKERS) +endif + help: ## Show this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST) diff --git a/python/pyproject.toml b/python/pyproject.toml index 6c8defcab9e..71b81eecafc 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -57,6 +57,7 @@ tests = [ "polars[pyarrow,pandas]", "psutil", "pytest", + "pytest-xdist", "tqdm", "datafusion>=54,<55", ] @@ -80,6 +81,7 @@ tests = [ "polars[pyarrow,pandas]==1.34.0", "psutil==7.1.0", "pytest==8.4.2", + "pytest-xdist==3.8.0", "tqdm==4.67.1", "datafusion==54.0.0", "opentelemetry-sdk==1.30.0", diff --git a/python/uv.lock b/python/uv.lock index fe581b05222..f6ef0b30405 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -644,6 +644,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "filelock" version = "3.29.7" @@ -2423,6 +2432,7 @@ tests = [ { name = "polars", extra = ["pandas", "pyarrow"] }, { name = "psutil" }, { name = "pytest" }, + { name = "pytest-xdist" }, { name = "tqdm" }, ] torch = [ @@ -2450,6 +2460,7 @@ tests = [ { name = "polars", extra = ["pandas", "pyarrow"] }, { name = "psutil" }, { name = "pytest" }, + { name = "pytest-xdist" }, { name = "tqdm" }, ] @@ -2474,6 +2485,7 @@ requires-dist = [ { name = "pyright", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'tests'" }, { name = "pytest-benchmark", marker = "extra == 'benchmarks'" }, + { name = "pytest-xdist", marker = "extra == 'tests'" }, { name = "ruff", marker = "extra == 'dev'", specifier = "==0.11.2" }, { name = "torch", marker = "extra == 'torch'", specifier = ">=2.0" }, { name = "tqdm", marker = "extra == 'tests'" }, @@ -2499,6 +2511,7 @@ tests = [ { name = "polars", extras = ["pyarrow", "pandas"], specifier = "==1.34.0" }, { name = "psutil", specifier = "==7.1.0" }, { name = "pytest", specifier = "==8.4.2" }, + { name = "pytest-xdist", specifier = "==3.8.0" }, { name = "tqdm", specifier = "==4.67.1" }, ] @@ -2663,6 +2676,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/d6/b41653199ea09d5969d4e385df9bbfd9a100f28ca7e824ce7c0a016e3053/pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89", size = 44259, upload-time = "2024-10-30T11:51:45.94Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" From db01ac796e3da0be095d5eb7990adc17c6f24ac2 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 19 Aug 2026 09:08:01 -0700 Subject: [PATCH 02/11] fix(python): make compat version parametrization deterministic `compat_versions` built its parameter list by iterating a set of version strings. Set iteration order for strings depends on PYTHONHASHSEED, which differs per process, so the collected test IDs came out in a different order every run -- 11 distinct orderings across 12 seeds locally. That was invisible while pytest ran in one process. Under pytest-xdist each worker is its own process with its own hash seed, so the workers disagree on collection order and xdist aborts the run with "Different tests were collected". Because `make test` collects (and then skips) the compat directory, this broke the whole unit suite, not just the compat job. Sorting by version fixes the ordering and also makes test IDs stable between runs, which `--lf` and shard-to-shard comparisons both rely on. Co-Authored-By: Claude Opus 5 (1M context) --- python/python/tests/compat/compat_decorator.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python/python/tests/compat/compat_decorator.py b/python/python/tests/compat/compat_decorator.py index fdfe09a6879..f3d48e9d763 100644 --- a/python/python/tests/compat/compat_decorator.py +++ b/python/python/tests/compat/compat_decorator.py @@ -233,8 +233,14 @@ def check_write(self): # Write data pass """ - version = set([min_version, *VERSIONS]) - versions = [v for v in version if Version(v) >= Version(min_version)] + # Sorted rather than taken straight off the set: set iteration order for + # strings depends on PYTHONHASHSEED, which differs per process, so every + # pytest-xdist worker would otherwise collect these parameters in its own + # order and xdist rejects the run as an inconsistent collection. + versions = sorted( + (v for v in {min_version, *VERSIONS} if Version(v) >= Version(min_version)), + key=Version, + ) def decorator(cls): # Extract existing parametrize marks from the class From ad36c06a604606a929eaa36395123c535a737f51 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 19 Aug 2026 09:10:04 -0700 Subject: [PATCH 03/11] perf(ci): run Windows Rust tests under nextest windows-build was the last job to finish the Rust workflow at 21.0m, after mac-build moved to nextest. It used plain `cargo test`, which runs one test binary at a time; `Run tests` alone was 10.2m of that. Switches to nextest and drops the benchmark compile-check, matching what mac-build now does. Doctests are unaffected -- nextest skips them, but linux-arm still runs `cargo test` with all features. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/rust.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 59160389f6e..00d1cd786bd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -378,12 +378,17 @@ jobs: 7z x protoc.zip Add-Content $env:GITHUB_PATH "C:\protoc\bin" shell: powershell + - name: Install cargo-nextest + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # nextest + with: + tool: nextest + # Same reasoning as mac-build: nextest schedules across all cores instead + # of running one test binary at a time, and build-no-lock already + # compile-checks the benchmarks on Linux. - name: Build tests - run: cargo test --profile ci --locked --no-run + run: cargo nextest run --cargo-profile ci --locked --no-run - name: Run tests - run: cargo test --profile ci - - name: Check benchmarks - run: cargo check --profile ci --benches + run: cargo nextest run --cargo-profile ci qemu-pre-haswell: # Verifies that lance-linalg's runtime SIMD dispatch still works From 3e63f07ffdaf6ce0c4a9ee7280daabf82174cc42 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 19 Aug 2026 09:27:05 -0700 Subject: [PATCH 04/11] fix(python): exit the forked child in test_fork `test_table_roundtrip` called bare `os.fork()` and let the child fall through into the rest of the test and the rest of the pytest session. Under a single pytest process that merely duplicated some work. Under pytest-xdist the child also inherits the worker's execnet connection to the controller, so it reports a second result for the same test; the controller then hits `ValueError: list.remove(x): x not in list` in its scheduler and aborts the whole session. That killed the Linux x86 and ARM jobs, while macOS happened to win the race and Windows skips the test entirely. The child now does its post-fork reads and leaves through `os._exit`, and the parent waits on it. That also makes the test stronger: previously nothing the child asserted could fail the run, so the post-fork read was never actually verified. Co-Authored-By: Claude Opus 5 (1M context) --- python/python/tests/test_fork.py | 40 +++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/python/python/tests/test_fork.py b/python/python/tests/test_fork.py index f36e13debed..c057d41a9e5 100644 --- a/python/python/tests/test_fork.py +++ b/python/python/tests/test_fork.py @@ -3,6 +3,7 @@ import os import sys +import traceback from pathlib import Path import lance @@ -24,14 +25,7 @@ def create_table(num_rows) -> pa.Table: ) -@pytest.mark.skipif(sys.platform == "win32", reason="Test not applicable on Windows") -def test_table_roundtrip(tmp_path: Path): - uri = tmp_path - - tbl = create_table(100) - lance.write_dataset(tbl, uri) - - os.fork() +def check_reads(uri: Path, tbl: pa.Table): dataset = lance.dataset(uri) assert dataset.uri == str(uri.absolute()) assert tbl.schema == dataset.schema @@ -42,3 +36,33 @@ def test_table_roundtrip(tmp_path: Path): table = dataset.to_table(columns=["a"], limit=20) assert len(table) == 20 + + +@pytest.mark.skipif(sys.platform == "win32", reason="Test not applicable on Windows") +def test_table_roundtrip(tmp_path: Path): + uri = tmp_path + + tbl = create_table(100) + lance.write_dataset(tbl, uri) + + child = os.fork() + if child == 0: + # The child has to leave through os._exit. Returning would run the rest + # of the pytest session a second time, and under pytest-xdist it would + # also report a second result for this test over the execnet connection + # inherited from the worker, which crashes the controller's scheduler. + status = 0 + try: + check_reads(uri, tbl) + except BaseException: + traceback.print_exc() + status = 1 + os._exit(status) + + check_reads(uri, tbl) + # Waiting on the child is what actually asserts the post-fork read worked; + # nothing it raises can reach this process. + _, wait_status = os.waitpid(child, 0) + assert os.waitstatus_to_exitcode(wait_status) == 0, ( + "reading the dataset failed in the forked child" + ) From 908d03b1a3363db249a370819128973d3138f62f Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 19 Aug 2026 09:51:57 -0700 Subject: [PATCH 05/11] perf(ci): run linux-arm Rust tests under nextest linux-arm was left on `cargo test` when mac-build and windows-build moved to nextest, to keep doctest coverage. It is now the longest job in the workflow at 20.5m, 13.7m of which is running tests one binary at a time. Run the tests under nextest and the doctests as a separate `cargo test --doc` step, so the bulk of the work parallelizes without losing doctests. The feature list is resolved once into the environment rather than recomputed per step. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/rust.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 00d1cd786bd..44fe1680125 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -284,20 +284,31 @@ jobs: rustup default stable - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 + - name: Install cargo-nextest + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # nextest + with: + tool: nextest - name: Install dependencies run: | sudo apt -y -qq update sudo apt install -y protobuf-compiler libssl-dev pkg-config - - name: Build tests + - name: Resolve feature list run: | ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | grep -v -e protoc | sort | uniq | paste -s -d "," -` - cargo test --profile ci --locked --features ${ALL_FEATURES} --no-run + echo "ALL_FEATURES=${ALL_FEATURES}" >> $GITHUB_ENV + - name: Build tests + run: | + cargo nextest run --cargo-profile ci --locked --features ${ALL_FEATURES} --no-run - name: Start DynamodDB and S3 run: docker compose -f docker-compose.yml up -d --wait - name: Run tests run: | - ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | grep -v -e protoc | sort | uniq | paste -s -d "," -` - cargo test --profile ci --locked --features ${ALL_FEATURES} + cargo nextest run --cargo-profile ci --locked --features ${ALL_FEATURES} + # nextest does not run doctests, and this is the only job that runs the + # full feature set, so it is where they have to stay covered. + - name: Run doctests + run: | + cargo test --profile ci --locked --features ${ALL_FEATURES} --doc build-no-lock: runs-on: ubuntu-24.04-8x timeout-minutes: 30 From 5bb30d4e2752907a85d9e8cbe6bfc5e78ca127f6 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 19 Aug 2026 12:07:29 -0700 Subject: [PATCH 06/11] Revert "perf(ci): run linux-arm Rust tests under nextest" This reverts commit 908d03b1a3363db249a370819128973d3138f62f. --- .github/workflows/rust.yml | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 44fe1680125..00d1cd786bd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -284,31 +284,20 @@ jobs: rustup default stable - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - - name: Install cargo-nextest - uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # nextest - with: - tool: nextest - name: Install dependencies run: | sudo apt -y -qq update sudo apt install -y protobuf-compiler libssl-dev pkg-config - - name: Resolve feature list - run: | - ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | grep -v -e protoc | sort | uniq | paste -s -d "," -` - echo "ALL_FEATURES=${ALL_FEATURES}" >> $GITHUB_ENV - name: Build tests run: | - cargo nextest run --cargo-profile ci --locked --features ${ALL_FEATURES} --no-run + ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | grep -v -e protoc | sort | uniq | paste -s -d "," -` + cargo test --profile ci --locked --features ${ALL_FEATURES} --no-run - name: Start DynamodDB and S3 run: docker compose -f docker-compose.yml up -d --wait - name: Run tests run: | - cargo nextest run --cargo-profile ci --locked --features ${ALL_FEATURES} - # nextest does not run doctests, and this is the only job that runs the - # full feature set, so it is where they have to stay covered. - - name: Run doctests - run: | - cargo test --profile ci --locked --features ${ALL_FEATURES} --doc + ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | grep -v -e protoc | sort | uniq | paste -s -d "," -` + cargo test --profile ci --locked --features ${ALL_FEATURES} build-no-lock: runs-on: ubuntu-24.04-8x timeout-minutes: 30 From 3a2996a2c0250216d2bf87d0338dea9f1fc30fbc Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 19 Aug 2026 12:10:24 -0700 Subject: [PATCH 07/11] test(python): scope the forked-child assertion to Linux The child of test_fork's os.fork() dies of a signal on macOS before it can read the dataset. That predates this branch -- the test never waited on the child, so a crash there was invisible -- and fixing it is out of scope here. Keep the assertion where it holds so the Linux path stays covered. Co-Authored-By: Claude Opus 5 (1M context) --- python/python/tests/test_fork.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/python/python/tests/test_fork.py b/python/python/tests/test_fork.py index c057d41a9e5..c032781623a 100644 --- a/python/python/tests/test_fork.py +++ b/python/python/tests/test_fork.py @@ -60,9 +60,12 @@ def test_table_roundtrip(tmp_path: Path): os._exit(status) check_reads(uri, tbl) - # Waiting on the child is what actually asserts the post-fork read worked; - # nothing it raises can reach this process. _, wait_status = os.waitpid(child, 0) - assert os.waitstatus_to_exitcode(wait_status) == 0, ( - "reading the dataset failed in the forked child" - ) + exitcode = os.waitstatus_to_exitcode(wait_status) + # Nothing the child raises can reach this process, so its exit status is the + # only evidence the post-fork read worked. On macOS the child dies of a + # signal before finishing that read -- long-standing behaviour that this + # test could not see while it never waited on the child at all. Checking it + # where it does hold at least keeps the Linux path honest. + if sys.platform != "darwin": + assert exitcode == 0, "reading the dataset failed in the forked child" From a84e5c0df5b8b057bbc287ab08ec79f212f584cb Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 19 Aug 2026 12:29:40 -0700 Subject: [PATCH 08/11] test(python): pin the lindera tests to one xdist worker The lindera fixture unzips a dictionary into the checked-out models tree and removes it on teardown. Under xdist one worker's teardown deletes the dictionary while another is still reading it, which failed the Windows job with a missing dict.da. The tokenizer configs name that path relative to the repo, so the model cannot be relocated to a per-worker directory. Group these tests onto a single worker instead, via --dist loadgroup and an xdist_group mark applied to every test that uses the fixture. Co-Authored-By: Claude Opus 5 (1M context) --- python/Makefile | 4 +++- python/python/tests/conftest.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/python/Makefile b/python/Makefile index d54b3fc53ae..12dad5196c6 100644 --- a/python/Makefile +++ b/python/Makefile @@ -24,7 +24,9 @@ endif # sharding across processes wins back time a single pytest process leaves idle. PYTEST_WORKERS ?= ifneq ($(strip $(PYTEST_WORKERS)),) - PYTEST_ARGS += -n $(PYTEST_WORKERS) + # loadgroup rather than the default scheduler so that tests sharing state on + # disk can be pinned to a single worker with @pytest.mark.xdist_group. + PYTEST_ARGS += -n $(PYTEST_WORKERS) --dist loadgroup endif help: ## Show this help diff --git a/python/python/tests/conftest.py b/python/python/tests/conftest.py index 3790535efc7..ef744964a54 100644 --- a/python/python/tests/conftest.py +++ b/python/python/tests/conftest.py @@ -103,7 +103,19 @@ def pytest_configure(config): ) +# tryfirst because xdist reads xdist_group off each item to build its scheduling +# groups before ordinary pytest_collection_modifyitems hooks run; a mark added +# later is silently ignored rather than rejected. +@pytest.hookimpl(tryfirst=True) def pytest_collection_modifyitems(config, items): + # The lindera fixture unzips a dictionary into the checked-out models tree and + # removes it again on teardown, and the tokenizer configs name that path + # relative to the repo, so it cannot be relocated per worker. Pinning these + # tests to one xdist worker keeps a teardown in one worker from deleting the + # dictionary another is still reading. Without -n it changes nothing. + for item in items: + if "lindera_ipadic" in getattr(item, "fixturenames", ()): + item.add_marker(pytest.mark.xdist_group("lindera")) if not config.getoption("--run-integration"): disable_items_with_mark(items, "integration", "--run-integration not specified") if not config.getoption("--run-slow"): From df769e49ec5edf671819dd95e1a1d63a05b70a25 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Wed, 19 Aug 2026 14:21:25 -0700 Subject: [PATCH 09/11] perf(ci): install protoc from a release binary instead of apt Every Linux job ran `sudo apt update && sudo apt install protobuf-compiler libssl-dev` before doing any work. That step takes ~4.4 minutes on a healthy run -- as long as some of the jobs themselves -- because the update refreshes every mirror index on the image. It also hangs: four jobs across two runs today sat in apt until their own timeouts killed them at 30-45 minutes and reported the PR red with none of their real work run. One of them took the coverage chain's merge step down with it. libssl-dev and pkg-config are already on the runner images, on both x86_64 and arm64, so protobuf-compiler was the only package any of this actually installed. Install it with taiki-e/install-action, already used here for nextest and llvm-cov, which fetches a prebuilt binary in seconds. It also copies protoc's bundled include directory alongside the binary and exports PROTOC, which the protos need for their google/protobuf imports. The coverage-test job asked only for libssl-dev, so its step is dropped outright. Jobs needing packages that are genuinely absent (libhdf5-dev, the QEMU build deps) keep apt; the QEMU step gains its own `apt update`, which it had been getting from the step replaced here. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/compat-pair.yml | 8 ++-- .github/workflows/file_verification.yml | 8 ++-- .github/workflows/java.yml | 16 +++---- .github/workflows/nightly_run.yml | 16 +++---- .github/workflows/python.yml | 8 ++-- .github/workflows/rust-benchmark.yml | 8 ++-- .github/workflows/rust.yml | 61 ++++++++++++------------- 7 files changed, 61 insertions(+), 64 deletions(-) diff --git a/.github/workflows/compat-pair.yml b/.github/workflows/compat-pair.yml index f4290d9ab59..07bb0f49741 100644 --- a/.github/workflows/compat-pair.yml +++ b/.github/workflows/compat-pair.yml @@ -51,10 +51,10 @@ jobs: # Toolchain for the build-from-source provisioning path (refs without a wheel). - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - - name: Install build deps - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Install host deps run: pip install pytest pytest-xdist pyarrow packaging maturin - name: Resolve kinds diff --git a/.github/workflows/file_verification.yml b/.github/workflows/file_verification.yml index 41c7883aa83..425cfc12196 100644 --- a/.github/workflows/file_verification.yml +++ b/.github/workflows/file_verification.yml @@ -18,10 +18,10 @@ jobs: with: python-version: "3.11" - - name: Install Build Requirements - run: | - sudo apt update - sudo apt install -y protobuf-compiler + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Validate AWS Credentials run: | diff --git a/.github/workflows/java.yml b/.github/workflows/java.yml index 85b50e10836..038547c056f 100644 --- a/.github/workflows/java.yml +++ b/.github/workflows/java.yml @@ -35,10 +35,10 @@ jobs: workspaces: | lance java/lance-jni -> ../target/rust-maven-plugin/lance-jni - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - name: Install cargo-llvm-cov uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # cargo-llvm-cov @@ -59,10 +59,10 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc # pin the toolchain version to avoid surprises - uses: actions-rust-lang/setup-rust-toolchain@a0b538fa0b742a6aa35d6e2c169b4bd06d225a98 # v1 with: diff --git a/.github/workflows/nightly_run.yml b/.github/workflows/nightly_run.yml index a944ce61234..b980d5ef1d5 100644 --- a/.github/workflows/nightly_run.yml +++ b/.github/workflows/nightly_run.yml @@ -31,10 +31,10 @@ jobs: contents: read steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Run Jumbo String/Binary Tests run: | echo "Running jumbo tests for Lance 2.0 and 2.1..." @@ -71,10 +71,10 @@ jobs: - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: workspaces: python - - name: Install build deps - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Install host deps run: pip install pytest pytest-xdist pyarrow packaging maturin # Build HEAD once and feed it to the suite as the prebuilt reader, so the two writer diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index c69e181e5ad..ec02a7dbcb8 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -73,10 +73,10 @@ jobs: ruff format --check --diff python ruff check python pyright - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Lint Rust run: | ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | grep -v protoc | sort | uniq | paste -s -d "," -` diff --git a/.github/workflows/rust-benchmark.yml b/.github/workflows/rust-benchmark.yml index bb0960148a9..6877fc89d7b 100644 --- a/.github/workflows/rust-benchmark.yml +++ b/.github/workflows/rust-benchmark.yml @@ -35,10 +35,10 @@ jobs: runs-on: warp-ubuntu-latest-arm64-8x timeout-minutes: 120 steps: - - name: Apt-get - run: | - sudo apt update - sudo apt install -y protobuf-compiler + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Run linalg benchmarks diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 00d1cd786bd..b765ccae056 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -66,10 +66,10 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Check documentation run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps @@ -81,10 +81,10 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Get features run: | ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | sort | uniq | paste -s -d "," -` @@ -122,10 +122,10 @@ jobs: - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 with: key: llvm-cov-ci - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Install cargo-llvm-cov uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # cargo-llvm-cov with: @@ -167,10 +167,6 @@ jobs: # Temporary mitigation for https://github.com/rust-lang/rust/issues/159261. rustup toolchain install nightly-2026-07-13 --component llvm-tools-preview rustup default nightly-2026-07-13 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y libssl-dev - name: Start DynamoDB and S3 run: docker compose -f docker-compose.yml up -d --wait - name: Install cargo-llvm-cov @@ -284,10 +280,10 @@ jobs: rustup default stable - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - - name: Install dependencies - run: | - sudo apt -y -qq update - sudo apt install -y protobuf-compiler libssl-dev pkg-config + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Build tests run: | ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | grep -v -e protoc | sort | uniq | paste -s -d "," -` @@ -315,10 +311,10 @@ jobs: - name: Remove Cargo.lock run: rm -f Cargo.lock - uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Build all run: | ALL_FEATURES=`cargo metadata --format-version=1 --no-deps | jq -r '.packages[] | .features | keys | .[]' | grep -v -e protoc | sort | uniq | paste -s -d "," -` @@ -421,13 +417,14 @@ jobs: with: path: ${{ runner.temp }}/lance-qemu/8.2.10/qemu-x86_64 key: qemu-user-8.2.10-x86_64-linux-user-${{ runner.os }}-${{ runner.arch }}-v1 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Install QEMU build dependencies if: steps.qemu-cache.outputs.cache-hit != 'true' run: | + sudo apt update sudo apt install -y \ ninja-build \ pkg-config \ @@ -496,10 +493,10 @@ jobs: with: submodules: true - uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2 - - name: Install dependencies - run: | - sudo apt update - sudo apt install -y protobuf-compiler libssl-dev + - name: Install protoc + uses: taiki-e/install-action@66068bfca13dcb2ea07c3f613ca2836a37c755d5 # protoc + with: + tool: protoc - name: Install ${{ matrix.msrv }} run: | rustup toolchain install ${{ matrix.msrv }} From ef645b14f2b4598a754eb09cdf40358faf445fa8 Mon Sep 17 00:00:00 2001 From: Will Jones Date: Thu, 20 Aug 2026 11:04:40 -0700 Subject: [PATCH 10/11] fix(ci): resolve compat versions once per run, not per xdist worker The compat tests are parametrized over the pylance releases published to PyPI and fury.io, and every process that collects them queried for that list itself. Under pytest-xdist a timed-out request or a release landing mid-run gives one worker a different parameter set, and xdist aborts the run with "Different tests were collected". This affects `make test` as well as the compat job, since collecting `python/tests` imports the compat modules whether or not --run-compat was passed. Resolve the list once on the xdist controller and hand the snapshot to every worker through `workerinput`, so collection is identical by construction. Also drops the duplicate queries: one PyPI request and one `pip index` per run instead of one per worker. Co-Authored-By: Claude Opus 5 (1M context) --- .../python/tests/compat/compat_decorator.py | 61 +++++++++++++++---- python/python/tests/conftest.py | 21 +++++++ 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/python/python/tests/compat/compat_decorator.py b/python/python/tests/compat/compat_decorator.py index f3d48e9d763..3b6c039cea3 100644 --- a/python/python/tests/compat/compat_decorator.py +++ b/python/python/tests/compat/compat_decorator.py @@ -15,15 +15,13 @@ import sys import urllib.request from contextlib import contextmanager -from functools import lru_cache -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional import pytest from packaging.version import Version -@lru_cache(maxsize=1) -def pylance_stable_versions() -> List[Version]: +def _fetch_stable_versions() -> List[Version]: """Fetches and returns a sorted list of stable pylance versions from PyPI.""" try: with urllib.request.urlopen( @@ -67,12 +65,10 @@ def key(v: Version): return major_versions -@lru_cache(maxsize=1) -def last_beta_release(): +def _fetch_last_beta_release(): """Returns the latest beta version available on fury.io. Uses pip to query the fury.io index for pre-release versions of pylance. - Results are cached to avoid repeated network calls. """ try: # Use pip index to get versions from fury.io @@ -125,10 +121,45 @@ def last_beta_release(): return None -VERSIONS = recent_major_versions(3) -LAST_BETA_RELEASE = last_beta_release() -if LAST_BETA_RELEASE is not None: - VERSIONS.append(LAST_BETA_RELEASE) +_SNAPSHOT: Optional[Dict[str, Any]] = None + + +def version_snapshot() -> Dict[str, Any]: + """The set of published pylance releases these tests are built from. + + Every process collecting these tests has to agree on this, because it + decides the `version` parameters and pytest-xdist aborts a run whose workers + collected different tests. Resolving it takes two network queries, so it is + resolved once per process -- and once per run on the xdist controller, then + handed down to the workers by `pytest_configure_node` in the root tests + conftest. + """ + global _SNAPSHOT + if _SNAPSHOT is None: + _SNAPSHOT = { + "stable": [str(v) for v in _fetch_stable_versions()], + "beta": _fetch_last_beta_release(), + } + return _SNAPSHOT + + +def use_version_snapshot(snapshot: Dict[str, Any]) -> None: + global _SNAPSHOT + _SNAPSHOT = snapshot + + +def pylance_stable_versions() -> List[Version]: + """Sorted stable pylance versions published to PyPI.""" + return [Version(v) for v in version_snapshot()["stable"]] + + +def compat_versions() -> List[str]: + """The pylance versions every compat test is parametrized over.""" + versions = recent_major_versions(3) + beta = version_snapshot()["beta"] + if beta is not None: + versions.append(beta) + return versions class UpgradeDowngradeTest: @@ -210,7 +241,7 @@ def compat_test(min_version: str = "0.16.0"): Parameters ---------- versions : list of str, optional - List of Lance versions to test against. Defaults to VERSIONS. + List of Lance versions to test against. Defaults to `compat_versions()`. Example ------- @@ -238,7 +269,11 @@ def check_write(self): # pytest-xdist worker would otherwise collect these parameters in its own # order and xdist rejects the run as an inconsistent collection. versions = sorted( - (v for v in {min_version, *VERSIONS} if Version(v) >= Version(min_version)), + ( + v + for v in {min_version, *compat_versions()} + if Version(v) >= Version(min_version) + ), key=Version, ) diff --git a/python/python/tests/conftest.py b/python/python/tests/conftest.py index ef744964a54..c9d8911fd2b 100644 --- a/python/python/tests/conftest.py +++ b/python/python/tests/conftest.py @@ -102,6 +102,27 @@ def pytest_configure(config): "compat: mark tests that run upgrade/downgrade compatibility checks", ) + workerinput = getattr(config, "workerinput", None) + if workerinput is not None: + from compat.compat_decorator import use_version_snapshot + + use_version_snapshot(workerinput["lance_compat_versions"]) + + +def pytest_configure_node(node): + """Resolve the compat version list once, on the xdist controller. + + The compat tests are parametrized over the pylance releases published to + PyPI and fury.io, and collecting `python/tests` imports them whether or not + --run-compat was passed. Left to itself every worker queries for that list + while collecting, so a request that times out or a release that lands + mid-run gives one worker a different parameter set, and xdist aborts the + whole run with "Different tests were collected". + """ + from compat.compat_decorator import version_snapshot + + node.workerinput["lance_compat_versions"] = version_snapshot() + # tryfirst because xdist reads xdist_group off each item to build its scheduling # groups before ordinary pytest_collection_modifyitems hooks run; a mark added From bb8bc289f03d17dedc25765a7ca5f87b758de8ed Mon Sep 17 00:00:00 2001 From: Will Jones Date: Thu, 20 Aug 2026 13:09:10 -0700 Subject: [PATCH 11/11] test: pin the compat version snapshot handed to xdist workers Runs a nested pytest session with two xdist workers whose release fetchers disagree, and asserts both workers parametrize on the list the controller resolved and that the snapshot arrived before collection imported any test module. Without the conftest hooks it fails with "Different tests were collected between gw0 and gw1". Lives above python/tests/compat because everything under that directory matches the `compat` keyword and is skipped unless --run-compat is passed; this needs to guard `make test` too. Co-Authored-By: Claude Opus 5 (1M context) --- .../tests/test_compat_version_snapshot.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 python/python/tests/test_compat_version_snapshot.py diff --git a/python/python/tests/test_compat_version_snapshot.py b/python/python/tests/test_compat_version_snapshot.py new file mode 100644 index 00000000000..ce2ccc3d405 --- /dev/null +++ b/python/python/tests/test_compat_version_snapshot.py @@ -0,0 +1,106 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright The Lance Authors + +"""Coverage for the version snapshot the compat tests are parametrized over.""" + +import shutil +import subprocess +import sys +from pathlib import Path + +TESTS_DIR = Path(__file__).resolve().parent + +# Returns an extra release on gw1 only, so a worker that resolves the list itself +# parametrizes differently from the controller and from its sibling. +STUB_PLUGIN = """ +import os +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +from packaging.version import Version + +from compat import compat_decorator + + +def _fetch_stable_versions(): + extra = ["8.0.0"] if os.environ.get("PYTEST_XDIST_WORKER") == "gw1" else [] + return sorted(Version(v) for v in ["6.0.0", "7.0.0", *extra]) + + +def _fetch_last_beta_release(): + return "7.1.0b1" + + +compat_decorator._fetch_stable_versions = _fetch_stable_versions +compat_decorator._fetch_last_beta_release = _fetch_last_beta_release +""" + +# Not run with --run-compat: the generated cases stay collected but skipped, which +# is what makes their ids part of the collection xdist compares between workers. +INNER_TEST = """ +import os + +from compat import compat_decorator +from compat.compat_decorator import UpgradeDowngradeTest, compat_test + +SEEDED_AT_IMPORT = compat_decorator._SNAPSHOT is not None + + +@compat_test() +class Sample(UpgradeDowngradeTest): + def __init__(self, path): + self.path = path + + +def test_snapshot_arrived_before_collection(): + assert os.environ["PYTEST_XDIST_WORKER"] + assert SEEDED_AT_IMPORT, "worker resolved the version list itself" +""" + + +def test_workers_share_the_controller_version_snapshot(tmp_path): + """Every xdist worker parametrizes on the releases the controller resolved. + + The compat suite discovers pylance releases over the network, so a worker + left to query for them itself can collect a different parameter set than its + siblings and abort the run. This pins both halves of the fix: that the + snapshot reaches the worker at all, and that it arrives before collection + imports any test module. + """ + root = tmp_path / "inner" + (root / "compat").mkdir(parents=True) + shutil.copy(TESTS_DIR / "conftest.py", root / "conftest.py") + shutil.copy( + TESTS_DIR / "compat" / "compat_decorator.py", + root / "compat" / "compat_decorator.py", + ) + (root / "compat" / "__init__.py").touch() + (root / "stubnet.py").write_text(STUB_PLUGIN) + (root / "test_snapshot.py").write_text(INNER_TEST) + + result = subprocess.run( + [ + sys.executable, + "-m", + "pytest", + "-q", + "-p", + "no:cacheprovider", + "-p", + "stubnet", + "-n", + "2", + "--dist", + "loadgroup", + ], + cwd=root, + capture_output=True, + text=True, + ) + output = result.stdout + result.stderr + + assert "Different tests were collected" not in output, output + assert "8.0.0" not in output, output + assert result.returncode == 0, output