Files
magnus919_agent-skills/binary-analysis/tests/unit/test_entities.py
T

349 lines
11 KiB
Python

"""Unit tests for domain entities and the Address type."""
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 uuid import UUID, uuid4
import pytest
from binary_analysis.domain.entities import (
Address,
AuditEvent,
BasicBlock,
Binary,
CallGraph,
Capability,
Diagnostic,
EntryPoint,
Export,
Function,
Heuristic,
Import,
Inference,
Instruction,
Observation,
Project,
Reference,
Report,
Section,
String,
Symbol,
Unknown,
)
from binary_analysis.domain.enums import (
Confidence,
FunctionNameSource,
ProjectState,
)
class TestAddress:
"""Tests for the canonical Address type."""
def test_create_address_minimal(self) -> None:
addr = Address(space="ram", offset="0x401000", display="0x401000")
assert addr.space == "ram"
assert addr.offset == "0x401000"
assert addr.display == "0x401000"
assert addr.file_offset is None
def test_create_address_with_file_offset(self) -> None:
addr = Address(space="ram", offset="0x401000", display="0x401000", file_offset=6352)
assert addr.file_offset == 6352
def test_address_is_frozen(self) -> None:
addr = Address(space="ram", offset="0x401000", display="0x401000")
with pytest.raises(AttributeError):
addr.space = "other" # type: ignore[misc]
def test_offset_must_start_with_0x(self) -> None:
with pytest.raises(ValueError, match="must start with '0x'"):
Address(space="ram", offset="401000", display="401000")
def test_to_dict_minimal(self) -> None:
addr = Address(space="ram", offset="0x401000", display="0x401000")
d = addr.to_dict()
assert d["space"] == "ram"
assert d["offset"] == "0x401000"
assert d["display"] == "0x401000"
assert "file_offset" not in d # omitted when None (VAL-JSON-003)
def test_to_dict_with_file_offset(self) -> None:
addr = Address(space="ram", offset="0x401000", display="0x401000", file_offset=6352)
d = addr.to_dict()
assert d["file_offset"] == 6352
def test_to_dict_file_offset_is_integer(self) -> None:
"""file_offset must be an integer, not a string (VAL-JSON-003)."""
addr = Address(space="ram", offset="0x401000", display="0x401000", file_offset=6352)
d = addr.to_dict()
assert isinstance(d["file_offset"], int)
def test_from_dict(self) -> None:
data = {"space": "ram", "offset": "0x401000", "display": "0x401000", "file_offset": 100}
addr = Address.from_dict(data)
assert addr.space == "ram"
assert addr.offset == "0x401000"
assert addr.display == "0x401000"
assert addr.file_offset == 100
def test_from_dict_minimal(self) -> None:
data = {"space": "ram", "offset": "0x401000", "display": "0x401000"}
addr = Address.from_dict(data)
assert addr.file_offset is None
def test_to_dict_serializable_to_json(self) -> None:
"""Address.to_dict() must produce JSON-serializable output."""
addr = Address(space="ram", offset="0x4018d0", display="0x4018d0", file_offset=6352)
d = addr.to_dict()
raw = json.dumps(d)
parsed = json.loads(raw)
assert parsed["space"] == "ram"
assert parsed["offset"] == "0x4018d0"
assert parsed["file_offset"] == 6352
def test_address_equality(self) -> None:
a1 = Address(space="ram", offset="0x401000", display="0x401000")
a2 = Address(space="ram", offset="0x401000", display="0x401000")
a3 = Address(space="ram", offset="0x402000", display="0x402000")
assert a1 == a2
assert a1 != a3
class TestProjectEntity:
"""Tests for Project entity."""
def test_default_project(self) -> None:
p = Project()
assert isinstance(p.id, UUID)
assert p.name == ""
assert p.state == ProjectState.CREATED
assert p.binary_count == 0
assert p.is_stale is False
assert p.lock is None
def test_custom_project(self) -> None:
pid = uuid4()
p = Project(
id=pid,
name="my-analysis",
state=ProjectState.READY,
binary_count=2,
is_stale=True,
)
assert p.id == pid
assert p.name == "my-analysis"
assert p.state == ProjectState.READY
assert p.binary_count == 2
assert p.is_stale is True
class TestBinaryEntity:
"""Tests for Binary entity."""
def test_default_binary(self) -> None:
b = Binary()
assert isinstance(b.id, UUID)
assert b.size_bytes == 0
assert b.import_mode == "copy"
assert b.is_stale is False
def test_size_bytes_is_int(self) -> None:
"""size_bytes must be an int, never a string (VAL-JSON-004)."""
b = Binary(size_bytes=4096)
assert isinstance(b.size_bytes, int)
assert b.size_bytes == 4096
def test_optional_fields_default_to_none(self) -> None:
"""Optional fields should default to None (VAL-JSON-006)."""
b = Binary()
assert b.architecture is None
assert b.endianness is None
assert b.entry_point is None
assert b.compiler is None
assert b.source_language is None
assert b.imported_at is None
assert b.analyzed_at is None
class TestSectionEntity:
"""Tests for Section entity."""
def test_default_section(self) -> None:
s = Section()
assert s.name == ""
assert s.virtual_size == 0
assert s.raw_size == 0
assert s.flags == []
assert s.entropy is None
def test_sizes_are_ints(self) -> None:
"""virtual_size and raw_size must be ints (VAL-JSON-004)."""
s = Section(name=".text", virtual_size=4096, raw_size=4096)
assert isinstance(s.virtual_size, int)
assert isinstance(s.raw_size, int)
def test_entropy_can_be_float_or_none(self) -> None:
s = Section(name=".text", entropy=6.5)
assert isinstance(s.entropy, float)
s2 = Section(name=".data")
assert s2.entropy is None
class TestFunctionEntity:
"""Tests for Function entity."""
def test_default_function(self) -> None:
f = Function()
assert f.name == ""
assert f.address is None
assert f.size_bytes == 0
assert f.confidence == Confidence.UNKNOWN
assert f.name_source == FunctionNameSource.UNKNOWN
assert f.is_external is False
assert f.is_thunk is False
def test_size_bytes_is_int(self) -> None:
f = Function(name="main", size_bytes=256)
assert isinstance(f.size_bytes, int)
def test_optional_fields_null(self) -> None:
f = Function()
assert f.signature is None
assert f.source_language is None
assert f.basic_block_count is None
assert f.instruction_count is None
assert f.cyclomatic_complexity is None
def test_confidece_is_enum(self) -> None:
f = Function(name="main", confidence=Confidence.HIGH)
assert f.confidence == Confidence.HIGH
assert isinstance(f.confidence, Confidence)
class TestAllEntityDefaults:
"""Verify that all optional fields default to None (VAL-JSON-006)."""
def test_entrypoint_optional_fields_null(self) -> None:
e = EntryPoint()
assert e.address is None
assert e.name is None
assert e.binary_id is None
def test_import_optional_fields_null(self) -> None:
imp = Import()
assert imp.address is None
assert imp.ordinal is None
assert imp.binary_id is None
def test_export_optional_fields_null(self) -> None:
exp = Export()
assert exp.address is None
assert exp.ordinal is None
assert exp.forwarder is None
assert exp.binary_id is None
def test_symbol_optional_fields_null(self) -> None:
s = Symbol()
assert s.address is None
assert s.binary_id is None
def test_string_optional_fields_null(self) -> None:
s = String()
assert s.address is None
assert s.binary_id is None
class TestEntitySizeFields:
"""Verify all size fields are integer bytes (VAL-JSON-004)."""
def test_all_size_fields_are_int(self) -> None:
"""Every size-like field must be int type."""
# Create each entity with size fields and verify they are ints
entities = [
("Binary", Binary(size_bytes=100)),
("Section", Section(virtual_size=200, raw_size=150)),
("Function", Function(size_bytes=300)),
("String", String(length=50)),
("Instruction", Instruction(size_bytes=4)),
("BasicBlock", BasicBlock(instruction_count=10)),
]
for name, entity in entities:
for field_name in [
"size_bytes",
"virtual_size",
"raw_size",
"length",
"instruction_count",
"binary_count",
]:
value = getattr(entity, field_name, None)
if value is not None:
assert isinstance(value, int), (
f"{name}.{field_name} is {type(value).__name__}, expected int"
)
class TestEntityOnlyCanonicalFields:
"""Verify entities contain only canonical fields (VAL-JSON-018)."""
def test_no_internal_fields_on_project(self) -> None:
"""Project entity must not have backend-specific keys."""
p = Project(name="test")
d = vars(p)
# Should not contain any backend-specific keys
assert "_ghidra_id" not in d
assert "program_address" not in d
assert "analyzer_ordinal" not in d
def test_no_internal_fields_on_function(self) -> None:
"""Function entity must not have backend-specific keys."""
f = Function(name="main")
d = vars(f)
assert "_ghidra_id" not in d
assert "ghidra_internal_id" not in d
assert "backend_raw" not in d
def test_no_internal_fields_on_section(self) -> None:
"""Section entity must not have backend-specific keys."""
s = Section(name=".text")
d = vars(s)
assert "ghidra_section_id" not in d
class TestAllEntitiesInstantiable:
"""Verify all 21 entity types can be instantiated."""
def test_all_entities_instantiate(self) -> None:
entities = [
Project(name="test"),
Binary(),
Section(),
EntryPoint(),
Import(),
Export(),
Symbol(),
String(),
Function(),
Instruction(),
BasicBlock(),
Reference(),
CallGraph(),
Diagnostic(),
Capability(),
Observation(),
Heuristic(),
Inference(),
Unknown(),
Report(),
AuditEvent(),
]
assert len(entities) == 21