r""" core/run_events.py — every event also goes to `dbo.CV_RunEvent` (operator, 2026-10-30): the Portal on Dokploy cannot read the bot PC's Logs folder, so its Runs page showed "No runs match" for every date. The file stays the record of a Run; the database row is a copy. A database that is off, missing the table or down never stops a Run — after the first failure the copy is switched off for the rest of the Run, said once in the log. python tests\test_run_events_db.py No database: the writer is a stand-in. """ import io import sys import json import pathlib import tempfile import contextlib sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src")) from ecw_bot import settings as cfg # noqa: E402 from ecw_bot.core import run_events # noqa: E402 CASES = [] def case(fn): CASES.append(fn) return fn def _start(tmp, writer): run_events._db_writer = writer log = pathlib.Path(tmp) / "run_20261030_101500.log" run_events.start(log, "VERIFY") return log @case def every_event_is_copied_with_its_run_id_and_kind(tmp): rows = [] _start(tmp, lambda run_id, at, kind, body: rows.append( (run_id, kind, json.loads(body)))) run_events.emit("claim_start", n=1, total=2, claim="9103000") run_events.close() assert [k for _, k, _ in rows] == ["run_start", "claim_start"], rows assert {r for r, _, _ in rows} == {"20261030_101500"} assert rows[1][2]["claim"] == "9103000" and rows[1][2]["t"], rows @case def the_file_and_the_row_carry_the_same_line(tmp): bodies = [] log = _start(tmp, lambda run_id, at, kind, body: bodies.append(body)) run_events.emit("warning", code="x", message="y") run_events.close() lines = run_events.path_for(log).read_text(encoding="utf-8").splitlines() assert lines == bodies, (lines, bodies) @case def a_database_error_switches_the_copy_off_and_the_run_goes_on(tmp): calls = [] def boom(*a): calls.append(a) raise OSError("no such table") out = io.StringIO() with contextlib.redirect_stdout(out): log = _start(tmp, boom) run_events.emit("claim_start", n=1) run_events.emit("claim_start", n=2) run_events.close() assert len(calls) == 1, "tried once, then off" assert out.getvalue().count("[EVENTS]") == 1, out.getvalue() assert len(run_events.path_for(log).read_text().splitlines()) == 3 @case def an_event_added_after_a_stop_is_copied_too(tmp): rows = [] run_events._db_writer = lambda run_id, at, kind, body: rows.append(kind) log = pathlib.Path(tmp) / "run_20261030_101500.log" run_events.append_to(log, "stopped", by="sam") assert rows == ["stopped"], rows @case def a_run_outside_the_logs_folder_is_never_copied(tmp): rows = [] run_events._db_writer = lambda *a: rows.append(a) other = pathlib.Path(tmp) / "elsewhere" other.mkdir() run_events.start(other / "run_20261030_101500.log", "VERIFY") run_events.emit("claim_start", n=1) run_events.close() run_events.append_to(other / "run_20261030_101500.log", "stopped") assert rows == [], "a test's run never reaches the database" def main() -> int: failures = 0 real = run_events._db_writer for fn in CASES: name = fn.__name__.replace("_", " ") with tempfile.TemporaryDirectory() as tmp: cfg.LOGS = pathlib.Path(tmp) # a real-looking Logs folder try: fn(tmp) print(f" ok {name}") except AssertionError as exc: print(f" FAIL {name}: {exc}") failures += 1 except Exception as exc: print(f" ERROR {name}: {type(exc).__name__}: {exc}") failures += 1 finally: run_events._reset() run_events._db_writer = real print(f"\n{len(CASES) - failures}/{len(CASES)} passed") return 1 if failures else 0 if __name__ == "__main__": sys.exit(main())