"""Tests for the progressive pipeline runner (dfgraph/pipeline.py). Tests verify: - dataflow-renderer.AC2.1: Clean source produces allocate-stage graph with zero errors - dataflow-renderer.AC2.2: Source with errors at any stage still returns a graph with valid nodes - dataflow-renderer.AC5.2: Error results contain AssemblyError objects with loc.line, category, and message - dataflow-renderer.AC5.3: Name resolution errors include suggestions (Levenshtein) - Parse errors are handled gracefully and return PARSE_ERROR stage with graph=None """ from textwrap import dedent from dfgraph.pipeline import run_progressive, PipelineStage, PipelineResult from asm.errors import ErrorCategory class TestProgressivePipelineCleanSource: """Tests for AC2.1: Clean source produces allocate-stage graph with zero errors.""" def test_simple_const_add_program(self): """Clean program with const and add nodes produces ALLOCATE stage with no errors.""" source = dedent("""\ @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) assert result.stage == PipelineStage.ALLOCATE assert len(result.errors) == 0 assert result.graph is not None assert len(result.graph.nodes) >= 4 def test_program_with_functions(self): """Clean program with function-scoped labels produces ALLOCATE stage with no errors.""" source = dedent("""\ @system pe=2, sm=0 $main |> { &input <| const, 42 &process <| pass &input |> &process:L } """) result = run_progressive(source) assert result.stage == PipelineStage.ALLOCATE assert len(result.errors) == 0 assert result.graph is not None def test_program_with_sm_operations(self): """Clean program with SM operations produces ALLOCATE stage with no errors.""" source = dedent("""\ @system pe=2, sm=1 @val|sm0:5 = 0x42 &trigger|pe0 <| const, 1 &reader|pe0 <| read, 5 &relay|pe1 <| pass &trigger|pe0 |> &reader|pe0:L &reader|pe0 |> &relay|pe1:L """) result = run_progressive(source) assert result.stage == PipelineStage.ALLOCATE assert len(result.errors) == 0 assert result.graph is not None def test_program_with_cross_pe_routing(self): """Clean program with cross-PE routing produces ALLOCATE stage with no errors.""" source = dedent("""\ @system pe=3, sm=0 &source|pe0 <| const, 99 &dest|pe1 <| pass &output|pe2 <| pass &source|pe0 |> &dest|pe1:L &dest|pe1 |> &output|pe2:L """) result = run_progressive(source) assert result.stage == PipelineStage.ALLOCATE assert len(result.errors) == 0 assert result.graph is not None class TestProgressivePipelineResolveErrors: """Tests for AC2.2: Source with resolve errors still returns graph at RESOLVE stage.""" def test_undefined_name_reference_stops_at_resolve(self): """Program with undefined node reference produces graph at RESOLVE stage.""" source = dedent("""\ @system pe=2, sm=0 &a <| const, 42 &b <| add &a |> &undefined:L """) result = run_progressive(source) assert result.stage == PipelineStage.RESOLVE assert len(result.errors) > 0 assert result.graph is not None assert len(result.graph.nodes) >= 2 def test_name_error_has_category(self): """NAME error contains ErrorCategory.NAME.""" source = dedent("""\ @system pe=2, sm=0 &a <| pass &b <| add &a |> &undefined:L """) result = run_progressive(source) assert result.stage == PipelineStage.RESOLVE name_errors = [e for e in result.errors if e.category == ErrorCategory.NAME] assert len(name_errors) > 0 def test_name_error_has_suggestions(self): """NAME error includes suggestions when available (AC5.3).""" source = dedent("""\ @system pe=2, sm=0 &nonexistent <| pass &a <| add &a |> &nonexistant:L """) result = run_progressive(source) assert result.stage == PipelineStage.RESOLVE name_errors = [e for e in result.errors if e.category == ErrorCategory.NAME] assert len(name_errors) > 0 error = name_errors[0] # Levenshtein suggestions should be present assert len(error.suggestions) > 0 def test_scope_violation_stops_at_resolve(self): """Program with scope violation produces graph at RESOLVE stage.""" source = dedent("""\ @system pe=2, sm=0 $foo |> { &private <| pass } &top <| add &top |> &private:L """) result = run_progressive(source) assert result.stage == PipelineStage.RESOLVE assert len(result.errors) > 0 scope_errors = [e for e in result.errors if e.category == ErrorCategory.SCOPE] assert len(scope_errors) > 0 class TestProgressivePipelinePlacementErrors: """Tests for AC2.2: Source with placement errors returns graph at PLACE stage.""" def test_explicit_placement_violation_stops_at_place(self): """Program with invalid explicit PE placement produces graph at PLACE stage.""" source = dedent("""\ @system pe=2, sm=0 &a|pe5 <| const, 1 &b <| add &a |> &b:L """) result = run_progressive(source) assert result.stage == PipelineStage.PLACE assert len(result.errors) > 0 assert result.graph is not None # Should have placement errors placement_errors = [e for e in result.errors if e.category == ErrorCategory.PLACEMENT] assert len(placement_errors) > 0 class TestProgressivePipelineErrorMetadata: """Tests for AC5.2: Errors contain loc.line, category, and message fields.""" def test_error_contains_source_location(self): """Error contains SourceLoc with line and column.""" source = dedent("""\ @system pe=2, sm=0 &a <| pass &b <| add &a |> &undefined:L """) result = run_progressive(source) assert len(result.errors) > 0 error = result.errors[0] assert error.loc.line > 0 assert error.loc.column >= 0 def test_error_contains_category(self): """Error contains an ErrorCategory enum value.""" source = dedent("""\ @system pe=2, sm=0 &a <| pass &b <| add &a |> &undefined:L """) result = run_progressive(source) assert len(result.errors) > 0 error = result.errors[0] assert isinstance(error.category, ErrorCategory) def test_error_contains_message(self): """Error contains a non-empty message string.""" source = dedent("""\ @system pe=2, sm=0 &a <| pass &b <| add &a |> &undefined:L """) result = run_progressive(source) assert len(result.errors) > 0 error = result.errors[0] assert len(error.message) > 0 assert isinstance(error.message, str) class TestProgressivePipelineParseErrors: """Tests for parse errors: invalid syntax returns PARSE_ERROR stage.""" def test_invalid_syntax_returns_parse_error(self): """Syntactically invalid source returns PARSE_ERROR stage.""" source = "this is not valid dfasm @ @@ {{{ syntax" result = run_progressive(source) assert result.stage == PipelineStage.PARSE_ERROR assert result.graph is None assert result.parse_error is not None assert isinstance(result.parse_error, str) assert len(result.parse_error) > 0 def test_parse_error_no_errors_list(self): """PARSE_ERROR result has empty errors list (error is in parse_error field).""" source = "@@@" result = run_progressive(source) assert result.stage == PipelineStage.PARSE_ERROR assert len(result.errors) == 0 class TestProgressivePipelineResultDataclass: """Tests for PipelineResult dataclass structure.""" def test_result_is_frozen_dataclass(self): """PipelineResult is a frozen dataclass.""" source = "@system pe=1, sm=0\n&a <| pass" result = run_progressive(source) assert isinstance(result, PipelineResult) # Should be frozen (immutable) try: result.stage = PipelineStage.LOWER assert False, "PipelineResult should be frozen" except (AttributeError, ValueError): pass # Expected def test_result_stage_is_enum(self): """result.stage is a PipelineStage enum value.""" source = "@system pe=1, sm=0\n&a <| pass" result = run_progressive(source) assert isinstance(result.stage, PipelineStage) def test_result_errors_is_list(self): """result.errors is a list of AssemblyError objects.""" source = "@system pe=2, sm=0\n&a <| pass\n&b <| add\n&a |> &undefined:L" result = run_progressive(source) assert isinstance(result.errors, list) if len(result.errors) > 0: from asm.errors import AssemblyError assert all(isinstance(e, AssemblyError) for e in result.errors) class TestProgressivePipelineGraphStructure: """Tests for IRGraph structure returned at each stage.""" def test_graph_at_lower_stage_has_nodes(self): """IRGraph at LOWER stage contains IRNodes.""" source = dedent("""\ @system pe=1, sm=0 &a <| const, 42 &b <| pass """) result = run_progressive(source) assert result.graph is not None assert len(result.graph.nodes) > 0 assert all(name for name in result.graph.nodes.keys()) def test_graph_at_resolve_stage_has_nodes(self): """IRGraph at RESOLVE stage contains IRNodes.""" source = dedent("""\ @system pe=2, sm=0 &a <| pass &b <| add &a |> &undefined:L """) result = run_progressive(source) assert result.stage == PipelineStage.RESOLVE assert result.graph is not None assert len(result.graph.nodes) > 0 def test_graph_at_allocate_stage_has_offsets(self): """IRGraph at ALLOCATE stage has IRAM offsets on nodes.""" source = dedent("""\ @system pe=2, sm=0 &a <| const, 42 &b <| pass &a |> &b:L """) result = run_progressive(source) assert result.stage == PipelineStage.ALLOCATE # After allocation, nodes should have iram_offset set for node in result.graph.nodes.values(): assert node.iram_offset is not None class TestProgressivePipelineEmptySource: """Tests for edge cases.""" def test_empty_source_parses_successfully(self): """Empty source parses and returns ALLOCATE stage with default @system.""" source = "" result = run_progressive(source) # Empty source parses successfully and creates a default @system pragma assert result.stage == PipelineStage.ALLOCATE assert result.graph is not None # Should have default system config (pe=1, sm=1) assert result.graph.system is not None def test_system_pragma_only(self): """Source with only @system pragma parses successfully.""" source = "@system pe=2, sm=0" result = run_progressive(source) assert result.stage == PipelineStage.ALLOCATE assert len(result.errors) == 0 assert result.graph is not None