mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
349 lines
11 KiB
Python
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
|