|
1 | 1 | import pandas as pd |
| 2 | +import graphistry |
2 | 3 | from common import NoAuthTestCase |
3 | 4 | from functools import lru_cache |
4 | 5 |
|
@@ -231,6 +232,60 @@ def test_hop_output_slice(self): |
231 | 232 | assert set(zip(g2._edges['s'], g2._edges['d'])) == {('b', 'c')} |
232 | 233 | assert set(g2._edges['edge_hop'].to_list()) == {2} |
233 | 234 |
|
| 235 | + def test_hop_cycle_min_gt_one(self): |
| 236 | + # Cycle a->b->c->a; ensure min>1 does not loop infinitely and labels stick to earliest hop |
| 237 | + edges = pd.DataFrame({'s': ['a', 'b', 'c'], 'd': ['b', 'c', 'a']}) |
| 238 | + g = graphistry.edges(edges, 's', 'd').nodes(pd.DataFrame({'id': ['a', 'b', 'c']}), 'id') |
| 239 | + seeds = pd.DataFrame({g._node: ['a']}) |
| 240 | + g2 = g.hop(seeds, min_hops=2, max_hops=3, label_nodes='hop', label_edges='edge_hop') |
| 241 | + assert set(zip(g2._edges['s'], g2._edges['d'])) == {('b', 'c'), ('c', 'a')} |
| 242 | + node_hops = dict(zip(g2._nodes[g._node], g2._nodes['hop'])) |
| 243 | + assert node_hops['a'] == 3 # first return to seed at hop 3 |
| 244 | + assert node_hops.get('c') == 2 |
| 245 | + assert set(g2._edges['edge_hop']) == {2, 3} |
| 246 | + |
| 247 | + def test_hop_undirected_min_gt_one(self): |
| 248 | + edges = pd.DataFrame({'s': ['a', 'b'], 'd': ['b', 'c']}) |
| 249 | + g = graphistry.edges(edges, 's', 'd').nodes(pd.DataFrame({'id': ['a', 'b', 'c']}), 'id') |
| 250 | + seeds = pd.DataFrame({g._node: ['a']}) |
| 251 | + g2 = g.hop(seeds, direction='undirected', min_hops=2, max_hops=3, label_nodes='hop', label_edges='edge_hop') |
| 252 | + assert set(zip(g2._edges['s'], g2._edges['d'])) == {('b', 'c')} |
| 253 | + assert set(g2._edges['edge_hop']) == {2} |
| 254 | + node_hops = dict(zip(g2._nodes[g._node], g2._nodes['hop'])) |
| 255 | + assert node_hops.get('c') == 2 |
| 256 | + |
| 257 | + def test_hop_label_collision_suffix(self): |
| 258 | + # Existing hop column should be preserved; new label suffixes |
| 259 | + g = simple_chain_graph() |
| 260 | + seeds = pd.DataFrame({g._node: ['a']}) |
| 261 | + g_existing = g.nodes(g._nodes.assign(hop='keep_me')) |
| 262 | + g2 = g_existing.hop(seeds, min_hops=1, max_hops=2, label_nodes='hop', label_edges='hop') |
| 263 | + assert 'hop' in g2._nodes.columns and 'hop_1' in g2._nodes.columns |
| 264 | + assert set(g2._edges.columns) & {'hop', 'hop_1'} == {'hop'} # edges only suffix once |
| 265 | + assert 'keep_me' in set(g2._nodes['hop']) |
| 266 | + |
| 267 | + def test_hop_seed_output_slice_with_label_seed(self): |
| 268 | + g = simple_chain_graph() |
| 269 | + seeds = pd.DataFrame({g._node: ['a']}) |
| 270 | + g2 = g.hop(seeds, min_hops=1, max_hops=3, output_min=2, output_max=3, label_nodes='hop', label_seed=True, drop_outside=False) |
| 271 | + # Seeds kept with hop 0 even though outside slice when drop_outside=False |
| 272 | + node_hops = dict(zip(g2._nodes[g._node], g2._nodes['hop'])) |
| 273 | + assert node_hops['a'] == 0 and node_hops['c'] == 2 and node_hops['d'] == 3 |
| 274 | + |
| 275 | + def test_hop_call_path_new_params(self): |
| 276 | + g = simple_chain_graph() |
| 277 | + seeds = pd.DataFrame({g._node: ['a']}) |
| 278 | + payload = {'type': 'Call', 'function': 'hop', 'params': { |
| 279 | + 'nodes': seeds, |
| 280 | + 'min_hops': 1, |
| 281 | + 'max_hops': 2, |
| 282 | + 'label_nodes': 'hop', |
| 283 | + 'label_edges': 'edge_hop' |
| 284 | + }} |
| 285 | + g2 = g.gfql([payload]) |
| 286 | + assert set(g2._nodes['hop']) == {1, 2} |
| 287 | + assert set(g2._edges['edge_hop']) == {1, 2} |
| 288 | + |
234 | 289 | class TestComputeHopMixinQuery(NoAuthTestCase): |
235 | 290 |
|
236 | 291 | def test_hop_source_query(self): |
|
0 commit comments