Publish tests/ from marimo
Browse files- tests/test_live_desktop.py +53 -22
tests/test_live_desktop.py
CHANGED
|
@@ -6,7 +6,7 @@ synthetic screenshot. It is opt-in because it moves the real mouse and clicks:
|
|
| 6 |
TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py
|
| 7 |
|
| 8 |
Run it on a throwaway desktop (VM, container with Xvfb), never your working
|
| 9 |
-
session. With Xvfb it needs no hardware:
|
| 10 |
|
| 11 |
Xvfb :99 -screen 0 1280x800x24 &
|
| 12 |
DISPLAY=:99 TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py
|
|
@@ -21,10 +21,11 @@ import time
|
|
| 21 |
import pytest
|
| 22 |
|
| 23 |
from tiny_computer_agent.act import PyAutoGuiExecutor
|
| 24 |
-
from tiny_computer_agent.actions import Action
|
| 25 |
from tiny_computer_agent.desktop import display_available
|
| 26 |
from tiny_computer_agent.observe import GrabScreen
|
| 27 |
-
from tiny_computer_agent.perception import
|
|
|
|
|
|
|
| 28 |
|
| 29 |
OPT_IN = os.environ.get("TINY_COMPUTER_AGENT_LIVE_TEST") == "1"
|
| 30 |
|
|
@@ -33,15 +34,18 @@ pytestmark = pytest.mark.skipif(
|
|
| 33 |
reason="moves the real mouse: set TINY_COMPUTER_AGENT_LIVE_TEST=1 on a throwaway desktop",
|
| 34 |
)
|
| 35 |
|
| 36 |
-
APP = '''import pathlib, tkinter as tk
|
| 37 |
-
|
| 38 |
-
MARKER = pathlib.Path("%(marker)s")
|
| 39 |
WINDOW = (120, 120)
|
| 40 |
BUTTON = (20, 155, 110, 40) # x, y, width, height inside the window
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
root = tk.Tk()
|
| 43 |
root.title("Document")
|
| 44 |
-
root.geometry("420x230+
|
| 45 |
root.configure(bg="#f5f6f8")
|
| 46 |
title = tk.Label(root, text="Untitled document - unsaved changes", bg="#f5f6f8")
|
| 47 |
title.place(x=20, y=18)
|
|
@@ -57,8 +61,9 @@ def chosen(what):
|
|
| 57 |
widget.place_forget()
|
| 58 |
|
| 59 |
buttons = [tk.Button(root, text="Save", command=lambda: chosen("Save"))]
|
| 60 |
-
buttons[0].place(x=
|
| 61 |
root.update()
|
|
|
|
| 62 |
root.mainloop()
|
| 63 |
'''
|
| 64 |
|
|
@@ -70,43 +75,69 @@ def save_dialog(tmp_path):
|
|
| 70 |
pytest.skip("no display session to drive")
|
| 71 |
if not detector_available("ocr")[0]:
|
| 72 |
pytest.skip("this test needs OCR to find the button")
|
|
|
|
| 73 |
marker = tmp_path / "clicked.txt"
|
|
|
|
| 74 |
app = tmp_path / "app.py"
|
| 75 |
-
app.write_text(APP
|
|
|
|
| 76 |
process = subprocess.Popen([sys.executable, str(app)])
|
| 77 |
try:
|
| 78 |
-
time.
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
finally:
|
|
|
|
|
|
|
| 81 |
process.terminate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
def test_ocr_finds_a_real_button_at_the_right_pixels(save_dialog):
|
| 85 |
"""Pixels in, a label whose coordinates really are the button's."""
|
| 86 |
-
|
| 87 |
-
observation = GrabScreen(monitor=1).capture()
|
| 88 |
-
elements = OcrDetector().detect(observation.frame)
|
| 89 |
|
| 90 |
save = [e for e in elements if e.matches("Save")]
|
| 91 |
assert save, f"OCR found no Save button, only {[e.label for e in elements]}"
|
| 92 |
-
|
| 93 |
-
assert
|
| 94 |
-
assert
|
| 95 |
|
| 96 |
|
| 97 |
def test_a_real_click_lands_on_the_button(save_dialog):
|
| 98 |
"""The whole loop: see it, choose it, click it, and the app reacts."""
|
| 99 |
from tiny_computer_agent import AgentLoop, PlannerBrain, Policy
|
| 100 |
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
episode = AgentLoop(
|
| 103 |
-
screen=GrabScreen(monitor=1),
|
| 104 |
brain=PlannerBrain(),
|
| 105 |
executor=PyAutoGuiExecutor(),
|
| 106 |
policy=Policy(dry_run=False, step_budget=2),
|
| 107 |
verify=lambda observation: "saved" in observation.text.lower(),
|
| 108 |
).run("click 'Save' to save the document")
|
| 109 |
|
| 110 |
-
|
| 111 |
-
assert
|
| 112 |
-
assert
|
|
|
|
|
|
| 6 |
TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py
|
| 7 |
|
| 8 |
Run it on a throwaway desktop (VM, container with Xvfb), never your working
|
| 9 |
+
session. With Xvfb it needs no hardware at all:
|
| 10 |
|
| 11 |
Xvfb :99 -screen 0 1280x800x24 &
|
| 12 |
DISPLAY=:99 TINY_COMPUTER_AGENT_LIVE_TEST=1 python -m pytest tests/test_live_desktop.py
|
|
|
|
| 21 |
import pytest
|
| 22 |
|
| 23 |
from tiny_computer_agent.act import PyAutoGuiExecutor
|
|
|
|
| 24 |
from tiny_computer_agent.desktop import display_available
|
| 25 |
from tiny_computer_agent.observe import GrabScreen
|
| 26 |
+
from tiny_computer_agent.perception import (
|
| 27 |
+
IndexedScreen, OcrDetector, detector_available,
|
| 28 |
+
)
|
| 29 |
|
| 30 |
OPT_IN = os.environ.get("TINY_COMPUTER_AGENT_LIVE_TEST") == "1"
|
| 31 |
|
|
|
|
| 34 |
reason="moves the real mouse: set TINY_COMPUTER_AGENT_LIVE_TEST=1 on a throwaway desktop",
|
| 35 |
)
|
| 36 |
|
|
|
|
|
|
|
|
|
|
| 37 |
WINDOW = (120, 120)
|
| 38 |
BUTTON = (20, 155, 110, 40) # x, y, width, height inside the window
|
| 39 |
+
READY_TIMEOUT = 20.0
|
| 40 |
+
|
| 41 |
+
APP = '''import pathlib, tkinter as tk
|
| 42 |
+
|
| 43 |
+
MARKER = pathlib.Path(r"__MARKER__")
|
| 44 |
+
READY = pathlib.Path(str(MARKER) + ".ready")
|
| 45 |
|
| 46 |
root = tk.Tk()
|
| 47 |
root.title("Document")
|
| 48 |
+
root.geometry("420x230+120+120")
|
| 49 |
root.configure(bg="#f5f6f8")
|
| 50 |
title = tk.Label(root, text="Untitled document - unsaved changes", bg="#f5f6f8")
|
| 51 |
title.place(x=20, y=18)
|
|
|
|
| 61 |
widget.place_forget()
|
| 62 |
|
| 63 |
buttons = [tk.Button(root, text="Save", command=lambda: chosen("Save"))]
|
| 64 |
+
buttons[0].place(x=20, y=155, width=110, height=40)
|
| 65 |
root.update()
|
| 66 |
+
READY.write_text("mapped")
|
| 67 |
root.mainloop()
|
| 68 |
'''
|
| 69 |
|
|
|
|
| 75 |
pytest.skip("no display session to drive")
|
| 76 |
if not detector_available("ocr")[0]:
|
| 77 |
pytest.skip("this test needs OCR to find the button")
|
| 78 |
+
|
| 79 |
marker = tmp_path / "clicked.txt"
|
| 80 |
+
ready = pathlib.Path(str(marker) + ".ready")
|
| 81 |
app = tmp_path / "app.py"
|
| 82 |
+
app.write_text(APP.replace("__MARKER__", str(marker)))
|
| 83 |
+
|
| 84 |
process = subprocess.Popen([sys.executable, str(app)])
|
| 85 |
try:
|
| 86 |
+
deadline = time.time() + READY_TIMEOUT
|
| 87 |
+
while time.time() < deadline and not ready.exists():
|
| 88 |
+
if process.poll() is not None:
|
| 89 |
+
pytest.fail(f"the test window exited immediately (code {process.returncode})")
|
| 90 |
+
time.sleep(0.2)
|
| 91 |
+
assert ready.exists(), "the test window never mapped"
|
| 92 |
+
yield marker
|
| 93 |
finally:
|
| 94 |
+
# Wait for the window to actually disappear: a ghost window left on the
|
| 95 |
+
# display makes the *next* test's OCR see two identical buttons.
|
| 96 |
process.terminate()
|
| 97 |
+
try:
|
| 98 |
+
process.wait(timeout=10)
|
| 99 |
+
except subprocess.TimeoutExpired: # pragma: no cover
|
| 100 |
+
process.kill()
|
| 101 |
+
process.wait(timeout=5)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _look() -> tuple:
|
| 105 |
+
"""(observation, elements) - one real capture, one real OCR pass."""
|
| 106 |
+
observation = GrabScreen(monitor=1).capture()
|
| 107 |
+
return observation, OcrDetector().detect(observation.frame)
|
| 108 |
|
| 109 |
|
| 110 |
def test_ocr_finds_a_real_button_at_the_right_pixels(save_dialog):
|
| 111 |
"""Pixels in, a label whose coordinates really are the button's."""
|
| 112 |
+
_, elements = _look()
|
|
|
|
|
|
|
| 113 |
|
| 114 |
save = [e for e in elements if e.matches("Save")]
|
| 115 |
assert save, f"OCR found no Save button, only {[e.label for e in elements]}"
|
| 116 |
+
x, y = save[0].center
|
| 117 |
+
assert WINDOW[0] + BUTTON[0] <= x <= WINDOW[0] + BUTTON[0] + BUTTON[2], f"{x} outside the button"
|
| 118 |
+
assert WINDOW[1] + BUTTON[1] <= y <= WINDOW[1] + BUTTON[1] + BUTTON[3], f"{y} outside the button"
|
| 119 |
|
| 120 |
|
| 121 |
def test_a_real_click_lands_on_the_button(save_dialog):
|
| 122 |
"""The whole loop: see it, choose it, click it, and the app reacts."""
|
| 123 |
from tiny_computer_agent import AgentLoop, PlannerBrain, Policy
|
| 124 |
|
| 125 |
+
_, elements = _look()
|
| 126 |
+
labels = [e.label for e in elements]
|
| 127 |
+
save = [e for e in elements if e.matches("Save")]
|
| 128 |
+
assert save, f"nothing named Save on screen before clicking: {labels}"
|
| 129 |
+
|
| 130 |
+
# IndexedScreen, not a bare GrabScreen: capture gives pixels, the detector
|
| 131 |
+
# gives labels, and without labels there is nothing for a brain to choose.
|
| 132 |
episode = AgentLoop(
|
| 133 |
+
screen=IndexedScreen(GrabScreen(monitor=1), OcrDetector()),
|
| 134 |
brain=PlannerBrain(),
|
| 135 |
executor=PyAutoGuiExecutor(),
|
| 136 |
policy=Policy(dry_run=False, step_budget=2),
|
| 137 |
verify=lambda observation: "saved" in observation.text.lower(),
|
| 138 |
).run("click 'Save' to save the document")
|
| 139 |
|
| 140 |
+
detail = episode.render() + f"\nclicked at {save[0].center}, labels were {labels}"
|
| 141 |
+
assert save_dialog.exists(), f"no click landed on the button\n{detail}"
|
| 142 |
+
assert save_dialog.read_text() == "Save"
|
| 143 |
+
assert episode.success, detail
|