Skip to content

Commit 237ceaf

Browse files
lmeyerovclaude
andcommitted
test(gfql): shape registry fed by the specialization tables + route harness
Each specialization's test module registers the shape table it already owns (routes corpus, the six test_chain tables, the alias-collision matrix) with its frames and defect-class tags. The harness tries every registered shape against every chain route whose admission predicate admits it and pins that the lane serves, that the answer matches the same engine's general path on values, and that node/edge sets match the pandas general path. A lane that declines an admitted shape is recorded as an expected failure (the attenuation ledger); filed divergences are strict expected failures keyed by tag. The pandas bypass table's prune shapes surface #2053 on the polars route. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QztW7jYsDd66e8rb8pJNQA
1 parent 511a36f commit 237ceaf

7 files changed

Lines changed: 335 additions & 49 deletions

File tree

graphistry/tests/compute/gfql/routes/corpus.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from graphistry.compute.ast import ASTObject, e_forward, e_reverse, e_undirected, n
1313
from graphistry.compute.predicates.numeric import GT
14+
from graphistry.tests.compute.gfql.routes.registry import Frames, register
1415

1516

1617
class Entry(NamedTuple):
@@ -54,3 +55,6 @@ def tagged(tag: str) -> List[Entry]:
5455

5556
def by_name() -> Dict[str, Entry]:
5657
return {e.name: e for e in CORPUS}
58+
59+
60+
register("routes.corpus", [(e.name, e.ops, e.tags) for e in CORPUS], Frames(NODES, EDGES, "key", "s", "d", "eid"))
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Shape registry for the route harness.
2+
3+
Each specialization's own test module registers the shape table it was written against
4+
(``register(...)`` returns the table unchanged, so the module keeps using it), with the
5+
frames those shapes address and the defect classes they exercise. The harness in
6+
``test_route_harness.py`` then tries every registered shape against every route whose
7+
admission predicate admits it, so one input is exercised by several hot paths, not one.
8+
"""
9+
from typing import Callable, Dict, Iterable, List, NamedTuple, Optional, Sequence, Tuple, Union
10+
11+
import pandas as pd
12+
import pytest
13+
14+
from graphistry.Plottable import Plottable
15+
from graphistry.compute.ast import ASTObject
16+
17+
Build = Callable[[], List[ASTObject]]
18+
Row = Union[Tuple[str, Build], Tuple[str, Build, Tuple[str, ...]]]
19+
20+
21+
class Frames(NamedTuple):
22+
nodes: pd.DataFrame
23+
edges: pd.DataFrame
24+
node: str
25+
src: str
26+
dst: str
27+
edge: Optional[str] = None
28+
29+
30+
class Shape(NamedTuple):
31+
table: str
32+
label: str
33+
build: Build
34+
frames: Frames
35+
tags: Tuple[str, ...]
36+
37+
@property
38+
def name(self) -> str:
39+
return f"{self.table}/{self.label}"
40+
41+
42+
REGISTRY: Dict[str, Shape] = {}
43+
44+
45+
def register(table: str, rows: Sequence[Row], frames: Frames, tags: Iterable[str] = (),
46+
row_tags: Optional[Dict[str, Tuple[str, ...]]] = None) -> Sequence[Row]:
47+
"""Register ``rows`` ((label, build[, tags]) ...) under ``table``; returns ``rows``."""
48+
base = tuple(tags)
49+
for row in rows:
50+
label, build = row[0], row[1]
51+
extra = tuple(row[2]) if len(row) > 2 else ()
52+
extra += (row_tags or {}).get(label, ())
53+
shape = Shape(table, label, build, frames, base + extra)
54+
REGISTRY.setdefault(shape.name, shape)
55+
return rows
56+
57+
58+
def to_engine(df: pd.DataFrame, engine: str):
59+
if engine == "pandas":
60+
return df
61+
if engine == "cudf":
62+
return pytest.importorskip("cudf").from_pandas(df)
63+
if engine == "polars":
64+
return pytest.importorskip("polars").from_pandas(df)
65+
raise ValueError(engine)
66+
67+
68+
def graph_for(shape: Shape, engine: str, indexed: bool = False) -> Plottable:
69+
import graphistry
70+
f = shape.frames
71+
g = graphistry.nodes(to_engine(f.nodes, engine), f.node).edges(to_engine(f.edges, engine), f.src, f.dst, f.edge)
72+
return g.gfql_index_all(engine=engine) if indexed else g
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Route switch for test amplification: make named GFQL hot paths decline for a scope."""
2+
from contextlib import contextmanager
3+
from typing import Iterable, Iterator, List, Tuple
4+
5+
ROUTES = ("native-fast", "polars-seeded", "polars-plain", "index-hop", "indexed-kernel", "cypher-fast")
6+
7+
8+
def _none(*a, **k):
9+
return None
10+
11+
12+
def _targets(routes: Iterable[str]) -> List[Tuple[object, str]]:
13+
import graphistry.compute.chain as chain_mod
14+
import graphistry.compute.gfql_unified as unified
15+
import graphistry.compute.gfql.index as index_pkg
16+
import graphistry.compute.gfql.index.api as index_api
17+
import graphistry.compute.gfql.index.bindings as bindings
18+
import graphistry.compute.gfql.lazy.engine.polars.chain as pchain
19+
routes = set(routes)
20+
unknown = routes - set(ROUTES)
21+
assert not unknown, f"unknown route(s) {sorted(unknown)}; known: {ROUTES}"
22+
out: List[Tuple[object, str]] = []
23+
if "native-fast" in routes:
24+
out.append((chain_mod, "_try_chain_fast_path"))
25+
if "polars-seeded" in routes:
26+
out.append((pchain, "_try_seeded_chain_polars"))
27+
if "polars-plain" in routes:
28+
out.append((pchain, "polars_plain_single_hop_admits"))
29+
if "index-hop" in routes:
30+
out += [(index_pkg, "maybe_index_hop"), (index_api, "maybe_index_hop")]
31+
if "indexed-kernel" in routes:
32+
out.append((bindings, "_try_indexed_connected_bindings_state"))
33+
if "cypher-fast" in routes:
34+
out += [(unified, name) for name in (
35+
"_execute_seeded_node_lookup_fast_path", "_execute_seeded_typed_hop_fast_path",
36+
"_execute_single_hop_grouped_aggregate_fast_path", "_execute_two_hop_count_fast_path")]
37+
return out
38+
39+
40+
@contextmanager
41+
def routes_off(routes: Iterable[str]) -> Iterator[None]:
42+
"""Within the block the named routes decline, so the general path answers."""
43+
saved = []
44+
for mod, name in _targets(routes):
45+
saved.append((mod, name, getattr(mod, name)))
46+
setattr(mod, name, _none)
47+
try:
48+
yield
49+
finally:
50+
for mod, name, value in reversed(saved):
51+
setattr(mod, name, value)
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""Route harness: every registered shape is tried against every chain route whose admission
2+
predicate admits it. Three pins per cell: the route SERVES (its lane answers; a lane that
3+
declines an admitted shape is recorded as an expected failure, the attenuation ledger), the
4+
answer matches the same engine's general path (all routes off) on node/edge values, and its
5+
node/edge sets match the pandas general path (the cross-engine oracle). Filed divergences are
6+
strict expected failures keyed by their tag, so they flip when fixed.
7+
"""
8+
import math
9+
import os
10+
from typing import Callable, Dict, List, NamedTuple, Tuple
11+
12+
import pandas as pd
13+
import pytest
14+
15+
import graphistry.compute.chain as chain_mod
16+
import graphistry.compute.gfql.lazy.engine.polars.chain as pchain
17+
from graphistry.Engine import Engine
18+
from graphistry.compute.ast import ASTObject
19+
from graphistry.compute.chain_specializations.admission import native_fast_path_admits
20+
from graphistry.compute.gfql.lazy.engine.polars.chain_specializations.admission import (
21+
polars_plain_single_hop_admits, polars_seeded_lane_admits,
22+
)
23+
from graphistry.tests.compute.gfql.routes.registry import REGISTRY, Shape, graph_for
24+
from graphistry.tests.compute.gfql.routes.switch import ROUTES as ALL_ROUTES, routes_off
25+
26+
import graphistry.tests.compute.gfql.routes.corpus # noqa: F401 registers routes.corpus
27+
import graphistry.tests.compute.test_chain # noqa: F401 registers test_chain.*
28+
import graphistry.tests.compute.test_chain_alias_column_collision # noqa: F401 registers collision.*
29+
30+
31+
class Route(NamedTuple):
32+
name: str
33+
engines: Tuple[str, ...]
34+
admits: Callable[[List[ASTObject], str], bool]
35+
lane: Tuple[object, str]
36+
indexed: bool
37+
38+
39+
ROUTES = [
40+
Route("native-fast", ("pandas", "cudf"),
41+
lambda ops, engine: native_fast_path_admits(ops, Engine(engine), None) is not None,
42+
(chain_mod, "_try_chain_fast_path"), False),
43+
Route("polars-plain", ("polars",),
44+
lambda ops, engine: polars_plain_single_hop_admits(ops, None) is not None,
45+
(pchain, "_plain_single_hop_polars"), False),
46+
Route("polars-seeded", ("polars",),
47+
lambda ops, engine: polars_seeded_lane_admits(ops),
48+
(pchain, "_try_seeded_chain_polars"), True),
49+
]
50+
51+
KNOWN: Dict[Tuple[str, str], str] = { # (route, tag) -> issue: strict xfail until it lands
52+
("polars-plain", "#2053"): "graphistry/pygraphistry#2053",
53+
}
54+
55+
56+
class Case(NamedTuple):
57+
route: Route
58+
engine: str
59+
shape: Shape
60+
61+
@property
62+
def id(self) -> str:
63+
return f"{self.route.name}/{self.engine}/{self.shape.name}"
64+
65+
66+
def _cases() -> List[Case]:
67+
out = []
68+
for shape in REGISTRY.values():
69+
for route in ROUTES:
70+
for engine in route.engines:
71+
try:
72+
admitted = route.admits(shape.build(), engine)
73+
except Exception:
74+
admitted = False
75+
if admitted:
76+
out.append(Case(route, engine, shape))
77+
return out
78+
79+
80+
CASES = _cases()
81+
82+
83+
def _topd(df):
84+
if df is None:
85+
return None
86+
if hasattr(df, "to_pandas"):
87+
return df.to_pandas()
88+
return df
89+
90+
91+
def _canon(df) -> Tuple[Tuple[str, ...], List[Tuple]]:
92+
df = _topd(df)
93+
if df is None:
94+
return ((), [])
95+
cols = tuple(sorted(df.columns))
96+
rows = []
97+
for row in df[list(cols)].itertuples(index=False, name=None):
98+
rows.append(tuple(None if (isinstance(v, float) and math.isnan(v)) or v is pd.NA or v is pd.NaT else v for v in row))
99+
rows.sort(key=repr)
100+
return cols, rows
101+
102+
103+
def _sig(res, frames) -> Tuple[List, List]:
104+
nn, ee = _topd(res._nodes), _topd(res._edges)
105+
nodes = sorted(nn[frames.node].tolist()) if nn is not None else []
106+
edges = sorted(map(tuple, ee[[frames.src, frames.dst]].values.tolist())) if ee is not None and len(ee) else []
107+
return nodes, edges
108+
109+
110+
def _served(case: Case, monkeypatch):
111+
mod, name = case.route.lane
112+
real = getattr(mod, name)
113+
calls = {"served": 0}
114+
115+
def spy(*a, **k):
116+
out = real(*a, **k)
117+
calls["served"] += out is not None
118+
return out
119+
monkeypatch.setattr(mod, name, spy)
120+
return calls
121+
122+
123+
def _skip_unavailable(engine: str) -> None:
124+
if engine == "cudf":
125+
if os.environ.get("TEST_CUDF") != "1":
126+
pytest.skip("cuDF lane runs with TEST_CUDF=1")
127+
pytest.importorskip("cudf")
128+
if engine == "polars":
129+
pytest.importorskip("polars")
130+
131+
132+
@pytest.mark.parametrize("case", CASES, ids=[c.id for c in CASES])
133+
def test_admitted_shape_is_served_and_matches_the_general_path(case: Case, request, monkeypatch):
134+
_skip_unavailable(case.engine)
135+
for tag in case.shape.tags:
136+
if (case.route.name, tag) in KNOWN:
137+
request.applymarker(pytest.mark.xfail(strict=True, reason=KNOWN[(case.route.name, tag)]))
138+
g = graph_for(case.shape, case.engine, indexed=case.route.indexed)
139+
calls = _served(case, monkeypatch)
140+
try:
141+
served = g.gfql(case.shape.build(), engine=case.engine)
142+
except Exception as served_exc:
143+
with routes_off(ALL_ROUTES):
144+
with pytest.raises(type(served_exc)):
145+
g.gfql(case.shape.build(), engine=case.engine)
146+
return
147+
with routes_off(ALL_ROUTES):
148+
general = g.gfql(case.shape.build(), engine=case.engine)
149+
oracle = _sig(graph_for(case.shape, "pandas").gfql(case.shape.build(), engine="pandas"), case.shape.frames)
150+
assert _canon(served._nodes) == _canon(general._nodes), f"{case.id}: node rows differ from the general path"
151+
assert _canon(served._edges) == _canon(general._edges), f"{case.id}: edge rows differ from the general path"
152+
assert _sig(served, case.shape.frames) == oracle, f"{case.id}: node/edge sets differ from the pandas general path"
153+
if calls["served"] == 0:
154+
pytest.xfail(f"{case.id}: admitted by the predicate, declined by the lane body (attenuation ledger)")
155+
156+
157+
def test_every_route_serves_most_of_what_it_admits(monkeypatch):
158+
"""A lane that declines most admitted shapes has a predicate that no longer describes it."""
159+
per_route: Dict[str, List[int]] = {}
160+
for case in CASES:
161+
if case.engine != ("polars" if case.route.name.startswith("polars") else "pandas"):
162+
continue
163+
if case.engine == "polars":
164+
pytest.importorskip("polars")
165+
g = graph_for(case.shape, case.engine, indexed=case.route.indexed)
166+
calls = _served(case, monkeypatch)
167+
try:
168+
g.gfql(case.shape.build(), engine=case.engine)
169+
except Exception:
170+
continue
171+
per_route.setdefault(case.route.name, []).append(calls["served"] > 0)
172+
for route, served in per_route.items():
173+
assert sum(served) * 2 >= len(served), f"{route}: served {sum(served)} of {len(served)} admitted shapes"
174+
175+
176+
def test_every_route_has_admitted_shapes():
177+
covered = {(c.route.name, c.engine) for c in CASES}
178+
for route in ROUTES:
179+
for engine in route.engines:
180+
assert (route.name, engine) in covered, f"{route.name}/{engine} admits no registered shape"

0 commit comments

Comments
 (0)