Skip to content

test(gfql): make the polars lane see the 280 tests it was skipping everywhere - #1805

Merged
lmeyerov merged 2 commits into
masterfrom
fix/gfql-polars-lane-invisible-tests
Jul 28, 2026
Merged

test(gfql): make the polars lane see the 280 tests it was skipping everywhere#1805
lmeyerov merged 2 commits into
masterfrom
fix/gfql-polars-lane-invisible-tests

Conversation

@lmeyerov

@lmeyerov lmeyerov commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

The finding

A full local suite on py3.11 + polars reported 3 failures that reproduce identically on
origin/master (233b64c8). "Pre-existing, not mine" was the tempting read. It is the wrong
one: all three fail because CI cannot see them, and one of them is a real product defect.

polars is installed in exactly one CI lane — test-polars, driven by bin/test-polars.sh,
which runs a hand-maintained file list. Every other lane collection-skips a module-level
pytest.importorskip("polars") and every polars / polars-gpu parameter. So a polars-gated
test the list omits runs nowhere, and both lanes report green about it.

Measured against every job log of run
30311675717 (PR #1799):
280 tests executed locally with polars installed and appear in ZERO CI job logs.

The three failures, and the cause of each

test assertion cause
index/test_indexed_bindings.py::test_explicit_polars_gpu_declines_indexed_helper_and_falls_back ImportError: GFQL engine='polars-gpu' requires the RAPIDS cudf_polars stack (chain.py:1005) the test is wrong — it executes engine='polars-gpu' but gates only on importorskip("polars")
test_viz_pipeline_conformance.py::test_case_regex_unicode_trick_matrix[toupper-eq-ss-fold] toupper-eq-ss-fold: hand pin disagrees with the pandas mirror; assert [9] == [8, 9] master is wrong — pandas/polars silently disagree under pandas>=3 (#1802)
…::test_case_regex_unicode_trick_matrix[tolower-turkish-dotted-i] tolower-turkish-dotted-i: hand pin disagrees with the pandas mirror; assert [] == [7] same

#1 — evidence. grep over all job logs: the test appears 9 times, every one SKIPPED, zero
executions.
It is collection-visible in the pandas lanes (test-gfql-core collects
graphistry/tests/compute/gfql) but skips there for want of polars, and its file is not in
bin/test-polars.sh, so the one lane that has polars never sees it. It has never run.

#2/#3 — evidence. These are not environment noise. pandas>=3 makes the Arrow-backed str
dtype the default, and Arrow's utf8_upper/utf8_lower are simple per-codepoint mappings,
where Python's (and pandas<3's object dtype) are full:

pd.__version__ == '3.0.5'
pd.Series(["straße", "istanbul İ"]).str.upper()  ->  ['STRAẞE', 'ISTANBUL İ']
pd.Series(["straße", "istanbul İ"]).str.lower()  ->  ['straße',  'istanbul i']

row/pipeline.py:1521 implements Cypher toLower/toUpper with that accessor, so:

MATCH (n) WHERE toUpper(n.name) = 'STRASSE' RETURN n.id AS id
  engine='pandas'  ->  [9]
  engine='polars'  ->  [8, 9]      # Rust full case mapping — and the Cypher-correct answer

A silent cross-engine divergence: no decline, no NIE, just different rows — precisely what
these conformance suites exist to forbid. Filed as #1802 with the repro and a sizing of the
fix. CI's own test-py3.12.lock pins pandas==3.0.3, so this is not an exotic local
environment; it is the environment CI runs. The test that catches it simply never executes.

Sibling inventory — the cause is structural

280 tests, in 10 files, ran in no CI job of that run:

file invisible tests why
test_engine_polars_narrow_combine.py 120 module-level gate, absent from lane
index/test_indexed_bindings.py 40 polars/polars-gpu params, absent from lane
test_engine_polars_semi_key_dedup.py 36 module-level gate, absent from lane
test_viz_pipeline_conformance.py 31 module-level gate, absent from lane
test_engine_coercion.py 13 polars params, absent from lane
test_engine_polars_call_modality.py 12 module-level gate, absent from lane
test_rows_table_named_middle.py 10 module-level gate, absent from lane
test_reentry_caller_graph_immutability.py 9 polars params, absent from lane
test_rewrite_param_discard.py 7 polars params, absent from lane
cypher/test_lowering.py 2 in the lane's -k polars phase, but the filter cannot select them

Six whole modules were invisible, four of them named test_engine_polars_*. Also confirmed
invisible for a different reason and left alone here: test_engine_polars_gpu.py and
index/test_index_gpu_edge_match.py are GPU-gated — a separate lane gap.

What this changes

  • bin/test-polars.sh — adds the six invisible modules and the four whose polars parameters
    were invisible. 277 of the 280 now execute in the lane; the other 3 are the two renamed
    test_lowering.py tests (now selected by -k polars) and the polars-gpu test (now skipping
    honestly, its CPU half split out and running).
  • test_polars_lane_completeness.py (new) — the durable part. It parses POLARS_TEST_FILES
    and fails when: a polars-mentioning test module is neither in the lane nor in a documented
    exemption; a module-level polars gate sits outside the lane; a listed path no longer exists; a
    polars-gated test in the -k polars phase is unselectable by that filter; or an exemption goes
    stale. Exemptions carry written reasons and are length-checked, so "" cannot re-open the hole.
    Mutation-checked: removing one entry from the array turns two of its assertions red.
  • The polars-gpu test is split — the CPU half (try_indexed_connected_bindings_state must
    decline for Engine.POLARS_GPU) now runs wherever polars does; the execution half is
    skipif-gated on cudf_polars, mirroring chain.py's own check.
  • The two unicode rows are xfail(strict=True), keyed on a probe of the pandas accessor's own
    behaviour — so they must still PASS on pandas<3, and the GFQL: pandas vs polars silently disagree on toUpper/toLower under pandas>=3 (simple vs full Unicode case mapping) #1802 fix turns them into an XPASS
    that forces the marks to be retired. The hand pins stay at the Cypher-correct values.
  • Renamed the two test_lowering.py polars tests that -k polars could not select.

Test-only. No product code touched, hence no CHANGELOG entry.

Verification — CI green, 77/77

  • test-polars green on all six python versions, and the behaviour is version-discriminating
    rather than suppressed:
    • py3.9 / py3.10 (pandas 2.3.3): the two unicode rows PASS — full case mapping, hand
      pins correct, xfail condition correctly not applied.
    • py3.12 / py3.13 / py3.14 (pandas 3.0.3): the same two rows XFAIL on GFQL: pandas vs polars silently disagree on toUpper/toLower under pandas>=3 (simple vs full Unicode case mapping) #1802.
    • test_explicit_polars_gpu_declines_indexed_helper PASSED; the execution half
      SKIPPED for want of cudf_polars — instead of the ImportError it used to raise wherever it
      was actually run.
    • py3.12 lane: 2726 passed, 142 skipped, 3 xfailed (was 2355 passed, 13 skipped), and the
      -k polars phase 51 passed (was 49). The polars per-file coverage audit passes unchanged.
  • Locally (py3.11 + polars 1.43.1 + pandas 3.0.5): identical outcome; the completeness guard was
    mutation-tested (dropping one array entry turns two of its assertions red), not just
    observed green.
  • ruff check clean; mypy reports no error in the new file.

Ordering dependency — #1797's workflow patch should land first

It passed, but by three seconds. Measured job durations for test-polars (3.12), the cell
that runs the suite under coverage:

duration budget
master (233b64c8) 8m21s timeout-minutes: 10
this PR 9m57s timeout-minutes: 10

Master was already at 84% of that budget before this change. #1797 records that adding a single
file previously tipped this cell into a timeout — which GitHub reports as cancelled, with zero
test failures in the log. The pending ci.yml split in the comment on #1794 (a dedicated
test-polars-coverage job at timeout-minutes: 20) should land before this, or the next
polars test — or a slightly slower runner — cancels the cell rather than failing it.

This PR changes no workflow file; that patch is untouched here.

🤖 Generated with Claude Code

https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB

lmeyerov and others added 2 commits July 27, 2026 17:56
…erywhere

polars is installed in exactly ONE CI lane (test-polars, driven by bin/test-polars.sh),
and that lane runs a hand-maintained file list. Every other lane collection-skips a
module-level `pytest.importorskip("polars")` and every polars/polars-gpu parameter. A
polars-gated test the list omits therefore runs NOWHERE while both lanes report green.

Measured on run 30311675717 (PR #1799): 280 tests executed locally with polars installed
and appear in ZERO job logs of that run, including six whole modules — four of them named
`test_engine_polars_*`. Two real defects were sitting in the blind spot:

  - `test_explicit_polars_gpu_declines_indexed_helper_and_falls_back` executes
    engine='polars-gpu' but only gates on `importorskip("polars")`, so it raises
    ImportError wherever polars is present without the RAPIDS cudf_polars stack. It has
    never once run: 9 SKIPPED, 0 executed, across the whole run.
  - `test_case_regex_unicode_trick_matrix[toupper-eq-ss-fold]` and
    `[tolower-turkish-dotted-i]` catch a genuine silent cross-engine divergence: under
    pandas>=3 the pandas engine answers [9] where polars answers [8, 9] for
    `toUpper(n.name) = 'STRASSE'`. Filed as #1802 with the repro; both rows are marked
    xfail(strict=True) keyed on the pandas accessor's own behaviour, so the fix XPASSes
    and retires the marks.

Changes:
  - bin/test-polars.sh gains the six invisible modules plus the four whose polars
    parameters were invisible.
  - New `test_polars_lane_completeness.py` parses POLARS_TEST_FILES and fails when a
    polars-mentioning test module is neither in the lane nor in a documented exemption,
    when a module-level polars gate sits outside the lane, when a listed path is gone, or
    when a polars-gated test in the `-k polars` phase is unselectable by that filter.
    Mutation-checked: dropping one entry turns two of its assertions red.
  - Split the polars-gpu test so the CPU half (helper must decline) runs wherever polars
    does, and the execution half skips honestly without cudf_polars.
  - Renamed the two test_lowering.py polars tests `-k polars` could not select.

Test-only; no product code touched, so no CHANGELOG entry.

Widened lane, py3.11 + polars 1.43.1 + pandas 3.0.5: 2726 passed, 142 skipped, 3 xfailed,
0 failed (was 1636 passed on the same box). ORDERING NOTE: #1797 records that the py3.12
coverage cell is already at its 10-minute ceiling and that adding a single file tipped it
into a timeout; the pending ci.yml split (comment on PR #1794) must land BEFORE this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
Replace the getattr/ast.AST pairing in the guard's two helpers with the concrete node
types they are actually called with: _node_source takes ast.stmt | ast.expr and reads
.lineno/.end_lineno directly (end_lineno is Optional[int], so the None case is handled
rather than defaulted through getattr), and _selected_by_k_polars takes a FuncDef alias
so .name and .decorator_list are checked attributes instead of getattr lookups with
invented fallbacks. No behaviour change; guard still 8 passed, ruff clean, mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
lmeyerov added a commit that referenced this pull request Jul 28, 2026
…olars

`_engines()` appended `"polars-gpu"` when **`cudf`** imported. But `polars-gpu`
is the `cudf_polars` GPU collect target and raises

    ImportError: GFQL engine='polars-gpu' requires the RAPIDS cudf_polars stack

without it. So any box with cuDF installed and `cudf_polars` absent — a common
developer configuration — got 20 failures indistinguishable from product
breakage, in a file that IS listed in `bin/test-polars.sh`. CI never caught it
because that lane installs neither package, so the parameter did not exist
there: a gate can only fabricate failures where it is never exercised.

Measured on a box with cuDF 25.10 and no `cudf_polars`:
  before  20 failed, 116 passed
  after    0 failed, 105 passed

SCOPE NOTE. This branch originally carried the whole CA4 allowlist change
(seven files added to `bin/test-polars.sh`). PR #1805
(`fix/gfql-polars-lane-invisible-tests`), opened concurrently, is a SUPERSET of
that list AND adds `test_polars_lane_completeness.py`, a guard that fails when a
polars-parametrized file is missing from the lane — which makes the fix
permanent rather than one-time. It also lands the same pandas-3 case-mapping
xfail, against the same issue. Reduced to the one hunk #1805 does not contain,
so the owner reviews the allowlist once instead of twice.

RUNTIME DELTA: zero. One test file's engine list and a CHANGELOG line; no
pyg-bench lane run required (CB5), checkable from the diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
lmeyerov added a commit that referenced this pull request Jul 28, 2026
The first commit quoted a local 4-CPU-pinned A/B (326s -> 109s, 2.98x). CI has now run
this branch and realizes less: the `test-polars (3.12)` cell goes 501s -> 322s and its
script step 484s -> 297s (1.63x), with the five non-coverage cells at 222-318s -> 115-172s.

Coverage tracing is per-worker CPU cost that does not parallelize away, and the ~8s serial
second phase plus 4x interpreter startup are fixed, so the coverage cell gains less than
the plain cells (which see ~2x). The CI figure is the one that decides whether #1805 fits,
so it is the one the changelog quotes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
@lmeyerov

Copy link
Copy Markdown
Contributor Author

The ci.yml job split is no longer a prerequisite for this PR — #1814 removes the timeout risk without touching a workflow file.

Context: this PR's test-polars (3.12) cell measured 9m57s against a 10-minute timeout — green by 3 seconds, which is why it was queued behind a ci.yml split that only someone with workflow scope can push.

#1814 parallelizes bin/test-polars.sh under pytest-xdist instead (no workflow edit, no dependency change). Measured on CI: the 3.12 cell goes 501s -> 322s, script step 484s -> 297s (1.63x). Applying that to this PR's cell gives ~373s (~6m13s), i.e. ~3.8 minutes of margin instead of 3 seconds.

Verified there before relying on it: identical pass/skip node-id sets serial vs parallel (2417 ids, 2404 passed / 13 skipped), coverage a strict superset with zero lines lost, the --cov-append phase still appending into the xdist-produced data file, and a byte-identical gfql-polars coverage audit — so changed-line-coverage sees no change.

Diffs are mechanically separate and rebase cleanly either way. #1814 touches only the block after the POLARS_TEST_FILES array's closing paren (plus CHANGELOG); this PR touches only inside the array and the comment above it. #1814 also confirms it does not break test_polars_lane_completeness.py's ^POLARS_TEST_FILES=\((.*?)^\) parser — that anchored non-greedy match ignores the new XDIST_ARGS=( ... ) array declared after it.

Suggested order: #1814, then this PR.

@lmeyerov
lmeyerov merged commit 49db91c into master Jul 28, 2026
77 checks passed
@lmeyerov
lmeyerov deleted the fix/gfql-polars-lane-invisible-tests branch July 28, 2026 02:55
lmeyerov added a commit that referenced this pull request Jul 28, 2026
The first commit quoted a local 4-CPU-pinned A/B (326s -> 109s, 2.98x). CI has now run
this branch and realizes less: the `test-polars (3.12)` cell goes 501s -> 322s and its
script step 484s -> 297s (1.63x), with the five non-coverage cells at 222-318s -> 115-172s.

Coverage tracing is per-worker CPU cost that does not parallelize away, and the ~8s serial
second phase plus 4x interpreter startup are fixed, so the coverage cell gains less than
the plain cells (which see ~2x). The CI figure is the one that decides whether #1805 fits,
so it is the one the changelog quotes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
lmeyerov added a commit that referenced this pull request Jul 28, 2026
…olars

`_engines()` appended `"polars-gpu"` when **`cudf`** imported. But `polars-gpu`
is the `cudf_polars` GPU collect target and raises

    ImportError: GFQL engine='polars-gpu' requires the RAPIDS cudf_polars stack

without it. So any box with cuDF installed and `cudf_polars` absent — a common
developer configuration — got 20 failures indistinguishable from product
breakage, in a file that IS listed in `bin/test-polars.sh`. CI never caught it
because that lane installs neither package, so the parameter did not exist
there: a gate can only fabricate failures where it is never exercised.

Measured on a box with cuDF 25.10 and no `cudf_polars`:
  before  20 failed, 116 passed
  after    0 failed, 105 passed

SCOPE NOTE. This branch originally carried the whole CA4 allowlist change
(seven files added to `bin/test-polars.sh`). PR #1805
(`fix/gfql-polars-lane-invisible-tests`), opened concurrently, is a SUPERSET of
that list AND adds `test_polars_lane_completeness.py`, a guard that fails when a
polars-parametrized file is missing from the lane — which makes the fix
permanent rather than one-time. It also lands the same pandas-3 case-mapping
xfail, against the same issue. Reduced to the one hunk #1805 does not contain,
so the owner reviews the allowlist once instead of twice.

RUNTIME DELTA: zero. One test file's engine list and a CHANGELOG line; no
pyg-bench lane run required (CB5), checkable from the diff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB
lmeyerov added a commit that referenced this pull request Jul 28, 2026
…cing its 10-minute timeout (#1814)

* ci(polars): parallelize the polars lane so the coverage cell stops racing its timeout

`test-polars (3.12)` is the only cell that runs the coverage-instrumented pass plus the
per-file coverage audit, and it had already been cancelled twice at its 10-minute budget
(615s, 611s) with zero test failures — pure budget exhaustion, the same commit passing or
failing depending on the runner it drew. Measured on the master run that produced this
base commit, the script step alone is 484s of the cell's 501s.

The lever that needs no workflow edit: pytest-xdist is already in the [test] extra, and
test-gfql-core already runs `-n auto` under `--cov`, so coverage+xdist is established here.
The main pytest phase now runs `-n auto --maxprocesses 4 --dist load`.

Measured (py3.12, polars 1.43.1, pandas 3.0.5, POLARS_COV=1), pinned to 4 CPUs to stand in
for a GitHub-hosted ubuntu-latest runner: 325.65s serial -> 109.34s parallel, 2.98x.

Verified, not assumed:
 - pass/skip NODE-ID SETS (not counts) identical serial vs parallel: 2417 node ids,
   2404 passed / 13 skipped, across 4-worker, 2-worker, --dist loadfile and 4-CPU-pinned runs
 - merged coverage is a strict superset of serial (28,480 vs 28,478 covered lines; zero lines
   lost, zero files dropped; the 2 extra are versioneer fallback lines in _version.py)
 - the second `--cov-append` invocation still appends into the xdist-produced data file
   (+1,791 lines, nothing lost) and the result is line-identical to the whole-script run
 - `bin/coverage_audit.py --profile gfql-polars` emits a byte-identical report (modulo its
   timestamp) from the parallel data, so `changed-line-coverage` sees no change

`--dist load` rather than `loadfile` because one module (test_engine_polars_chain.py) is 69%
of the lane's test time, which caps file-level distribution at ~1.4x; no test in this lane
depends on execution order or cross-test module state. POLARS_XDIST=0 restores the serial
path and POLARS_XDIST_DIST selects another distribution mode if that ever changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB

* docs(changelog): quote the MEASURED CI number, not the local one

The first commit quoted a local 4-CPU-pinned A/B (326s -> 109s, 2.98x). CI has now run
this branch and realizes less: the `test-polars (3.12)` cell goes 501s -> 322s and its
script step 484s -> 297s (1.63x), with the five non-coverage cells at 222-318s -> 115-172s.

Coverage tracing is per-worker CPU cost that does not parallelize away, and the ~8s serial
second phase plus 4x interpreter startup are fixed, so the coverage cell gains less than
the plain cells (which see ~2x). The CI figure is the one that decides whether #1805 fits,
so it is the one the changelog quotes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YsqAZQLbqjSDrYSFz2GoB

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
lmeyerov added a commit that referenced this pull request Jul 28, 2026
Resolves both conflicts and corrects two defects the conflict exposed.

bin/test-polars.sh -- take master's version unchanged. This branch adds
graphistry/tests/compute/gfql/test_rows_table_named_middle.py and
test_rewrite_param_discard.py to POLARS_TEST_FILES, but #1805 (49db91c,
this branch's own merge-base) already added both. Keeping the branch side
would list each file twice, so the polars lane would run both modules twice
-- and that lane's py3.12 cell is the coverage cell, currently 442s against
a 600s cap. Master's version also carries #1814's xdist plumbing, which the
branch side predates.

CHANGELOG.md -- the entry claimed "neither file was in bin/test-polars.sh,
so test_rows_table_named_middle.py ... ran in no CI lane at all". That is
false at this branch's base: both files are listed there at lines 43 and 50.
Rewritten to the residual that is true -- the suites were parametrized over
pandas + polars only, so cuDF and polars-gpu never exercised the table
guards; and the module-level pytest.importorskip("polars") skipped the whole
file in test-gfql-core, so its pandas cases ran only in the polars lane.
The measured claims (45 passed / 3 xfailed / 0 skipped on GB10; 24 mutation
failures, 6 per engine) are unchanged.

Also types engine_skip_reason, per review: the engine -> required-modules
map is hoisted to a module-level Mapping[str, Tuple[str, ...]] constant
rather than rebuilt inline per call, the signature is
(engine: str, smoke: Callable[[], object]) -> Optional[str], and
gpu_environment_reason's "str | None" string annotation is normalized to
Optional[str] to match (this file is imported under py3.8 lanes).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYZRXegrALuXd3NHH5evqx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant