Files
Rpa_Skills/tests/test_bot_runner.py
T

113 lines
5.0 KiB
Python

"""Bot wiring self-check (templates/bot_runner.py): which page events each raw bot line makes."""
import importlib.util
import os
import sys
import time
from pathlib import Path
import pytest
RUNNER = Path(__file__).resolve().parents[1] / "skills" / "rpa-dashboard-no-ai" / "templates" / "bot_runner.py"
@pytest.fixture
def runner():
spec = importlib.util.spec_from_file_location("bot_runner", RUNNER)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
mod.STEP_MARKERS = [("[step 1]", "Initializing", 10), ("[step 2]", "Login", 40),
("[step 3]", "Process items", 70), ("[all done]", "Completed", 100)]
mod.FRIENDLY_RULES = [("[login] done", "Logged in.", "success"), ("[login]", "Logging in...", "info"),
("could not save receipt", "Could not save the receipt.", "error")]
return mod
def steps(runner, line):
return [d for e, d in runner.events_for(line) if e == "step"]
def test_step_marker_moves_progress(runner):
assert steps(runner, "[step 2] Opening the login page") == [{"label": "Login", "pct": 40}]
assert steps(runner, "[STEP 3] Processing 12 items") == [{"label": "Process items", "pct": 70}]
def test_common_word_does_not_move_progress(runner):
assert steps(runner, "Login page loaded slowly") == []
assert steps(runner, "Completed loading the config") == []
def test_first_step_marker_wins(runner):
assert steps(runner, "[step 1] retry after [step 3] timed out") == [{"label": "Initializing", "pct": 10}]
def logs(runner, line):
return [d for e, d in runner.events_for(line) if e == "log"]
def test_friendly_rule_rewrites_the_line_and_first_rule_wins(runner):
assert logs(runner, "[login] done in 2.1s") == [{"msg": "Logged in.", "type": "success"}]
assert logs(runner, "[LOGIN] sending credentials") == [{"msg": "Logging in...", "type": "info"}]
def test_unmatched_non_error_lines_are_hidden(runner):
for line in ["Loading config from settings.ini", "[step 2] Opening the login page", "warning: slow page"]:
assert logs(runner, line) == [], line
@pytest.mark.parametrize("line", [
"ERROR: portal timed out", "Traceback (most recent call last):", "ValueError: bad date",
"Exception: boom", "FATAL disk full", "Login failed for user 3"])
def test_unmatched_error_line_is_always_shown_raw(runner, line):
assert logs(runner, line) == [{"msg": line, "type": "error"}]
def test_matched_error_line_shows_its_friendly_message(runner):
assert logs(runner, "ERROR could not save receipt 42") == [{"msg": "Could not save the receipt.", "type": "error"}]
def test_error_on_a_step_line_moves_progress_and_is_shown(runner):
line = "[step 3] ERROR item 2 rejected"
assert runner.events_for(line) == [("step", {"label": "Process items", "pct": 70}),
("log", {"msg": line, "type": "error"})]
def run_bot(runner, tmp_path, script):
"""Run a fake bot through runner.run(); return [(seconds since start, event, data)]."""
bot = tmp_path / "fake_bot.py"
bot.write_text(script, encoding="utf-8")
runner.BOT_CMD, runner.BOT_CWD, runner.RAW_LOG = [sys.executable, str(bot)], tmp_path, tmp_path / "raw.log"
t0, sent = time.monotonic(), []
runner.run(lambda event, data: sent.append((time.monotonic() - t0, event, data)))
return sent
def test_run_streams_bot_output_live_from_a_child_process(runner, tmp_path):
sent = run_bot(runner, tmp_path, "import os, time\n"
"print('[step 1] pid', os.getpid())\n"
"print('[login] done')\n"
"time.sleep(1)\n"
"print('[all done] ✓ 3 items')\n")
events = [(e, d) for _, e, d in sent]
assert events == [("status", {"state": "running", "msg": "Running…"}),
("step", {"label": "Initializing", "pct": 10}),
("log", {"msg": "Logged in.", "type": "success"}),
("step", {"label": "Completed", "pct": 100}),
("status", {"state": "done", "msg": "Completed"})]
assert sent[3][0] - sent[1][0] > 0.5, "lines must reach the page while the bot runs, not at exit"
raw = (tmp_path / "raw.log").read_text(encoding="utf-8")
assert "[login] done" in raw and "✓ 3 items" in raw # raw lines kept for audit
assert f"pid {os.getpid()}" not in raw # the bot ran in its own process
def test_run_ends_in_error_when_the_bot_fails(runner, tmp_path):
sent = run_bot(runner, tmp_path, "print('[step 1] start')\nraise SystemExit(3)\n")
assert sent[-1][1:] == ("status", {"state": "error", "msg": "Failed (exit code 3)"})
def test_run_ends_in_error_when_the_bot_cannot_start(runner, tmp_path):
runner.BOT_CMD, runner.BOT_CWD, runner.RAW_LOG = ["no-such-bot-xyz"], tmp_path, tmp_path / "raw.log"
sent = []
runner.run(lambda event, data: sent.append((event, data)))
assert sent[-2][0] == "log" and sent[-2][1]["type"] == "error"
assert sent[-1] == ("status", {"state": "error", "msg": "Could not start the bot"})