driver.tree() failures (WDA/Appium session errors) previously raised uncaught, killing describe_screen() before OCR ever ran. Malformed tree content (invalid XML) had the same problem inside parse_ui_tree(). Both are now caught and logged, falling back to an empty ui_elements list so the scene degrades to OCR-only, mirroring the existing OCR-failure fallback in run_ocr().
This commit is contained in:
+11
-1
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -9,6 +10,8 @@ from perception.scene_builder import build_scene as build_fused_scene
|
|||||||
from perception.scene_builder import infer_png_size
|
from perception.scene_builder import infer_png_size
|
||||||
from perception.ui_parser import parse_ui_tree
|
from perception.ui_parser import parse_ui_tree
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PerceptionProvider(ABC):
|
class PerceptionProvider(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -22,10 +25,17 @@ class DefaultPerceptionProvider(PerceptionProvider):
|
|||||||
|
|
||||||
def build_scene(self, screenshot: bytes, tree: Any) -> Scene:
|
def build_scene(self, screenshot: bytes, tree: Any) -> Scene:
|
||||||
width, height = infer_png_size(screenshot)
|
width, height = infer_png_size(screenshot)
|
||||||
|
try:
|
||||||
|
ui_elements = parse_ui_tree(tree)
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"UI tree parsing failed; continuing without UI tree", exc_info=True
|
||||||
|
)
|
||||||
|
ui_elements = []
|
||||||
return build_fused_scene(
|
return build_fused_scene(
|
||||||
screen_width=width,
|
screen_width=width,
|
||||||
screen_height=height,
|
screen_height=height,
|
||||||
ui_elements=parse_ui_tree(tree),
|
ui_elements=ui_elements,
|
||||||
ocr_elements=run_ocr(screenshot, engine=self._ocr_engine),
|
ocr_elements=run_ocr(screenshot, engine=self._ocr_engine),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from core.errors import DriverError
|
||||||
|
from core.models import Bounds
|
||||||
|
from device.manager import DeviceManager
|
||||||
|
from driver.base import Driver
|
||||||
|
from perception.ocr import OCRBox
|
||||||
|
from tools.describe_screen import describe_screen
|
||||||
|
|
||||||
|
PNG_10X20 = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x0a\x00\x00\x00\x14"
|
||||||
|
|
||||||
|
|
||||||
|
class FakeOCREngine:
|
||||||
|
def __init__(self, boxes: list[OCRBox]) -> None:
|
||||||
|
self._boxes = boxes
|
||||||
|
|
||||||
|
def extract(self, image: bytes | str | Path) -> list[OCRBox]:
|
||||||
|
return self._boxes
|
||||||
|
|
||||||
|
|
||||||
|
class TreeFailingDriver(Driver):
|
||||||
|
def connect(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def disconnect(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def screenshot(self) -> bytes:
|
||||||
|
return PNG_10X20
|
||||||
|
|
||||||
|
def tap(self, x: float, y: float) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def swipe(
|
||||||
|
self,
|
||||||
|
start_x: float,
|
||||||
|
start_y: float,
|
||||||
|
end_x: float,
|
||||||
|
end_y: float,
|
||||||
|
duration_ms: int = 500,
|
||||||
|
) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def input(self, text: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def launch(self, app_id: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def terminate(self, app_id: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def tree(self):
|
||||||
|
raise DriverError("ui tree retrieval failed")
|
||||||
|
|
||||||
|
def home(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def lock(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def unlock(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def test_describe_screen_degrades_to_ocr_only_when_ui_tree_unavailable() -> None:
|
||||||
|
manager = DeviceManager()
|
||||||
|
manager.register_device("phone-1", lambda: TreeFailingDriver())
|
||||||
|
manager.connect("phone-1")
|
||||||
|
fake_box = OCRBox(text="Search", bounds=Bounds(1, 2, 4, 4), confidence=0.9)
|
||||||
|
fake_engine = FakeOCREngine([fake_box])
|
||||||
|
|
||||||
|
scene = describe_screen(
|
||||||
|
"phone-1",
|
||||||
|
manager=manager,
|
||||||
|
ocr_engine=fake_engine, # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [element.text for element in scene.elements] == ["Search"]
|
||||||
|
assert all(element.source == "ocr" for element in scene.elements)
|
||||||
@@ -9,13 +9,7 @@ from perception.scene_builder import build_scene as build_fused_scene
|
|||||||
from perception.scene_builder import infer_png_size
|
from perception.scene_builder import infer_png_size
|
||||||
from perception.ui_parser import parse_ui_tree
|
from perception.ui_parser import parse_ui_tree
|
||||||
|
|
||||||
PNG_10X20 = (
|
PNG_10X20 = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x0a\x00\x00\x00\x14"
|
||||||
b"\x89PNG\r\n\x1a\n"
|
|
||||||
b"\x00\x00\x00\r"
|
|
||||||
b"IHDR"
|
|
||||||
b"\x00\x00\x00\x0a"
|
|
||||||
b"\x00\x00\x00\x14"
|
|
||||||
)
|
|
||||||
|
|
||||||
TREE_XML = """
|
TREE_XML = """
|
||||||
<AppiumAUT x="0" y="0" width="10" height="20">
|
<AppiumAUT x="0" y="0" width="10" height="20">
|
||||||
@@ -57,6 +51,27 @@ def test_null_perception_provider_infers_zero_size_for_non_png() -> None:
|
|||||||
assert scene == Scene(width=0, height=0, elements=[])
|
assert scene == Scene(width=0, height=0, elements=[])
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_perception_provider_degrades_when_ui_tree_parsing_fails() -> None:
|
||||||
|
fake_boxes = [
|
||||||
|
OCRBox(text="Search", bounds=parse_ui_tree(TREE_XML)[0].bounds, confidence=0.9),
|
||||||
|
]
|
||||||
|
fake_engine = FakeOCREngine(fake_boxes)
|
||||||
|
provider = DefaultPerceptionProvider(ocr_engine=fake_engine) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
scene = provider.build_scene(PNG_10X20, "<unclosed-tag")
|
||||||
|
|
||||||
|
width, height = infer_png_size(PNG_10X20)
|
||||||
|
expected = build_fused_scene(
|
||||||
|
screen_width=width,
|
||||||
|
screen_height=height,
|
||||||
|
ui_elements=[],
|
||||||
|
ocr_elements=[fake_boxes[0].to_scene_element("ocr-000")],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert scene == expected
|
||||||
|
assert scene.elements == [fake_boxes[0].to_scene_element("ocr-000")]
|
||||||
|
|
||||||
|
|
||||||
def test_default_perception_provider_wires_through_scene_builder() -> None:
|
def test_default_perception_provider_wires_through_scene_builder() -> None:
|
||||||
fake_boxes = [
|
fake_boxes = [
|
||||||
OCRBox(text="Search", bounds=parse_ui_tree(TREE_XML)[0].bounds, confidence=0.9),
|
OCRBox(text="Search", bounds=parse_ui_tree(TREE_XML)[0].bounds, confidence=0.9),
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from core.models import Scene
|
from core.models import Scene
|
||||||
from device.manager import DeviceManager
|
from device.manager import DeviceManager
|
||||||
from perception.ocr import PaddleOCREngine
|
from perception.ocr import PaddleOCREngine
|
||||||
from perception.provider import DefaultPerceptionProvider, PerceptionProvider
|
from perception.provider import DefaultPerceptionProvider, PerceptionProvider
|
||||||
from tools._device import get_driver
|
from tools._device import get_driver
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def describe_screen(
|
def describe_screen(
|
||||||
device_id: str | None = None,
|
device_id: str | None = None,
|
||||||
@@ -16,6 +20,12 @@ def describe_screen(
|
|||||||
) -> Scene:
|
) -> Scene:
|
||||||
driver = get_driver(device_id, manager=manager)
|
driver = get_driver(device_id, manager=manager)
|
||||||
screenshot = driver.screenshot()
|
screenshot = driver.screenshot()
|
||||||
raw_tree = driver.tree()
|
try:
|
||||||
|
raw_tree = driver.tree()
|
||||||
|
except Exception:
|
||||||
|
logger.warning(
|
||||||
|
"UI tree retrieval failed; continuing without UI tree", exc_info=True
|
||||||
|
)
|
||||||
|
raw_tree = None
|
||||||
provider = perception_provider or DefaultPerceptionProvider(ocr_engine=ocr_engine)
|
provider = perception_provider or DefaultPerceptionProvider(ocr_engine=ocr_engine)
|
||||||
return provider.build_scene(screenshot, raw_tree)
|
return provider.build_scene(screenshot, raw_tree)
|
||||||
|
|||||||
Reference in New Issue
Block a user