Skip to content

Commit 78c8032

Browse files
committed
Reference the filter set algebra independently, and cover negated composites
1 parent 6241140 commit 78c8032

2 files changed

Lines changed: 114 additions & 2 deletions

File tree

python/tests/test_base_install/test_filters/test_edges_collection_filter.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,85 @@ def _not_is_broken(a):
7676
return a in VIEWS or a in NODE_KIND
7777

7878

79+
def _not_composite_is_broken(a, b):
80+
# `~(A & B)` and `~(A | B)`: negating a *composite* reaches the same wrappers
81+
# through the negation, so `~(A & view)` degenerates to `~A` and loses the
82+
# view entirely. Only a composite of two edge predicates survives.
83+
return _kind(a) != "edge" or _kind(b) != "edge"
84+
85+
7986
def _ids(collection):
8087
return frozenset(e.id for e in collection)
8188

8289

90+
def _view_references(graph):
91+
"""Each view atom spelled as the equivalent chained view."""
92+
return {
93+
"layer": graph.layers(["work"]),
94+
"layers2": graph.layers(["work", "friends"]),
95+
"before": graph.before(10),
96+
"after": graph.after(8),
97+
"window": graph.window(3, 12),
98+
"at": graph.at(10),
99+
"latest": graph.latest(),
100+
"snap_at": graph.snapshot_at(10),
101+
"snap_latest": graph.snapshot_latest(),
102+
}
103+
104+
105+
# What each non-view atom selects, evaluated directly over the collection. Node
106+
# filters keep the edges whose *both* endpoints pass, which is how a node filter
107+
# reduces onto an edge.
108+
def _node_scores(graph, threshold):
109+
return {
110+
node.name
111+
for node in graph.nodes
112+
if (node.properties.get("score") or 0) > threshold
113+
}
114+
115+
116+
def _both_endpoints(graph, names):
117+
return {
118+
edge.id
119+
for edge in graph.edges
120+
if edge.src.name in names and edge.dst.name in names
121+
}
122+
123+
124+
def _predicate_references(graph):
125+
return {
126+
"edge_prop": {
127+
e.id for e in graph.edges if (e.properties.get("weight") or 0) > 5
128+
},
129+
"src": {e.id for e in graph.edges if e.src.name == "a"},
130+
"dst": {e.id for e in graph.edges if e.dst.name == "c"},
131+
"node_prop": _both_endpoints(graph, _node_scores(graph, 15)),
132+
"node_name": _both_endpoints(graph, {"b", "c"}),
133+
"is_valid": {e.id for e in graph.edges if e.is_valid()},
134+
"is_deleted": {e.id for e in graph.edges if e.is_deleted()},
135+
"is_active": {e.id for e in graph.edges if e.is_active()},
136+
"self_loop": {e.id for e in graph.edges if e.src.name == e.dst.name},
137+
}
138+
139+
83140
def _singles(graph):
84-
return {name: _ids(graph.edges[expr]) for name, expr in _atoms().items()}
141+
"""What each atom selects, computed *without* the subscript under test.
142+
143+
Reading these back through `graph.edges[atom]` would make the expectations
144+
below agree with the thing they are meant to check: where a single filter
145+
fails open, `EVERYTHING & X == X`, so a combination that dropped a term
146+
matches its expectation and the pins report a live bug as fixed. Views are
147+
referenced through the equivalent chained view instead, and predicates are
148+
evaluated over the collection directly.
149+
"""
150+
views = _view_references(graph)
151+
singles = {
152+
name: frozenset(ids) for name, ids in _predicate_references(graph).items()
153+
}
154+
singles.update({name: _ids(view.edges) for name, view in views.items()})
155+
missing = set(_atoms()) - set(singles)
156+
assert not missing, f"no independent reference for {sorted(missing)}"
157+
return singles
85158

86159

87160
def _assert_discriminating(graph, single, names):
@@ -157,6 +230,21 @@ def check(graph):
157230
cases.append((f"{a} & {b}", atoms[a] & atoms[b], single[a] & single[b]))
158231
if not _or_is_broken(a, b):
159232
cases.append((f"{a} | {b}", atoms[a] | atoms[b], single[a] | single[b]))
233+
if not _not_composite_is_broken(a, b):
234+
cases.append(
235+
(
236+
f"~({a} & {b})",
237+
~(atoms[a] & atoms[b]),
238+
every - (single[a] & single[b]),
239+
)
240+
)
241+
cases.append(
242+
(
243+
f"~({a} | {b})",
244+
~(atoms[a] | atoms[b]),
245+
every - (single[a] | single[b]),
246+
)
247+
)
160248
for a in atoms:
161249
if not _not_is_broken(a):
162250
cases.append((f"~{a}", ~atoms[a], every - single[a]))
@@ -309,6 +397,17 @@ def check(graph):
309397
~atoms["node_name"],
310398
every - single["node_name"],
311399
),
400+
# Negating a composite that contains a view: the pairwise rules
401+
# above only ever negate a single atom, so these shapes need their
402+
# own representatives.
403+
"not of an and containing a view loses the view": (
404+
~(atoms["edge_prop"] & atoms["layer"]),
405+
every - (single["edge_prop"] & single["layer"]),
406+
),
407+
"not of an or containing a view returns every edge": (
408+
~(atoms["edge_prop"] | atoms["layer"]),
409+
every - (single["edge_prop"] | single["layer"]),
410+
),
312411
}
313412
fixed = []
314413
for label, (expr, want) in representatives.items():

python/tests/test_base_install/test_filters/test_nodes_collection_filter.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,20 @@ def check(graph):
7070
"before": Graph.before(12),
7171
"layer": Graph.layer("work"),
7272
}
73-
single = {n: frozenset(graph.nodes[e].name) for n, e in atoms.items()}
73+
# References computed without `nodes[...]`, which is the thing under
74+
# test: views come from the equivalent chained view, predicates are
75+
# evaluated over the collection. Reading them back through the subscript
76+
# would make these expectations agree with it by construction.
77+
single = {
78+
"name": frozenset({"a", "b"}) & frozenset(graph.nodes.name),
79+
"prop": frozenset(
80+
n.name for n in graph.nodes if (n.properties.get("score") or 0) > 15
81+
),
82+
"window": frozenset(graph.window(3, 12).nodes.name),
83+
"before": frozenset(graph.before(12).nodes.name),
84+
"layer": frozenset(graph.layer("work").nodes.name),
85+
}
86+
assert set(single) == set(atoms), "every atom needs an independent reference"
7487
every = frozenset(graph.nodes.name)
7588
cases = []
7689
for a, b in combinations(atoms, 2):

0 commit comments

Comments
 (0)