|
38 | 38 | plumbing, but the shapes below are exercised on all four engines because the two attach/clear |
39 | 39 | pairs are duplicated across the generic chain and the native polars chain. No CI lane runs cuDF |
40 | 40 | or polars-gpu (``ci-gpu.yml`` is hard-disabled and does not install the ``polars`` extra), so |
41 | | -those two parameters report SKIPPED on CI — visibly, via a runtime probe, never as a silent |
42 | | -pass. They are exercised out of band on the dgx GPU box against |
| 41 | +those two parameters report SKIPPED on CI — visibly, with the reason quoted, never as a silent |
| 42 | +pass. The availability check CLASSIFIES rather than swallows: a missing module skips, a |
| 43 | +recognisable GPU-stack error skips with its text quoted, and any other failure propagates, |
| 44 | +because a silently skipped GPU parameter reads as evidence of passing. They are exercised out |
| 45 | +of band on the dgx GPU box against |
43 | 46 | ``graphistry/test-rapids-official:26.02-gfql-polars`` (``docker run --gpus all`` — omitting |
44 | 47 | ``--gpus all`` FABRICATES failures rather than skipping). |
45 | 48 | """ |
46 | 49 | from __future__ import annotations |
47 | 50 |
|
48 | 51 | from functools import lru_cache |
49 | | -from typing import Any, List, Tuple |
| 52 | +from typing import Any, List, Optional, Tuple |
50 | 53 |
|
51 | 54 | import pandas as pd |
52 | 55 | import pytest |
@@ -101,26 +104,61 @@ def _seed(engine: str) -> Any: |
101 | 104 | return _frame(engine, pd.DataFrame({"id": [0]})) |
102 | 105 |
|
103 | 106 |
|
104 | | -@lru_cache(maxsize=None) |
105 | | -def _engine_runnable(engine: str) -> bool: |
106 | | - """Probe by RUNNING the smallest version of what these tests do. |
| 107 | +#: Substrings that identify a broken/absent GPU STACK rather than a product defect. Matched |
| 108 | +#: against ``type(ex).__name__: str(ex)``, lowercased. Deliberately narrow — anything not |
| 109 | +#: listed is treated as a REAL failure, because guessing wrong in that direction produces a |
| 110 | +#: silently green GPU lane. (Duplicated from the copy landing in `polars_test_utils.py` |
| 111 | +#: alongside the #1788/#1790 engine parametrization; collapse to one definition once both land.) |
| 112 | +_GPU_ENVIRONMENT_MARKERS = ( |
| 113 | + "libnvrtc", "libcuda", "libcudart", "nvrtc", "cuinit", |
| 114 | + "no cuda-capable device", "cuda driver", "cuda runtime", |
| 115 | + "cuda_error", "cudaerror", "driver version", |
| 116 | + "gpu engine requested", # polars' own message when cudf_polars is absent |
| 117 | +) |
| 118 | + |
107 | 119 |
|
108 | | - Cheaper probes do not discriminate on a box with cudf/cudf_polars importable but no working |
109 | | - CUDA runtime — frame construction and simple ops all succeed there and the suite then dies |
110 | | - inside the first real kernel. So the probe is an actual boundary-call run. |
| 120 | +@lru_cache(maxsize=None) |
| 121 | +def _engine_skip_reason(engine: str) -> Optional[str]: |
| 122 | + """``None`` => this engine MUST run here; a string => a stated, checkable skip reason. |
| 123 | +
|
| 124 | + Two traps, both established by trying rather than by argument: |
| 125 | +
|
| 126 | + * An IMPORT-only check does not discriminate on a box where cudf / cudf_polars import |
| 127 | + against an incomplete CUDA runtime — construction and simple ops succeed and the suite |
| 128 | + then dies inside the first real kernel, which reads as a product failure. So a smoke |
| 129 | + QUERY runs. |
| 130 | + * A check that SWALLOWS every exception is worse. A transient `cudaErrorMemoryAllocation` |
| 131 | + in a fresh GPU container silently dropped cuDF from an otherwise-green run, and a |
| 132 | + skipped GPU parameter reads as evidence of passing. So: a missing module skips, a |
| 133 | + recognisable GPU-stack error skips WITH ITS TEXT QUOTED, and anything else propagates. |
| 134 | +
|
| 135 | + The smoke query is a plain traversal, never the shape under test — a check that ran the |
| 136 | + asserted shape would let a regression disarm its own test. |
111 | 137 | """ |
| 138 | + import importlib.util |
| 139 | + |
| 140 | + for module in {"polars": ("polars",), "polars-gpu": ("polars", "cudf_polars"), |
| 141 | + "cudf": ("cudf",)}.get(engine, ()): |
| 142 | + if importlib.util.find_spec(module) is None: |
| 143 | + return f"{module} is not installed" |
112 | 144 | try: |
113 | | - _graph(engine).gfql(list(BOUNDARY_OPS), engine=engine) |
114 | | - return True |
115 | | - except Exception: # noqa: BLE001 — any failure means "cannot run", never "test fails" |
116 | | - return False |
| 145 | + _graph(engine).gfql([n(), e_forward(), n()], engine=engine) |
| 146 | + except BaseException as ex: # noqa: BLE001 — classified below, never blanket-swallowed |
| 147 | + if isinstance(ex, ImportError): |
| 148 | + return f"{type(ex).__name__}: {ex}" |
| 149 | + text = f"{type(ex).__name__}: {ex}".lower() |
| 150 | + if any(marker in text for marker in _GPU_ENVIRONMENT_MARKERS): |
| 151 | + return f"{type(ex).__name__}: {ex}" |
| 152 | + raise |
| 153 | + return None |
117 | 154 |
|
118 | 155 |
|
119 | 156 | def _require(engine: str) -> None: |
120 | | - if not _engine_runnable(engine): |
| 157 | + reason = _engine_skip_reason(engine) |
| 158 | + if reason is not None: |
121 | 159 | pytest.skip( |
122 | | - f"engine {engine!r} is not runnable in this environment — NOT evidence that it " |
123 | | - "passes; see the COVERAGE BOUNDARY note in this module's docstring" |
| 160 | + f"engine {engine!r} unavailable here ({reason}) — NOT evidence that it passes; " |
| 161 | + "see the COVERAGE BOUNDARY note in this module's docstring" |
124 | 162 | ) |
125 | 163 |
|
126 | 164 |
|
|
0 commit comments