Files
60eac7f1d4 feat(compiler): add Compiler Result wrapper and diagnostic messages (#4) (#48)
Implement the uniform result wrapper every stage returns (MASTER_SPEC §8.6, §21),
in compiler/common/result.py.

- Severity: StrEnum with the four levels (Information/Warning/Error/Fatal, §21.1).
- Message: immutable, machine-readable diagnostic (code, severity, title,
  description, optional read-only context) with lossless to_json/from_json.
- CompilerResult: immutable wrapper (data, warnings, errors, metadata) with
  success/has_warnings properties and functional add_warning/add_error/merge
  returning new instances. merge concatenates warnings then errors
  deterministically, takes the downstream data, and unions metadata.
- to_json serializes stage data via its own to_json when available.

Tests: construction, message round-trip, severity handling, immutability,
deterministic merge, and machine-readable serialization.

Co-authored-by: kunail0804 <kunail0804@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 19:07:25 +02:00

184 lines
5.2 KiB
Python

"""Tests for the Compiler Result wrapper (issue #4, MASTER_SPEC §8.6, §21)."""
from __future__ import annotations
import dataclasses
import pytest
from compiler.common.result import CompilerResult, Message, Severity
# --- Severity --------------------------------------------------------------
def test_all_four_severity_levels_exist() -> None:
assert {s.value for s in Severity} == {"information", "warning", "error", "fatal"}
# --- Message ---------------------------------------------------------------
def sample_message(code: str = "SC0001", severity: Severity = Severity.WARNING) -> Message:
return Message(
code=code,
severity=severity,
title="Unknown concept",
description="Concept 'foo' is not in the Knowledge Base.",
)
def test_message_construction() -> None:
m = sample_message()
assert m.code == "SC0001"
assert m.severity is Severity.WARNING
assert m.context == {}
def test_message_with_context() -> None:
m = Message(
code="SC0002",
severity=Severity.ERROR,
title="Missing field",
description="Field 'metadata' is required.",
context={"field": "metadata", "stage": "validator"},
)
assert m.context["field"] == "metadata"
def test_message_roundtrip_machine_readable() -> None:
m = sample_message()
data = m.to_json()
assert data["severity"] == "warning"
assert Message.from_json(data) == m
def test_message_context_roundtrip() -> None:
m = Message(
code="SC0002",
severity=Severity.ERROR,
title="t",
description="d",
context={"stage": "resolver"},
)
assert Message.from_json(m.to_json()) == m
def test_message_is_immutable() -> None:
with pytest.raises(dataclasses.FrozenInstanceError):
sample_message().code = "X" # type: ignore[misc]
def test_message_context_is_read_only() -> None:
m = Message(
code="SC0002",
severity=Severity.ERROR,
title="t",
description="d",
context={"stage": "resolver"},
)
with pytest.raises(TypeError):
m.context["stage"] = "other" # type: ignore[index]
# --- CompilerResult construction -------------------------------------------
def test_empty_result_defaults() -> None:
r = CompilerResult()
assert r.data is None
assert r.warnings == ()
assert r.errors == ()
assert r.success is True
assert r.has_warnings is False
def test_result_carries_data() -> None:
r = CompilerResult(data={"scene": True})
assert r.data == {"scene": True}
# --- add_warning / add_error (functional, immutable) -----------------------
def test_add_warning_returns_new_result() -> None:
original = CompilerResult(data="x")
warned = original.add_warning(sample_message())
assert original.warnings == () # unchanged
assert len(warned.warnings) == 1
assert warned.data == "x"
assert warned.success is True
assert warned.has_warnings is True
def test_add_error_marks_failure() -> None:
r = CompilerResult().add_error(sample_message("SC0100", Severity.ERROR))
assert r.success is False
assert len(r.errors) == 1
def test_fatal_is_stored_as_error_and_fails() -> None:
r = CompilerResult().add_error(sample_message("SC0900", Severity.FATAL))
assert r.success is False
assert r.errors[0].severity is Severity.FATAL
# --- merge -----------------------------------------------------------------
def test_merge_concatenates_deterministically() -> None:
a = (
CompilerResult(data="a")
.add_warning(sample_message("SC0001"))
.add_error(sample_message("SC0101", Severity.ERROR))
)
b = (
CompilerResult(data="b")
.add_warning(sample_message("SC0002"))
.add_error(sample_message("SC0102", Severity.ERROR))
)
merged = a.merge(b)
assert [w.code for w in merged.warnings] == ["SC0001", "SC0002"]
assert [e.code for e in merged.errors] == ["SC0101", "SC0102"]
assert merged.success is False
def test_merge_takes_downstream_data() -> None:
a = CompilerResult(data="a")
b = CompilerResult(data="b")
assert a.merge(b).data == "b"
def test_merge_combines_metadata() -> None:
a = CompilerResult(metadata={"stage": "validator", "count": 1})
b = CompilerResult(metadata={"stage": "resolver", "extra": 2})
merged = a.merge(b)
assert merged.metadata == {"stage": "resolver", "count": 1, "extra": 2}
def test_merge_does_not_mutate_operands() -> None:
a = CompilerResult().add_warning(sample_message("SC0001"))
b = CompilerResult().add_warning(sample_message("SC0002"))
a.merge(b)
assert [w.code for w in a.warnings] == ["SC0001"]
assert [w.code for w in b.warnings] == ["SC0002"]
# --- serialization ---------------------------------------------------------
def test_result_to_json_is_machine_readable() -> None:
r = CompilerResult(data="x").add_warning(sample_message())
data = r.to_json()
assert data["warnings"][0]["code"] == "SC0001"
assert data["errors"] == []
assert data["success"] is True
def test_result_to_json_serializes_model_data() -> None:
class FakeModel:
def to_json(self) -> dict:
return {"kind": "scene"}
r = CompilerResult(data=FakeModel())
assert r.to_json()["data"] == {"kind": "scene"}