mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-13 04:26:28 +03:00
303 lines
11 KiB
Python
303 lines
11 KiB
Python
"""Unit tests for JSON serialization schemas and helpers.
|
|
|
|
Validates contract assertions:
|
|
- VAL-JSON-003: Addresses use canonical structured format
|
|
- VAL-JSON-004: All size fields are integer bytes
|
|
- VAL-JSON-006: Unknown/null fields serialize as JSON null
|
|
- VAL-JSON-013: Enum values serialize as UPPER_CASE strings
|
|
- VAL-JSON-018: Entity objects contain only canonical fields
|
|
- VAL-STRUCT-017: Strings with embedded quotes and backslashes escaped
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
_scripts_dir = Path(__file__).resolve().parents[2] / "scripts"
|
|
if str(_scripts_dir) not in sys.path:
|
|
sys.path.insert(0, str(_scripts_dir))
|
|
|
|
import json
|
|
|
|
from binary_analysis.domain.entities import (
|
|
Address,
|
|
Function,
|
|
Section,
|
|
)
|
|
from binary_analysis.domain.enums import (
|
|
Confidence,
|
|
DiagnosticSeverity,
|
|
FunctionNameSource,
|
|
ProjectState,
|
|
ReferenceKind,
|
|
)
|
|
from binary_analysis.domain.schemas import (
|
|
FUNCTION_CANONICAL_FIELDS,
|
|
canonical_address,
|
|
deserialize_address,
|
|
entity_to_dict,
|
|
safe_json_dumps,
|
|
serialize_address,
|
|
serialize_enum,
|
|
)
|
|
|
|
|
|
class TestAddressSerialization:
|
|
"""Tests for address serialization (VAL-JSON-003)."""
|
|
|
|
def test_serialize_address_dict(self) -> None:
|
|
"""serialize_address returns a structured dict."""
|
|
addr = Address(space="ram", offset="0x4018d0", display="0x4018d0", file_offset=6352)
|
|
result = serialize_address(addr)
|
|
assert result is not None
|
|
assert result["space"] == "ram"
|
|
assert result["offset"] == "0x4018d0"
|
|
assert result["display"] == "0x4018d0"
|
|
assert result["file_offset"] == 6352
|
|
|
|
def test_serialize_address_null(self) -> None:
|
|
"""serialize_address returns None for null input (VAL-JSON-006)."""
|
|
result = serialize_address(None)
|
|
assert result is None
|
|
|
|
def test_serialize_address_no_file_offset(self) -> None:
|
|
"""Optional file_offset is omitted when absent."""
|
|
addr = Address(space="ram", offset="0x401000", display="0x401000")
|
|
result = serialize_address(addr)
|
|
assert result is not None
|
|
assert "file_offset" not in result
|
|
|
|
def test_deserialize_address(self) -> None:
|
|
data = {"space": "ram", "offset": "0x401000", "display": "0x401000"}
|
|
addr = deserialize_address(data)
|
|
assert addr is not None
|
|
assert addr.space == "ram"
|
|
assert addr.offset == "0x401000"
|
|
|
|
def test_deserialize_address_null(self) -> None:
|
|
"""deserialize_address returns None for null input."""
|
|
result = deserialize_address(None)
|
|
assert result is None
|
|
|
|
def test_canonical_address_factory(self) -> None:
|
|
"""canonical_address factory creates valid addresses."""
|
|
addr = canonical_address("ram", "0x401000")
|
|
assert addr.space == "ram"
|
|
assert addr.offset == "0x401000"
|
|
assert addr.display == "0x401000"
|
|
|
|
def test_canonical_address_adds_0x_prefix(self) -> None:
|
|
"""canonical_address adds 0x prefix if missing."""
|
|
addr = canonical_address("ram", "401000")
|
|
assert addr.offset == "0x401000"
|
|
|
|
def test_address_in_json(self) -> None:
|
|
"""Address must serialize as a structured object in JSON,
|
|
never a bare string or integer (VAL-JSON-003)."""
|
|
addr = Address(space="ram", offset="0x4018d0", display="0x4018d0")
|
|
d = addr.to_dict()
|
|
raw = json.dumps(d)
|
|
# Must be an object, not a bare string
|
|
assert raw.startswith("{")
|
|
assert '"space"' in raw
|
|
assert '"offset"' in raw
|
|
assert '"display"' in raw
|
|
|
|
|
|
class TestEnumSerialization:
|
|
"""Tests for enum serialization (VAL-JSON-013)."""
|
|
|
|
def test_serialize_enum_to_string(self) -> None:
|
|
"""Enums serialize as UPPER_CASE strings."""
|
|
assert serialize_enum(ProjectState.CREATED) == "CREATED"
|
|
assert serialize_enum(Confidence.HIGH) == "HIGH"
|
|
assert serialize_enum(DiagnosticSeverity.WARNING) == "WARNING"
|
|
assert serialize_enum(ReferenceKind.CALL) == "CALL"
|
|
assert serialize_enum(FunctionNameSource.ORIGINAL) == "ORIGINAL"
|
|
|
|
def test_serialize_enum_null(self) -> None:
|
|
"""Null enum serializes as None (JSON null)."""
|
|
assert serialize_enum(None) is None
|
|
|
|
def test_serialize_enum_string_passthrough(self) -> None:
|
|
"""String input is uppercased."""
|
|
assert serialize_enum("high") == "HIGH"
|
|
|
|
def test_enum_json_output(self) -> None:
|
|
"""Verify enum values in JSON are quoted UPPER_CASE strings."""
|
|
data = {"state": ProjectState.CREATED.value, "confidence": Confidence.HIGH.value}
|
|
raw = json.dumps(data)
|
|
assert '"CREATED"' in raw
|
|
assert '"HIGH"' in raw
|
|
# Must NOT be integer ordinals
|
|
assert "0" not in raw.split('"CREATED"')[0]
|
|
|
|
|
|
class TestEntitySerialization:
|
|
"""Tests for entity-to-dict serialization."""
|
|
|
|
def test_entity_to_dict_basic(self) -> None:
|
|
"""entity_to_dict converts a dataclass to a plain dict."""
|
|
f = Function(name="main", size_bytes=256, confidence=Confidence.HIGH)
|
|
d = entity_to_dict(f)
|
|
assert d["name"] == "main"
|
|
assert d["size_bytes"] == 256
|
|
assert d["confidence"] == "HIGH"
|
|
|
|
def test_address_in_entity_serialized_as_dict(self) -> None:
|
|
"""Address fields in entities must be structured dicts."""
|
|
addr = Address(space="ram", offset="0x401000", display="0x401000")
|
|
f = Function(name="main", address=addr, size_bytes=128)
|
|
d = entity_to_dict(f)
|
|
assert isinstance(d["address"], dict)
|
|
assert d["address"]["space"] == "ram"
|
|
|
|
def test_null_address_serializes_as_null(self) -> None:
|
|
"""Null address field must serialize as JSON null (VAL-JSON-006)."""
|
|
f = Function(name="main")
|
|
d = entity_to_dict(f)
|
|
assert d["address"] is None
|
|
|
|
def test_size_fields_are_ints(self) -> None:
|
|
"""All size fields must be JSON numbers (integers), never strings (VAL-JSON-004)."""
|
|
f = Function(name="main", size_bytes=4096)
|
|
d = entity_to_dict(f)
|
|
assert isinstance(d["size_bytes"], int)
|
|
assert d["size_bytes"] == 4096
|
|
|
|
def test_canonical_fields_restriction(self) -> None:
|
|
"""entity_to_dict with canonical_fields restricts output (VAL-JSON-018)."""
|
|
f = Function(
|
|
name="main",
|
|
address=Address(space="ram", offset="0x401000", display="0x401000"),
|
|
size_bytes=256,
|
|
confidence=Confidence.HIGH,
|
|
name_source=FunctionNameSource.ORIGINAL,
|
|
is_external=True,
|
|
)
|
|
d = entity_to_dict(f, canonical_fields={"name", "address", "size_bytes"})
|
|
assert set(d.keys()) == {"name", "address", "size_bytes"}
|
|
assert "confidence" not in d
|
|
assert "name_source" not in d
|
|
assert "is_external" not in d
|
|
|
|
def test_function_canonical_fields_match_expected(self) -> None:
|
|
"""Verify the function canonical field list covers key fields."""
|
|
assert "name" in FUNCTION_CANONICAL_FIELDS
|
|
assert "address" in FUNCTION_CANONICAL_FIELDS
|
|
assert "size_bytes" in FUNCTION_CANONICAL_FIELDS
|
|
assert "confidence" in FUNCTION_CANONICAL_FIELDS
|
|
assert "name_source" in FUNCTION_CANONICAL_FIELDS
|
|
assert "is_external" in FUNCTION_CANONICAL_FIELDS
|
|
assert "is_thunk" in FUNCTION_CANONICAL_FIELDS
|
|
|
|
|
|
class TestSafeJsonDumps:
|
|
"""Tests for JSON escaping (VAL-STRUCT-017)."""
|
|
|
|
def test_valid_json_output(self) -> None:
|
|
"""safe_json_dumps produces valid parseable JSON."""
|
|
data = {"message": "hello world", "count": 42}
|
|
raw = safe_json_dumps(data)
|
|
parsed = json.loads(raw)
|
|
assert parsed == data
|
|
|
|
def test_embedded_quotes_escaped(self) -> None:
|
|
"""Strings with embedded double quotes must be escaped (VAL-STRUCT-017)."""
|
|
text = 'He said "hello"'
|
|
data = {"text": text}
|
|
raw = safe_json_dumps(data)
|
|
parsed = json.loads(raw)
|
|
assert parsed["text"] == text
|
|
|
|
def test_embedded_backslashes_escaped(self) -> None:
|
|
"""Strings with backslashes must be escaped (VAL-STRUCT-017)."""
|
|
text = "C:\\path\\to\\file"
|
|
data = {"path": text}
|
|
raw = safe_json_dumps(data)
|
|
parsed = json.loads(raw)
|
|
assert parsed["path"] == text
|
|
|
|
def test_combined_quotes_and_backslashes(self) -> None:
|
|
"""Combined quotes and backslashes must be escaped."""
|
|
text = 'file "test" at C:\\dir\\file.txt'
|
|
data = {"text": text}
|
|
raw = safe_json_dumps(data)
|
|
parsed = json.loads(raw)
|
|
assert parsed["text"] == text
|
|
|
|
def test_control_characters_escaped(self) -> None:
|
|
"""Control characters must be escaped."""
|
|
text = "line1\nline2\tindented"
|
|
data = {"text": text}
|
|
raw = safe_json_dumps(data)
|
|
parsed = json.loads(raw)
|
|
assert parsed["text"] == text
|
|
# Verify the raw JSON contains escape sequences
|
|
assert "\\n" in raw
|
|
assert "\\t" in raw
|
|
|
|
def test_unicode_preserved(self) -> None:
|
|
"""Unicode characters should be preserved by default (ensure_ascii=False)."""
|
|
text = "こんにちは"
|
|
data = {"text": text}
|
|
raw = safe_json_dumps(data)
|
|
parsed = json.loads(raw)
|
|
assert parsed["text"] == text
|
|
|
|
def test_ascii_mode_escapes_unicode(self) -> None:
|
|
"""ensure_ascii=True should escape non-ASCII characters."""
|
|
text = "こんにちは"
|
|
data = {"text": text}
|
|
raw = safe_json_dumps(data, ensure_ascii=True)
|
|
parsed = json.loads(raw)
|
|
assert parsed["text"] == text
|
|
# In ASCII mode, non-ASCII chars are \u-escaped
|
|
assert "こ" not in raw
|
|
|
|
|
|
class TestNullSerialization:
|
|
"""Tests for null serialization (VAL-JSON-006)."""
|
|
|
|
def test_none_serializes_as_null(self) -> None:
|
|
"""Python None must serialize as JSON null, never "" or 0."""
|
|
data = {"value": None}
|
|
raw = json.dumps(data)
|
|
assert '"value": null' in raw
|
|
|
|
def test_optional_entity_field_null(self) -> None:
|
|
"""Optional entity fields with None serialize as null."""
|
|
f = Function(name="main", signature=None)
|
|
d = entity_to_dict(f)
|
|
assert d["signature"] is None
|
|
|
|
def test_null_not_empty_string(self) -> None:
|
|
"""Null must not serialize as empty string."""
|
|
data = {"compiler": None}
|
|
raw = json.dumps(data)
|
|
assert '""' not in raw
|
|
|
|
def test_null_not_zero(self) -> None:
|
|
"""Null must not serialize as zero."""
|
|
data = {"entry_point": None}
|
|
raw = json.dumps(data)
|
|
# Entry point should be null, not 0
|
|
parsed = json.loads(raw)
|
|
assert parsed["entry_point"] is None
|
|
|
|
|
|
class TestSizeFieldIntegers:
|
|
"""Tests for size field serialization (VAL-JSON-004)."""
|
|
|
|
def test_sizes_not_strings(self) -> None:
|
|
"""Size fields must NEVER be strings."""
|
|
f = Function(name="main", size_bytes=4096)
|
|
d = entity_to_dict(f)
|
|
assert isinstance(d["size_bytes"], int)
|
|
|
|
def test_simple_section_sizes(self) -> None:
|
|
s = Section(name=".text", virtual_size=8192, raw_size=4096)
|
|
d = entity_to_dict(s)
|
|
assert isinstance(d["virtual_size"], int)
|
|
assert isinstance(d["raw_size"], int)
|