"""Tests for IR-to-JSON conversion (graph_to_json). Tests verify: - dataflow-renderer.AC1.1 (fully allocated graph): Valid dfasm renders as JSON with nodes, edges, metadata - dataflow-renderer.AC1.1 (partially resolved graph): Incomplete programs produce partial JSON with errors - Error nodes flagged: Nodes with errors marked as has_error: true - Function regions: Function regions appear in JSON with correct node_ids - Parse error case: Parse failures produce empty JSON with parse_error string """ from dfgraph.graph_json import graph_to_json from dfgraph.pipeline import run_progressive, PipelineStage class TestFullyAllocatedGraph: """Test graph_to_json on fully allocated programs (stage=allocate).""" def test_simple_const_add_chain_json(self): """AC1.1: Simple CONST→ADD chain renders with full node/edge data.""" source = """\ @system pe=2, sm=0 &c1|pe0 <| const, 3 &c2|pe0 <| const, 7 &result|pe0 <| add &output|pe1 <| pass &c1|pe0 |> &result|pe0:L &c2|pe0 |> &result|pe0:R &result|pe0 |> &output|pe1:L """ result = run_progressive(source) json_out = graph_to_json(result) # Verify stage reached assert result.stage == PipelineStage.ALLOCATE assert json_out["stage"] == "allocate" # Verify nodes are present with all required fields assert len(json_out["nodes"]) >= 4 node_ids = {n["id"] for n in json_out["nodes"]} assert "&c1" in node_ids assert "&c2" in node_ids assert "&result" in node_ids assert "&output" in node_ids # Check a node has full allocation data result_node = next(n for n in json_out["nodes"] if n["id"] == "&result") assert result_node["opcode"] == "add" assert result_node["category"] == "arithmetic" assert result_node["pe"] == 0 assert result_node["iram_offset"] is not None assert result_node["ctx"] is not None assert result_node["has_error"] is False # Verify edges are present assert len(json_out["edges"]) >= 3 edges = json_out["edges"] assert any(e["source"] == "&c1" and e["target"] == "&result" for e in edges) assert any(e["source"] == "&result" and e["target"] == "&output" for e in edges) # Verify metadata assert json_out["metadata"]["pe_count"] == 2 assert json_out["metadata"]["sm_count"] == 0 assert json_out["parse_error"] is None assert len(json_out["errors"]) == 0 def test_sm_program_json(self): """AC1.1: SM operations include sm_id in nodes.""" source = """\ @system pe=2, sm=1 &trigger|pe0 <| const, 1 &reader|pe0 <| read &relay|pe1 <| pass &trigger|pe0 |> &reader|pe0:L &reader|pe0 |> &relay|pe1:L """ result = run_progressive(source) json_out = graph_to_json(result) assert result.stage == PipelineStage.ALLOCATE assert json_out["stage"] == "allocate" # Check nodes reader_node = next(n for n in json_out["nodes"] if n["id"] == "&reader") assert reader_node["opcode"] == "read" assert reader_node["category"] == "memory" assert reader_node["pe"] == 0 assert reader_node["iram_offset"] is not None # Check metadata reflects SM assert json_out["metadata"]["sm_count"] == 1 def test_edge_port_serialization(self): """Edges include source/target ports correctly.""" source = """\ @system pe=1, sm=0 &a|pe0 <| const, 5 &b|pe0 <| add &c|pe0 <| pass &a|pe0 |> &b|pe0:L &b|pe0 |> &c|pe0:R """ result = run_progressive(source) json_out = graph_to_json(result) # Find the edge from a to b edge_ab = next(e for e in json_out["edges"] if e["source"] == "&a" and e["target"] == "&b") assert edge_ab["port"] == "L" # Destination input port # Find the edge from b to c edge_bc = next(e for e in json_out["edges"] if e["source"] == "&b" and e["target"] == "&c") assert edge_bc["port"] == "R" # Destination input port class TestPartiallyResolvedGraph: """Test graph_to_json on graphs with errors (stage=resolve or lower).""" def test_undefined_reference_json(self): """AC1.1: Undefined reference creates error but partial graph still renders.""" source = """\ @system pe=1, sm=0 &a <| const, 5 &b <| add &a |> &b:L &b |> &undefined:R """ result = run_progressive(source) json_out = graph_to_json(result) # Should stop at resolve (error prevents further stages) assert result.stage == PipelineStage.RESOLVE assert json_out["stage"] == "resolve" # But we still get nodes that were lowered node_ids = {n["id"] for n in json_out["nodes"]} assert "&a" in node_ids assert "&b" in node_ids # Nodes should not have allocation data (stopped at resolve) # Note: pe is set by explicit placement, iram_offset and ctx are set by allocate for node in json_out["nodes"]: assert node["iram_offset"] is None assert node["ctx"] is None # Error list should be populated assert len(json_out["errors"]) > 0 assert any("undefined" in e["message"].lower() for e in json_out["errors"]) def test_error_nodes_flagged(self): """Nodes on error lines are flagged with has_error: true.""" source = """\ @system pe=1, sm=0 &a <| const, 5 &b <| add &a |> &b:L &b |> &undefined:R """ result = run_progressive(source) json_out = graph_to_json(result) # The edge referencing undefined will have an error # which means the destination node (if it exists) or source gets flagged assert len(json_out["errors"]) > 0 error_line = json_out["errors"][0]["line"] # Check if any node on this error line is flagged # (The edge error is at the line with &b |> &undefined...) error_flagged = any(n["has_error"] for n in json_out["nodes"]) assert error_flagged or any(e["has_error"] for e in json_out["edges"]) class TestFunctionRegions: """Test function region serialization.""" def test_function_regions_json(self): """Function regions appear in JSON with correct tag, kind, and node_ids.""" source = """\ @system pe=2, sm=0 $func1 |> { &a|pe0 <| const, 1 &b|pe0 <| add &a|pe0 |> &b|pe0:L } $func2 |> { &c|pe1 <| const, 2 } """ result = run_progressive(source) json_out = graph_to_json(result) # Check regions regions = json_out["regions"] assert len(regions) >= 2 # Find function regions by tag func1_region = next((r for r in regions if r["tag"] == "$func1"), None) func2_region = next((r for r in regions if r["tag"] == "$func2"), None) assert func1_region is not None assert func1_region["kind"] == "function" assert set(func1_region["node_ids"]) == {"$func1.&a", "$func1.&b"} assert func2_region is not None assert func2_region["kind"] == "function" assert set(func2_region["node_ids"]) == {"$func2.&c"} class TestParseErrorCase: """Test graph_to_json on parse failures.""" def test_parse_error_json(self): """Parse error produces empty JSON with parse_error string.""" source = """\ @system pe=1, sm=0 &invalid syntax [[[ """ result = run_progressive(source) json_out = graph_to_json(result) # Should be in parse error stage assert result.stage == PipelineStage.PARSE_ERROR assert json_out["stage"] == "parse_error" # Should have no nodes/edges assert json_out["nodes"] == [] assert json_out["edges"] == [] assert json_out["regions"] == [] # Should have parse_error string assert json_out["parse_error"] is not None assert len(json_out["parse_error"]) > 0 # metadata should have zeros assert json_out["metadata"]["pe_count"] == 0 assert json_out["metadata"]["sm_count"] == 0 class TestJsonStructure: """Test JSON output structure compliance.""" def test_node_structure(self): """Each node has all required fields.""" source = """\ @system pe=1, sm=0 &a|pe0 <| const, 5 """ result = run_progressive(source) json_out = graph_to_json(result) node = json_out["nodes"][0] required_fields = {"id", "opcode", "category", "colour", "const", "pe", "iram_offset", "ctx", "has_error", "loc"} assert set(node.keys()) >= required_fields # Location should have required fields loc = node["loc"] assert "line" in loc assert "column" in loc def test_edge_structure(self): """Each edge has all required fields.""" source = """\ @system pe=1, sm=0 &a|pe0 <| const, 5 &b|pe0 <| add &a|pe0 |> &b|pe0:L """ result = run_progressive(source) json_out = graph_to_json(result) edge = json_out["edges"][0] required_fields = {"source", "target", "port", "source_port", "has_error"} assert set(edge.keys()) >= required_fields def test_error_structure(self): """Each error has required fields.""" source = """\ @system pe=1, sm=0 &a|pe0 <| const, 5 &b|pe0 |> &undefined|pe0:L """ result = run_progressive(source) json_out = graph_to_json(result) assert len(json_out["errors"]) > 0, "Expected errors for undefined reference" error = json_out["errors"][0] required_fields = {"line", "column", "category", "message", "suggestions"} assert set(error.keys()) >= required_fields def test_metadata_structure(self): """Metadata has all required fields.""" source = "@system pe=2, sm=1" result = run_progressive(source) json_out = graph_to_json(result) metadata = json_out["metadata"] required_fields = {"stage", "pe_count", "sm_count"} assert set(metadata.keys()) >= required_fields assert metadata["pe_count"] == 2 assert metadata["sm_count"] == 1 def test_top_level_structure(self): """Top-level JSON has all required fields.""" source = "@system pe=1, sm=0" result = run_progressive(source) json_out = graph_to_json(result) required_fields = {"type", "stage", "nodes", "edges", "regions", "errors", "parse_error", "metadata"} assert set(json_out.keys()) >= required_fields assert json_out["type"] == "graph_update" class TestEmptyProgram: """Test minimal/empty programs.""" def test_system_only_json(self): """Program with only @system pragma produces valid JSON.""" source = "@system pe=2, sm=1" result = run_progressive(source) json_out = graph_to_json(result) assert json_out["stage"] == "allocate" assert json_out["nodes"] == [] assert json_out["edges"] == [] assert json_out["metadata"]["pe_count"] == 2 assert json_out["metadata"]["sm_count"] == 1 class TestColourMapping: """Test opcode-to-colour mapping.""" def test_arithmetic_colour(self): """Arithmetic ops get correct colour.""" source = """\ @system pe=1, sm=0 &add_node|pe0 <| add """ result = run_progressive(source) json_out = graph_to_json(result) node = next(n for n in json_out["nodes"] if n["id"] == "&add_node") assert node["category"] == "arithmetic" assert node["colour"] == "#4a90d9" # arithmetic blue def test_memory_colour(self): """Memory ops get correct colour.""" source = """\ @system pe=1, sm=0 &read_node|pe0 <| read """ result = run_progressive(source) json_out = graph_to_json(result) node = next(n for n in json_out["nodes"] if n["id"] == "&read_node") assert node["category"] == "memory" assert node["colour"] == "#ff5722" # memory red def test_routing_colour(self): """Routing ops get correct colour.""" source = """\ @system pe=1, sm=0 &pass_node|pe0 <| pass """ result = run_progressive(source) json_out = graph_to_json(result) node = next(n for n in json_out["nodes"] if n["id"] == "&pass_node") assert node["category"] == "routing" assert node["colour"] == "#9c27b0" # routing purple class TestMultipleRegions: """Test handling of nested and multiple regions.""" def test_multiple_functions_and_locations(self): """Multiple function and location regions handled correctly.""" source = """\ @system pe=2, sm=0 $func1 |> { &a|pe0 <| const, 1 } @loc1 $func2 |> { &c|pe0 <| add } """ result = run_progressive(source) json_out = graph_to_json(result) # Only FUNCTION regions should appear in output regions = json_out["regions"] function_regions = [r for r in regions if r["kind"] == "function"] # Should have 2 function regions assert len(function_regions) == 2 tags = {r["tag"] for r in function_regions} assert "$func1" in tags assert "$func2" in tags