This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
TOOL_ROOT = Path(__file__).parents[1]
|
||||
sys.path.insert(0, str(TOOL_ROOT))
|
||||
|
||||
from raptor_graph_explorer.cli import build_output
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fixture_dir() -> Path:
|
||||
return Path(__file__).with_name("fixtures")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def built_output(tmp_path: Path, fixture_dir: Path) -> Path:
|
||||
reports = tmp_path / "reports"
|
||||
reports.mkdir()
|
||||
for source in fixture_dir.glob("demo_*.csv"):
|
||||
shutil.copy2(source, reports / source.name)
|
||||
output = tmp_path / "built"
|
||||
build_output([reports], output)
|
||||
return output
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def regression_output(tmp_path: Path, fixture_dir: Path) -> Path:
|
||||
output = tmp_path / "regressions"
|
||||
build_output([fixture_dir / "regressions"], output)
|
||||
return output
|
||||
@@ -0,0 +1,10 @@
|
||||
Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id,edge_extra
|
||||
gc:0,gcb:1:0,16,tensor<4xf32>,spatial1,,0,,a
|
||||
gc:0,gcb:1:1,16,tensor<4xf32>,spatial1,,1,,a
|
||||
gcb:1:0,gcb:2:0,16,tensor<4xf32>,spatial1,0,0,,b
|
||||
gcb:1:1,gcb:2:1,16,tensor<4xf32>,spatial1,1,1,,b
|
||||
gcb:2:0,gcb:3:0,8,tensor<2xf32>,spatial1,,,7,c
|
||||
gcb:2:1,gcb:3:1,8,tensor<2xf32>,spatial1,,,8,c
|
||||
gcb:3:0,gc:4,8,tensor<2xf32>,spatial1,0,,,d
|
||||
gcb:3:1,gc:4,8,tensor<2xf32>,spatial1,1,,,d
|
||||
gc:4,mystery:5,,tensor<1xf32>,spatial1,,,,e
|
||||
|
@@ -0,0 +1,10 @@
|
||||
Id,op_id,lane,core,ssa_name,node_extra
|
||||
gc:0,0,,,%0,root
|
||||
gcb:1:0,1,0,,%1#0,lane
|
||||
gcb:1:1,1,1,,%1#1,lane
|
||||
gcb:2:0,2,0,,%2#0,lane
|
||||
gcb:2:1,2,1,,%2#1,lane
|
||||
gcb:3:0,3,0,,%3#0,lane
|
||||
gcb:3:1,3,1,,%3#1,lane
|
||||
gc:4,4,,,%4,sink
|
||||
mystery:5,5,,,%5,unknown
|
||||
|
@@ -0,0 +1,3 @@
|
||||
Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id
|
||||
scb:0:0,sc:1,4,tensor<1xf32>,spatial3,0,,21
|
||||
scb:0:1,sc:1,4,tensor<1xf32>,spatial3,1,,22
|
||||
|
@@ -0,0 +1,4 @@
|
||||
Id,op_id,lane,core,ssa_name
|
||||
scb:0:0,0,0,10,%0
|
||||
scb:0:1,0,1,11,%0
|
||||
sc:1,1,,12,%1
|
||||
|
@@ -0,0 +1 @@
|
||||
Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id
|
||||
|
@@ -0,0 +1,4 @@
|
||||
Id,op_id,lane,core,ssa_name
|
||||
gc:0,0,,,%0
|
||||
gcb:1:0,1,0,,%1
|
||||
gcb:1:1,1,1,,%1
|
||||
|
+65
@@ -0,0 +1,65 @@
|
||||
Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id
|
||||
gcb:1:0,gcb:2:0,8192,tensor<16x1x8x1x16xf32>,spatial1,0,0,
|
||||
gcb:1:1,gcb:2:1,8192,tensor<16x1x8x1x16xf32>,spatial1,1,1,
|
||||
gcb:1:2,gcb:2:2,8192,tensor<16x1x8x1x16xf32>,spatial1,2,2,
|
||||
gcb:1:3,gcb:2:3,8192,tensor<16x1x8x1x16xf32>,spatial1,3,3,
|
||||
gcb:1:4,gcb:2:4,8192,tensor<16x1x8x1x16xf32>,spatial1,4,4,
|
||||
gcb:1:5,gcb:2:5,8192,tensor<16x1x8x1x16xf32>,spatial1,5,5,
|
||||
gcb:1:6,gcb:2:6,8192,tensor<16x1x8x1x16xf32>,spatial1,6,6,
|
||||
gcb:1:7,gcb:2:7,8192,tensor<16x1x8x1x16xf32>,spatial1,7,7,
|
||||
gcb:1:8,gcb:2:8,8192,tensor<16x1x8x1x16xf32>,spatial1,8,8,
|
||||
gcb:1:9,gcb:2:9,8192,tensor<16x1x8x1x16xf32>,spatial1,9,9,
|
||||
gcb:1:10,gcb:2:10,8192,tensor<16x1x8x1x16xf32>,spatial1,10,10,
|
||||
gcb:1:11,gcb:2:11,8192,tensor<16x1x8x1x16xf32>,spatial1,11,11,
|
||||
gcb:1:12,gcb:2:12,8192,tensor<16x1x8x1x16xf32>,spatial1,12,12,
|
||||
gcb:1:13,gcb:2:13,8192,tensor<16x1x8x1x16xf32>,spatial1,13,13,
|
||||
gcb:1:14,gcb:2:14,8192,tensor<16x1x8x1x16xf32>,spatial1,14,14,
|
||||
gcb:1:15,gcb:2:15,8192,tensor<16x1x8x1x16xf32>,spatial1,15,15,
|
||||
gcb:2:0,gcb:3:0,8192,tensor<16x1x8x1x16xf32>,spatial1,0,0,
|
||||
gcb:2:1,gcb:3:1,8192,tensor<16x1x8x1x16xf32>,spatial1,1,1,
|
||||
gcb:2:2,gcb:3:2,8192,tensor<16x1x8x1x16xf32>,spatial1,2,2,
|
||||
gcb:2:3,gcb:3:3,8192,tensor<16x1x8x1x16xf32>,spatial1,3,3,
|
||||
gcb:2:4,gcb:3:4,8192,tensor<16x1x8x1x16xf32>,spatial1,4,4,
|
||||
gcb:2:5,gcb:3:5,8192,tensor<16x1x8x1x16xf32>,spatial1,5,5,
|
||||
gcb:2:6,gcb:3:6,8192,tensor<16x1x8x1x16xf32>,spatial1,6,6,
|
||||
gcb:2:7,gcb:3:7,8192,tensor<16x1x8x1x16xf32>,spatial1,7,7,
|
||||
gcb:2:8,gcb:3:8,8192,tensor<16x1x8x1x16xf32>,spatial1,8,8,
|
||||
gcb:2:9,gcb:3:9,8192,tensor<16x1x8x1x16xf32>,spatial1,9,9,
|
||||
gcb:2:10,gcb:3:10,8192,tensor<16x1x8x1x16xf32>,spatial1,10,10,
|
||||
gcb:2:11,gcb:3:11,8192,tensor<16x1x8x1x16xf32>,spatial1,11,11,
|
||||
gcb:2:12,gcb:3:12,8192,tensor<16x1x8x1x16xf32>,spatial1,12,12,
|
||||
gcb:2:13,gcb:3:13,8192,tensor<16x1x8x1x16xf32>,spatial1,13,13,
|
||||
gcb:2:14,gcb:3:14,8192,tensor<16x1x8x1x16xf32>,spatial1,14,14,
|
||||
gcb:2:15,gcb:3:15,8192,tensor<16x1x8x1x16xf32>,spatial1,15,15,
|
||||
gcb:3:0,gcb:4:0,8192,tensor<16x1x8x1x16xf32>,spatial1,0,0,
|
||||
gcb:3:1,gcb:4:1,8192,tensor<16x1x8x1x16xf32>,spatial1,1,1,
|
||||
gcb:3:2,gcb:4:2,8192,tensor<16x1x8x1x16xf32>,spatial1,2,2,
|
||||
gcb:3:3,gcb:4:3,8192,tensor<16x1x8x1x16xf32>,spatial1,3,3,
|
||||
gcb:3:4,gcb:4:4,8192,tensor<16x1x8x1x16xf32>,spatial1,4,4,
|
||||
gcb:3:5,gcb:4:5,8192,tensor<16x1x8x1x16xf32>,spatial1,5,5,
|
||||
gcb:3:6,gcb:4:6,8192,tensor<16x1x8x1x16xf32>,spatial1,6,6,
|
||||
gcb:3:7,gcb:4:7,8192,tensor<16x1x8x1x16xf32>,spatial1,7,7,
|
||||
gcb:3:8,gcb:4:8,8192,tensor<16x1x8x1x16xf32>,spatial1,8,8,
|
||||
gcb:3:9,gcb:4:9,8192,tensor<16x1x8x1x16xf32>,spatial1,9,9,
|
||||
gcb:3:10,gcb:4:10,8192,tensor<16x1x8x1x16xf32>,spatial1,10,10,
|
||||
gcb:3:11,gcb:4:11,8192,tensor<16x1x8x1x16xf32>,spatial1,11,11,
|
||||
gcb:3:12,gcb:4:12,8192,tensor<16x1x8x1x16xf32>,spatial1,12,12,
|
||||
gcb:3:13,gcb:4:13,8192,tensor<16x1x8x1x16xf32>,spatial1,13,13,
|
||||
gcb:3:14,gcb:4:14,8192,tensor<16x1x8x1x16xf32>,spatial1,14,14,
|
||||
gcb:3:15,gcb:4:15,8192,tensor<16x1x8x1x16xf32>,spatial1,15,15,
|
||||
gc:0,gcb:1:0,3888,tensor<1x3x18x18xf32>,spatial1,,0,
|
||||
gc:0,gcb:1:1,3888,tensor<1x3x18x18xf32>,spatial1,,1,
|
||||
gc:0,gcb:1:2,3888,tensor<1x3x18x18xf32>,spatial1,,2,
|
||||
gc:0,gcb:1:3,3888,tensor<1x3x18x18xf32>,spatial1,,3,
|
||||
gc:0,gcb:1:4,3888,tensor<1x3x18x18xf32>,spatial1,,4,
|
||||
gc:0,gcb:1:5,3888,tensor<1x3x18x18xf32>,spatial1,,5,
|
||||
gc:0,gcb:1:6,3888,tensor<1x3x18x18xf32>,spatial1,,6,
|
||||
gc:0,gcb:1:7,3888,tensor<1x3x18x18xf32>,spatial1,,7,
|
||||
gc:0,gcb:1:8,3888,tensor<1x3x18x18xf32>,spatial1,,8,
|
||||
gc:0,gcb:1:9,3888,tensor<1x3x18x18xf32>,spatial1,,9,
|
||||
gc:0,gcb:1:10,3888,tensor<1x3x18x18xf32>,spatial1,,10,
|
||||
gc:0,gcb:1:11,3888,tensor<1x3x18x18xf32>,spatial1,,11,
|
||||
gc:0,gcb:1:12,3888,tensor<1x3x18x18xf32>,spatial1,,12,
|
||||
gc:0,gcb:1:13,3888,tensor<1x3x18x18xf32>,spatial1,,13,
|
||||
gc:0,gcb:1:14,3888,tensor<1x3x18x18xf32>,spatial1,,14,
|
||||
gc:0,gcb:1:15,3888,tensor<1x3x18x18xf32>,spatial1,,15,
|
||||
|
+66
@@ -0,0 +1,66 @@
|
||||
Id,op_id,lane,core,ssa_name
|
||||
gc:0,0,,,%0
|
||||
gcb:1:0,1,0,,%1
|
||||
gcb:1:1,1,1,,%1
|
||||
gcb:1:2,1,2,,%1
|
||||
gcb:1:3,1,3,,%1
|
||||
gcb:1:4,1,4,,%1
|
||||
gcb:1:5,1,5,,%1
|
||||
gcb:1:6,1,6,,%1
|
||||
gcb:1:7,1,7,,%1
|
||||
gcb:1:8,1,8,,%1
|
||||
gcb:1:9,1,9,,%1
|
||||
gcb:1:10,1,10,,%1
|
||||
gcb:1:11,1,11,,%1
|
||||
gcb:1:12,1,12,,%1
|
||||
gcb:1:13,1,13,,%1
|
||||
gcb:1:14,1,14,,%1
|
||||
gcb:1:15,1,15,,%1
|
||||
gcb:2:0,2,0,,%2
|
||||
gcb:2:1,2,1,,%2
|
||||
gcb:2:2,2,2,,%2
|
||||
gcb:2:3,2,3,,%2
|
||||
gcb:2:4,2,4,,%2
|
||||
gcb:2:5,2,5,,%2
|
||||
gcb:2:6,2,6,,%2
|
||||
gcb:2:7,2,7,,%2
|
||||
gcb:2:8,2,8,,%2
|
||||
gcb:2:9,2,9,,%2
|
||||
gcb:2:10,2,10,,%2
|
||||
gcb:2:11,2,11,,%2
|
||||
gcb:2:12,2,12,,%2
|
||||
gcb:2:13,2,13,,%2
|
||||
gcb:2:14,2,14,,%2
|
||||
gcb:2:15,2,15,,%2
|
||||
gcb:3:0,3,0,,%3
|
||||
gcb:3:1,3,1,,%3
|
||||
gcb:3:2,3,2,,%3
|
||||
gcb:3:3,3,3,,%3
|
||||
gcb:3:4,3,4,,%3
|
||||
gcb:3:5,3,5,,%3
|
||||
gcb:3:6,3,6,,%3
|
||||
gcb:3:7,3,7,,%3
|
||||
gcb:3:8,3,8,,%3
|
||||
gcb:3:9,3,9,,%3
|
||||
gcb:3:10,3,10,,%3
|
||||
gcb:3:11,3,11,,%3
|
||||
gcb:3:12,3,12,,%3
|
||||
gcb:3:13,3,13,,%3
|
||||
gcb:3:14,3,14,,%3
|
||||
gcb:3:15,3,15,,%3
|
||||
gcb:4:0,4,0,,%4
|
||||
gcb:4:1,4,1,,%4
|
||||
gcb:4:2,4,2,,%4
|
||||
gcb:4:3,4,3,,%4
|
||||
gcb:4:4,4,4,,%4
|
||||
gcb:4:5,4,5,,%4
|
||||
gcb:4:6,4,6,,%4
|
||||
gcb:4:7,4,7,,%4
|
||||
gcb:4:8,4,8,,%4
|
||||
gcb:4:9,4,9,,%4
|
||||
gcb:4:10,4,10,,%4
|
||||
gcb:4:11,4,11,,%4
|
||||
gcb:4:12,4,12,,%4
|
||||
gcb:4:13,4,13,,%4
|
||||
gcb:4:14,4,14,,%4
|
||||
gcb:4:15,4,15,,%4
|
||||
|
+2
@@ -0,0 +1,2 @@
|
||||
Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id
|
||||
scb:1:5,sc:0,512,tensor<1x8x1x16xf32>,spatial3,5,,0
|
||||
|
+17
@@ -0,0 +1,17 @@
|
||||
Id,op_id,lane,core,ssa_name
|
||||
sc:0,0,,500,%0#0;%0#1;%0#2;%0#3;%0#4
|
||||
scb:1:0,1,0,380,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:1,1,1,419,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:2,1,2,420,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:3,1,3,421,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:4,1,4,459,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:5,1,5,460,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:6,1,6,461,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:7,1,7,498,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:8,1,8,499,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:9,1,9,501,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:10,1,10,502,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:11,1,11,539,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:12,1,12,540,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:13,1,13,541,%1#0;%1#1;%1#2;%1#3
|
||||
scb:1:14,1,14,580,%1#0;%1#1;%1#2;%1#3
|
||||
|
@@ -0,0 +1,57 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import os
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from raptor_graph_explorer.cli import build_output
|
||||
|
||||
|
||||
def test_operation_core_and_node_kind_aggregation(built_output: Path):
|
||||
db = sqlite3.connect(built_output / "graph.sqlite")
|
||||
report = db.execute("SELECT * FROM reports WHERE report_id='demo_graph'").fetchone()
|
||||
assert report[2:8] == (9, 9, 6, 5, 1, 1)
|
||||
op = db.execute("SELECT instance_count,lane_count,minimum_lane,maximum_lane FROM aggregate_nodes WHERE aggregate_id='operation:demo_graph:1'").fetchone()
|
||||
assert op == (2, 2, 0, 1)
|
||||
assert db.execute("SELECT COUNT(*) FROM aggregate_nodes WHERE report_id='demo_graph' AND level='node_kind'").fetchone()[0] == 3
|
||||
core = db.execute("SELECT group_key FROM aggregate_nodes WHERE report_id='demo_scheduled' AND level='core' ORDER BY group_key").fetchall()
|
||||
assert core == [("10",), ("11",), ("12",)]
|
||||
db.close()
|
||||
|
||||
|
||||
def test_manifest_generation(built_output: Path):
|
||||
import json
|
||||
manifest = json.loads((built_output / "manifest.json").read_text())
|
||||
assert manifest["schema_version"] == 1
|
||||
assert [report["report_id"] for report in manifest["reports"]] == ["demo_graph", "demo_scheduled"]
|
||||
assert all("input" not in report for report in manifest["reports"])
|
||||
|
||||
|
||||
def _generated_report(directory: Path, operations=10, lanes=50, edges=500):
|
||||
directory.mkdir()
|
||||
with (directory / "generated.nodes.csv").open("w", newline="") as stream:
|
||||
writer=csv.writer(stream);writer.writerow(["Id","op_id","lane","core","ssa_name"])
|
||||
for op in range(operations):
|
||||
for lane in range(lanes):writer.writerow([f"gcb:{op}:{lane}",op,lane,"",f"%{op}"])
|
||||
with (directory / "generated.edges.csv").open("w", newline="") as stream:
|
||||
writer=csv.writer(stream);writer.writerow(["Source","Target","Weight","Type","stage","source_lane","target_lane","channel_id"])
|
||||
for index in range(edges):
|
||||
op=index%(operations-1);lane=index%lanes;writer.writerow([f"gcb:{op}:{lane}",f"gcb:{op+1}:{lane}",4,"tensor<1xf32>","generated",lane,lane,""])
|
||||
|
||||
|
||||
def test_generated_performance_shape(tmp_path: Path):
|
||||
_generated_report(tmp_path / "reports")
|
||||
manifest=build_output([tmp_path / "reports"],tmp_path / "out")
|
||||
assert manifest["reports"][0]["raw_node_count"] == 500
|
||||
assert manifest["reports"][0]["raw_edge_count"] == 500
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.skipif(not os.getenv("RAPTOR_GRAPH_SLOW"), reason="set RAPTOR_GRAPH_SLOW=1")
|
||||
def test_100k_edge_smoke(tmp_path: Path):
|
||||
_generated_report(tmp_path / "reports",operations=100,lanes=1000,edges=100_000)
|
||||
manifest=build_output([tmp_path / "reports"],tmp_path / "out")
|
||||
assert manifest["reports"][0]["raw_edge_count"] == 100_000
|
||||
@@ -0,0 +1,45 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from raptor_graph_explorer.api import create_app
|
||||
|
||||
|
||||
def test_manifest_graph_motifs_and_pagination(built_output: Path):
|
||||
client=TestClient(create_app(built_output,max_page_size=2))
|
||||
assert "Raptor Graph Explorer" in client.get("/").text
|
||||
assert "runLayout" in client.get("/app.js").text
|
||||
assert client.get("/api/manifest").status_code==200
|
||||
assert len(client.get("/api/reports").json())==2
|
||||
graph=client.get("/api/reports/demo_graph/graph/operation").json()
|
||||
assert len(graph["nodes"])==6 and len(graph["edges"])==5
|
||||
assert client.get("/api/reports/demo_graph/motifs").status_code==200
|
||||
page=client.get("/api/reports/demo_graph/raw-nodes?limit=2").json()
|
||||
assert page["total"]==9 and len(page["items"])==2
|
||||
assert client.get("/api/reports/demo_graph/raw-nodes?limit=3").status_code==400
|
||||
|
||||
|
||||
def test_edge_mapping_matrix_and_binning(built_output: Path):
|
||||
client=TestClient(create_app(built_output,exact_matrix_points=1))
|
||||
edge="operation:demo_graph:1->2"
|
||||
detail=client.get(f"/api/aggregate-edges/{edge}").json()
|
||||
assert detail["mapping_kind"]=="identity"
|
||||
assert client.get(f"/api/aggregate-edges/{edge}/mapping").json()["total"]==2
|
||||
matrix=client.get(f"/api/aggregate-edges/{edge}/matrix").json()
|
||||
assert matrix["mode"]=="binned" and matrix["dimensions"]==[64,64]
|
||||
|
||||
|
||||
def test_expansion_safety_cap(built_output: Path):
|
||||
client=TestClient(create_app(built_output,expansion_cap=2))
|
||||
selected="operation:demo_graph:1"
|
||||
response=client.get(f"/api/reports/demo_graph/subgraph?aggregate_id={selected}&expand_lanes=true")
|
||||
assert response.status_code==400 and "safety cap" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_unsupported_database_schema_is_rejected(built_output: Path):
|
||||
import sqlite3
|
||||
db=sqlite3.connect(built_output/"graph.sqlite")
|
||||
db.execute("UPDATE metadata SET value='999' WHERE key='schema_version'");db.commit();db.close()
|
||||
with pytest.raises(ValueError,match="unsupported database schema"):
|
||||
create_app(built_output)
|
||||
@@ -0,0 +1,13 @@
|
||||
from pathlib import Path
|
||||
|
||||
from raptor_graph_explorer.cli import main
|
||||
|
||||
|
||||
def test_build_inspect_and_exit_codes(tmp_path: Path, fixture_dir: Path, capsys):
|
||||
output=tmp_path/"output"
|
||||
assert main(["build",str(fixture_dir),"--output",str(output)])==0
|
||||
assert main(["inspect",str(output)])==0
|
||||
captured=capsys.readouterr().out
|
||||
assert "demo_graph" in captured and "mappings:" in captured
|
||||
assert main(["build",str(fixture_dir),"--output",str(output)])==2
|
||||
assert main(["build",str(tmp_path/"missing"),"--output",str(tmp_path/"bad")])==2
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from raptor_graph_explorer.discovery import discover_reports
|
||||
|
||||
|
||||
def test_discovers_complete_pairs_and_reports_incomplete(tmp_path: Path, fixture_dir: Path):
|
||||
nested = tmp_path / "nested"
|
||||
nested.mkdir()
|
||||
for path in fixture_dir.glob("demo_graph.*.csv"):
|
||||
(nested / path.name).write_bytes(path.read_bytes())
|
||||
(tmp_path / "lonely.nodes.csv").write_text("Id,op_id,lane,core,ssa_name\n")
|
||||
pairs, diagnostics = discover_reports([tmp_path], tmp_path / "extract")
|
||||
assert [pair.report_id for pair in pairs] == ["demo_graph"]
|
||||
assert any(item.code == "incomplete_pair" for item in diagnostics.items)
|
||||
|
||||
|
||||
def test_zip_and_explicit_repeated_inputs(tmp_path: Path, fixture_dir: Path):
|
||||
archive = tmp_path / "reports.zip"
|
||||
with zipfile.ZipFile(archive, "w") as zipped:
|
||||
for path in fixture_dir.glob("demo_graph.*.csv"):
|
||||
zipped.write(path, f"sub/{path.name}")
|
||||
pairs, _ = discover_reports([archive], tmp_path / "extract")
|
||||
assert len(pairs) == 1 and pairs[0].report_id == "demo_graph"
|
||||
|
||||
|
||||
def test_zip_traversal_is_rejected(tmp_path: Path):
|
||||
archive = tmp_path / "bad.zip"
|
||||
with zipfile.ZipFile(archive, "w") as zipped:
|
||||
zipped.writestr("../escape.nodes.csv", "bad")
|
||||
pairs, diagnostics = discover_reports([archive], tmp_path / "extract")
|
||||
assert not pairs
|
||||
assert diagnostics.items[0].code == "invalid_zip"
|
||||
assert not (tmp_path / "escape.nodes.csv").exists()
|
||||
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
from raptor_graph_explorer.cli import build_output
|
||||
|
||||
|
||||
def test_normalization_unknown_columns_prefix_and_lane_fallback(built_output: Path):
|
||||
db = sqlite3.connect(built_output / "graph.sqlite")
|
||||
db.row_factory = sqlite3.Row
|
||||
unknown = db.execute("SELECT * FROM raw_nodes WHERE node_id='mystery:5'").fetchone()
|
||||
assert unknown["node_kind"] == "unknown:mystery"
|
||||
assert json.loads(unknown["attributes_json"]) == {"node_extra": "unknown"}
|
||||
edge = db.execute("SELECT * FROM raw_edges WHERE source_node_id='gcb:2:0'").fetchone()
|
||||
assert (edge["source_lane"], edge["target_lane"]) == (0, 0)
|
||||
assert json.loads(edge["attributes_json"])["edge_extra"] == "c"
|
||||
db.close()
|
||||
|
||||
|
||||
def test_malformed_rows_unknown_endpoint_and_strict_mode(tmp_path: Path):
|
||||
reports = tmp_path / "reports"; reports.mkdir()
|
||||
(reports / "bad.nodes.csv").write_text(
|
||||
"Id,op_id,lane,core,ssa_name\ngc:0,0,,,%0\ngc:1,nope,,,%1\n")
|
||||
(reports / "bad.edges.csv").write_text(
|
||||
"Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id\n"
|
||||
"gc:0,gc:missing,1,tensor<1xf32>,bad,,,\n")
|
||||
manifest = build_output([reports], tmp_path / "out")
|
||||
assert manifest["reports"][0]["raw_node_count"] == 1
|
||||
assert manifest["reports"][0]["raw_edge_count"] == 0
|
||||
assert manifest["diagnostic_count"] == 2
|
||||
try:
|
||||
build_output([reports], tmp_path / "strict", strict=True)
|
||||
except RuntimeError as exc:
|
||||
assert "strict build" in str(exc)
|
||||
else:
|
||||
raise AssertionError("strict mode accepted invalid rows")
|
||||
|
||||
|
||||
def test_missing_schema_column_is_diagnosed(tmp_path: Path):
|
||||
reports = tmp_path / "reports"; reports.mkdir()
|
||||
(reports / "x.nodes.csv").write_text("Id,op_id,lane,core\ngc:0,0,,,\n")
|
||||
(reports / "x.edges.csv").write_text("Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id\n")
|
||||
manifest = build_output([reports], tmp_path / "out")
|
||||
assert manifest["reports"][0]["raw_node_count"] == 0
|
||||
assert manifest["diagnostic_count"] == 2
|
||||
|
||||
|
||||
def test_explicit_edge_lane_precedes_endpoint_lane(tmp_path: Path):
|
||||
reports=tmp_path/"reports";reports.mkdir()
|
||||
(reports/"x.nodes.csv").write_text("Id,op_id,lane,core,ssa_name\ngcb:0:0,0,0,,%0\ngcb:1:0,1,0,,%1\n")
|
||||
(reports/"x.edges.csv").write_text("Source,Target,Weight,Type,stage,source_lane,target_lane,channel_id\ngcb:0:0,gcb:1:0,1,tensor<1xf32>,x,9,8,\n")
|
||||
output=tmp_path/"out";build_output([reports],output)
|
||||
db=sqlite3.connect(output/"graph.sqlite")
|
||||
assert db.execute("SELECT source_lane,target_lane FROM raw_edges").fetchone()==(9,8)
|
||||
assert db.execute("SELECT occurrence_count FROM diagnostics WHERE code='contradictory_lane'").fetchone()==(1,)
|
||||
db.close()
|
||||
@@ -0,0 +1,22 @@
|
||||
import pytest
|
||||
|
||||
from raptor_graph_explorer.mappings import classify_lane_pairs
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("pairs","kind"),[
|
||||
([(None,None)],"scalar_to_scalar"), ([(None,0),(None,1)],"scalar_to_batch"),
|
||||
([(0,None),(1,None)],"batch_to_scalar"), ([(0,0),(1,1)],"identity"),
|
||||
([(0,2),(1,3)],"constant_offset"), ([(0,1),(1,0)],"permutation"),
|
||||
([(3,0),(3,1)],"broadcast"), ([(0,3),(1,3)],"fan_in"),
|
||||
([(0,0),(0,1),(1,1),(1,2)],"stencil"),
|
||||
([(0,0),(0,2),(1,1)],"irregular"), ([(None,1),(2,3)],"unknown"),
|
||||
])
|
||||
def test_all_mapping_classes(pairs,kind):
|
||||
assert classify_lane_pairs(pairs)[0] == kind
|
||||
|
||||
|
||||
def test_mapping_metadata_is_exact():
|
||||
assert classify_lane_pairs([(0,2),(1,3)])[1] == {"offset":2}
|
||||
kind,metadata=classify_lane_pairs([(0,0),(0,1),(1,1),(1,2)])
|
||||
assert kind == "stencil" and metadata == {"offsets":[0,1]}
|
||||
assert classify_lane_pairs([(None,2),(None,3)])[1]["target"]["contiguous"] is True
|
||||
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from raptor_graph_explorer.motifs import detect_motifs
|
||||
|
||||
|
||||
def graph(edges):
|
||||
result=nx.DiGraph();result.add_edges_from(edges);return result
|
||||
|
||||
|
||||
def test_exact_diamond():
|
||||
motifs,scc=detect_motifs(graph([("s","a"),("s","b"),("a","t"),("b","t")]))
|
||||
assert scc == 4 and len(motifs)==1 and motifs[0].kind=="exact_diamond"
|
||||
|
||||
|
||||
def test_longer_rhomboid():
|
||||
motifs,_=detect_motifs(graph([("s","a"),("a","c"),("c","t"),("s","b"),("b","d"),("d","t")]))
|
||||
assert len(motifs)==1 and motifs[0].kind=="rhomboid"
|
||||
|
||||
|
||||
def test_three_branch_split_rejoin():
|
||||
motifs,_=detect_motifs(graph([("s","a"),("s","b"),("s","c"),("a","t"),("b","t"),("c","t")]))
|
||||
assert len(motifs)==1 and len(motifs[0].branches)==3
|
||||
|
||||
|
||||
def test_nested_motifs_are_preserved():
|
||||
edges=[("s","a"),("s","b"),("a","c"),("a","d"),("c","e"),("d","e"),("e","t"),("b","t")]
|
||||
motifs,_=detect_motifs(graph(edges))
|
||||
assert {(motif.entry,motif.exit) for motif in motifs} == {("a","e"),("s","t")}
|
||||
|
||||
|
||||
def test_disconnected_graph():
|
||||
motifs,scc=detect_motifs(graph([("s","a"),("s","b"),("a","t"),("b","t"),("x","y")]))
|
||||
assert len(motifs)==1 and scc==6
|
||||
|
||||
|
||||
def test_cycle_is_condensed():
|
||||
edges=[("s","a"),("s","b"),("a","c"),("c","a"),("c","t"),("b","t")]
|
||||
motifs,_=detect_motifs(graph(edges))
|
||||
assert len(motifs)==1 and motifs[0].contains_cycle
|
||||
|
||||
|
||||
def test_external_entrance_rejected():
|
||||
edges=[("s","a"),("s","b"),("a","t"),("b","t"),("x","a")]
|
||||
assert detect_motifs(graph(edges))[0] == []
|
||||
|
||||
|
||||
def test_external_exit_rejected():
|
||||
edges=[("s","a"),("s","b"),("a","t"),("b","t"),("a","x")]
|
||||
assert detect_motifs(graph(edges))[0] == []
|
||||
@@ -0,0 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
import pytest
|
||||
|
||||
from raptor_graph_explorer.api import create_app
|
||||
from raptor_graph_explorer.database import connect_readonly
|
||||
from raptor_graph_explorer.projection import DisplayGraphTooLarge, display_graph
|
||||
|
||||
|
||||
def project(output: Path, report_id: str, expanded=(), *, view="operation", cap=5_000):
|
||||
db = connect_readonly(output / "graph.sqlite")
|
||||
try:
|
||||
return display_graph(db, report_id, view=view, expanded_operation_ids=expanded, cap=cap)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_spatial1_baseline_and_one_operation_expansion(regression_output: Path):
|
||||
db = connect_readonly(regression_output / "graph.sqlite")
|
||||
source_counts = db.execute("""
|
||||
SELECT operation_node_count,operation_edge_count,raw_node_count,raw_edge_count
|
||||
FROM reports WHERE report_id='spatial1_graph'
|
||||
""").fetchone()
|
||||
db.close()
|
||||
assert tuple(source_counts) == (5, 4, 65, 64)
|
||||
baseline = project(regression_output, "spatial1_graph")
|
||||
assert baseline["counts"] == {
|
||||
"nodes": 5, "edges": 4, "isolated_nodes": 0,
|
||||
"raw_nodes": 0, "aggregate_nodes": 5, "raw_edges": 0, "aggregate_edges": 4,
|
||||
}
|
||||
expanded = project(regression_output, "spatial1_graph", [1])
|
||||
node_ids = {node["display_id"] for node in expanded["nodes"]}
|
||||
assert "operation:spatial1_graph:1" not in node_ids
|
||||
assert {f"gcb:1:{lane}" for lane in range(16)} <= node_ids
|
||||
assert {f"operation:spatial1_graph:{op_id}" for op_id in (0, 2, 3, 4)} <= node_ids
|
||||
aggregate_pairs = {(edge["source_op_id"], edge["target_op_id"])
|
||||
for edge in expanded["edges"] if edge["representation"] == "aggregate"}
|
||||
assert aggregate_pairs == {(2, 3), (3, 4)}
|
||||
assert {(edge["source_op_id"], edge["target_op_id"])
|
||||
for edge in expanded["edges"] if edge["representation"] == "raw"} == {(0, 1), (1, 2)}
|
||||
assert len({edge["display_id"] for edge in expanded["edges"]}) == len(expanded["edges"])
|
||||
|
||||
|
||||
def test_expansion_order_is_irrelevant(regression_output: Path):
|
||||
first = project(regression_output, "spatial1_graph", [1, 2])
|
||||
second = project(regression_output, "spatial1_graph", [2, 1])
|
||||
assert first["nodes"] == second["nodes"]
|
||||
assert first["edges"] == second["edges"]
|
||||
|
||||
|
||||
def test_expand_all_spatial1_is_exact_raw_graph(regression_output: Path):
|
||||
graph = project(regression_output, "spatial1_graph", range(5))
|
||||
assert graph["counts"] == {
|
||||
"nodes": 65, "edges": 64, "isolated_nodes": 0,
|
||||
"raw_nodes": 65, "aggregate_nodes": 0, "raw_edges": 64, "aggregate_edges": 0,
|
||||
}
|
||||
node_ids = {node["node_id"] for node in graph["nodes"]}
|
||||
assert all(edge["source"] in node_ids and edge["target"] in node_ids for edge in graph["edges"])
|
||||
identity_edges = [edge for edge in graph["edges"] if edge["source_op_id"] >= 1]
|
||||
assert identity_edges and all(edge["source_lane"] == edge["target_lane"] for edge in identity_edges)
|
||||
|
||||
|
||||
def test_collapse_reconstructs_every_boundary(regression_output: Path):
|
||||
graph = project(regression_output, "spatial1_graph", [0, 1, 3, 4])
|
||||
node_ids = {node["display_id"] for node in graph["nodes"]}
|
||||
assert "operation:spatial1_graph:2" in node_ids
|
||||
assert not any(node.get("op_id") == 2 and node["representation"] == "raw" for node in graph["nodes"])
|
||||
assert all(edge["source"] in node_ids and edge["target"] in node_ids for edge in graph["edges"])
|
||||
boundaries = {(edge["source_op_id"], edge["target_op_id"]): (edge["source"], edge["target"])
|
||||
for edge in graph["edges"]
|
||||
if edge["source_lane"] == 0 or edge["source_lane"] is None and edge["target_lane"] == 0}
|
||||
assert boundaries[(1, 2)] == ("gcb:1:0", "operation:spatial1_graph:2")
|
||||
assert boundaries[(2, 3)] == ("operation:spatial1_graph:2", "gcb:3:0")
|
||||
assert boundaries[(0, 1)] == ("gc:0", "gcb:1:0")
|
||||
assert boundaries[(3, 4)] == ("gcb:3:0", "gcb:4:0")
|
||||
assert all(edge["representation"] == "raw" for edge in graph["edges"])
|
||||
|
||||
|
||||
def test_spatial3_includes_isolated_nodes_and_has_distinct_positions(regression_output: Path):
|
||||
graph = project(regression_output, "spatial3_scheduled", view="raw")
|
||||
assert graph["counts"] == {
|
||||
"nodes": 16, "edges": 1, "isolated_nodes": 14,
|
||||
"raw_nodes": 16, "aggregate_nodes": 0, "raw_edges": 1, "aggregate_edges": 0,
|
||||
}
|
||||
nodes = {node["node_id"]: node for node in graph["nodes"]}
|
||||
assert {"scb:1:0", "scb:1:14", "sc:0"} <= nodes.keys()
|
||||
assert len({(node["x"], node["y"]) for node in graph["nodes"]}) == 16
|
||||
assert [(edge["source"], edge["target"]) for edge in graph["edges"]] == [("scb:1:5", "sc:0")]
|
||||
|
||||
|
||||
def test_header_only_edges_preserve_all_nodes(regression_output: Path):
|
||||
graph = project(regression_output, "empty_edges", view="raw")
|
||||
assert graph["counts"]["nodes"] == 3
|
||||
assert graph["counts"]["edges"] == 0
|
||||
assert graph["counts"]["isolated_nodes"] == 3
|
||||
|
||||
|
||||
def test_projection_cap_is_actionable_and_never_partial(regression_output: Path):
|
||||
with pytest.raises(DisplayGraphTooLarge, match="Use an aggregate view or increase"):
|
||||
project(regression_output, "spatial3_scheduled", view="raw", cap=16)
|
||||
client = TestClient(create_app(regression_output, expansion_cap=16))
|
||||
response = client.get("/api/reports/spatial3_scheduled/display-graph?view=raw")
|
||||
assert response.status_code == 400
|
||||
assert "safety cap" in response.json()["detail"] and "aggregate view" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_display_api_and_static_raw_interactions(regression_output: Path):
|
||||
client = TestClient(create_app(regression_output))
|
||||
assert client.get("/api/reports/spatial3_scheduled/display-graph?view=raw").json()["counts"]["nodes"] == 16
|
||||
index = client.get("/").text
|
||||
script = client.get("/app.js").text
|
||||
assert '<option value="raw">Raw instances</option>' in index
|
||||
assert 'report?.stage === "spatial3"' in script
|
||||
assert 'local.representation === "raw"' in script
|
||||
reducer = script[script.index("function reduceEdge"):script.index("function applyFilters")]
|
||||
assert "state.graph.source(id)" not in reducer
|
||||
assert "edge.source === edge.target" in reducer
|
||||
raw_edge_branch = script.index('if (local.representation === "raw")', script.index("async function showEdge"))
|
||||
aggregate_request = script.index("/api/aggregate-edges/", raw_edge_branch)
|
||||
assert "return;" in script[raw_edge_branch:aggregate_request]
|
||||
aggregate_edge = "operation:spatial1_graph:1->2"
|
||||
assert client.get(f"/api/aggregate-edges/{aggregate_edge}/matrix").status_code == 200
|
||||
Reference in New Issue
Block a user