mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-12 20:16:29 +03:00
190 lines
7.2 KiB
Python
190 lines
7.2 KiB
Python
"""Unit tests for entity selectors."""
|
|
|
|
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 pytest
|
|
from binary_analysis.domain.entities import Address, Function
|
|
from binary_analysis.domain.errors import AmbiguousSelectorError, EntityNotFoundError
|
|
from binary_analysis.domain.selectors import (
|
|
SelectorKind,
|
|
format_candidates,
|
|
parse_selector,
|
|
resolve_function,
|
|
resolve_functions,
|
|
)
|
|
|
|
|
|
class TestParseSelector:
|
|
"""Tests for selector parsing."""
|
|
|
|
def test_parse_function_by_name(self) -> None:
|
|
parsed = parse_selector("function:main")
|
|
assert parsed.kind == SelectorKind.FUNCTION
|
|
assert parsed.value == "main"
|
|
assert parsed.is_address is False
|
|
|
|
def test_parse_function_by_address(self) -> None:
|
|
parsed = parse_selector("function:0x401000")
|
|
assert parsed.kind == SelectorKind.FUNCTION
|
|
assert parsed.value == "0x401000"
|
|
assert parsed.is_address is True
|
|
assert parsed.address_value == "401000"
|
|
|
|
def test_parse_address_range(self) -> None:
|
|
parsed = parse_selector("address:0x1000..0x2000")
|
|
assert parsed.kind == SelectorKind.ADDRESS
|
|
assert parsed.is_range is True
|
|
assert parsed.range_start == "0x1000"
|
|
assert parsed.range_end == "0x2000"
|
|
|
|
def test_parse_implicit_function_name(self) -> None:
|
|
"""Bare name should be parsed as implicit function selector."""
|
|
parsed = parse_selector("main")
|
|
assert parsed.kind == SelectorKind.FUNCTION
|
|
assert parsed.value == "main"
|
|
|
|
def test_parse_implicit_address(self) -> None:
|
|
"""Bare hex address should be parsed as implicit address selector."""
|
|
parsed = parse_selector("0x401000")
|
|
assert parsed.kind == SelectorKind.ADDRESS
|
|
assert parsed.value == "0x401000"
|
|
assert parsed.is_address is True
|
|
|
|
def test_parse_case_insensitive_kind(self) -> None:
|
|
"""Selector kind should be case-insensitive."""
|
|
parsed = parse_selector("FUNCTION:main")
|
|
assert parsed.kind == SelectorKind.FUNCTION
|
|
|
|
def test_parse_name_selector(self) -> None:
|
|
parsed = parse_selector("name:my_entity")
|
|
assert parsed.kind == SelectorKind.NAME
|
|
assert parsed.value == "my_entity"
|
|
|
|
def test_parse_address_without_prefix(self) -> None:
|
|
"""Address without 0x prefix is still recognized as address."""
|
|
parsed = parse_selector("401000")
|
|
assert parsed.kind == SelectorKind.ADDRESS
|
|
assert parsed.is_address is True
|
|
|
|
def test_parse_function_without_prefix(self) -> None:
|
|
"""function:addr without 0x prefix still works."""
|
|
parsed = parse_selector("function:401000")
|
|
assert parsed.kind == SelectorKind.FUNCTION
|
|
assert parsed.is_address is True
|
|
assert parsed.address_value == "401000"
|
|
|
|
def test_parse_empty_string(self) -> None:
|
|
"""Empty string should still parse without error."""
|
|
parsed = parse_selector("")
|
|
assert parsed.kind == SelectorKind.FUNCTION
|
|
assert parsed.value == ""
|
|
|
|
|
|
class TestResolveFunction:
|
|
"""Tests for function resolution."""
|
|
|
|
def _make_functions(self) -> list[Function]:
|
|
"""Create a standard set of test functions."""
|
|
return [
|
|
Function(
|
|
name="main",
|
|
address=Address(space="ram", offset="0x401000", display="0x401000"),
|
|
size_bytes=256,
|
|
),
|
|
Function(
|
|
name="_start",
|
|
address=Address(space="ram", offset="0x401100", display="0x401100"),
|
|
size_bytes=64,
|
|
),
|
|
Function(
|
|
name="helper_func",
|
|
address=Address(space="ram", offset="0x401200", display="0x401200"),
|
|
size_bytes=128,
|
|
),
|
|
Function(
|
|
name="helper_other",
|
|
address=Address(space="ram", offset="0x401300", display="0x401300"),
|
|
size_bytes=96,
|
|
),
|
|
]
|
|
|
|
def test_resolve_by_exact_name(self) -> None:
|
|
"""Resolve function by exact name."""
|
|
funcs = self._make_functions()
|
|
parsed = parse_selector("function:main")
|
|
result = resolve_function(parsed, funcs)
|
|
assert result.name == "main"
|
|
assert result.address is not None
|
|
assert result.address.offset == "0x401000"
|
|
|
|
def test_resolve_by_address(self) -> None:
|
|
"""Resolve function by address."""
|
|
funcs = self._make_functions()
|
|
parsed = parse_selector("function:0x401200")
|
|
result = resolve_function(parsed, funcs)
|
|
assert result.name == "helper_func"
|
|
|
|
def test_resolve_by_fuzzy_name(self) -> None:
|
|
"""Resolve function by fuzzy name (substring match)."""
|
|
funcs = self._make_functions()
|
|
parsed = parse_selector("function:helper")
|
|
# "helper" matches both helper_func and helper_other
|
|
# When require_unique=True, this should raise AmbiguousSelectorError
|
|
with pytest.raises(AmbiguousSelectorError) as exc_info:
|
|
resolve_function(parsed, funcs, require_unique=True)
|
|
assert len(exc_info.value.candidates) == 2
|
|
|
|
def test_resolve_fuzzy_not_unique(self) -> None:
|
|
"""resolve_functions returns all matches for fuzzy selector."""
|
|
funcs = self._make_functions()
|
|
parsed = parse_selector("function:helper")
|
|
results = resolve_functions(parsed, funcs)
|
|
assert len(results) == 2
|
|
|
|
def test_resolve_not_found(self) -> None:
|
|
"""Resolve nonexistent function raises EntityNotFoundError."""
|
|
funcs = self._make_functions()
|
|
parsed = parse_selector("function:nonexistent")
|
|
with pytest.raises(EntityNotFoundError) as exc_info:
|
|
resolve_function(parsed, funcs)
|
|
assert exc_info.value.entity_type == "Function"
|
|
assert exc_info.value.selector == "function:nonexistent"
|
|
|
|
def test_resolve_implicit_function_name(self) -> None:
|
|
"""Bare name should resolve as function."""
|
|
funcs = self._make_functions()
|
|
parsed = parse_selector("_start")
|
|
result = resolve_function(parsed, funcs)
|
|
assert result.name == "_start"
|
|
|
|
def test_resolve_unique_match_no_ambiguity(self) -> None:
|
|
"""Unique match should not raise AmbiguousSelectorError."""
|
|
funcs = self._make_functions()
|
|
parsed = parse_selector("function:_start")
|
|
result = resolve_function(parsed, funcs, require_unique=True)
|
|
assert result.name == "_start"
|
|
|
|
|
|
class TestFormatCandidates:
|
|
"""Tests for candidate formatting."""
|
|
|
|
def test_format_candidates(self) -> None:
|
|
candidates = [
|
|
{"name": "func_a", "address": {"display": "0x401000"}},
|
|
{"name": "func_b", "address": {"display": "0x402000"}},
|
|
]
|
|
output = format_candidates(candidates)
|
|
assert "Ambiguous selector matches multiple entities:" in output
|
|
assert "func_a @ 0x401000" in output
|
|
assert "func_b @ 0x402000" in output
|
|
|
|
def test_format_candidates_empty(self) -> None:
|
|
output = format_candidates([])
|
|
assert output # Should not error, just be empty-list message
|