"""Unit tests for executor structured-results parsing + cancel flagging.""" from steward.ansible.executor import _cancelled, cancel_run, parse_recap def test_parse_recap_extracts_per_host_counts(): lines = [ "PLAY RECAP *****************************************************************", "web01 : ok=5 changed=2 unreachable=0 failed=0 skipped=1 rescued=0", "db01 : ok=3 changed=0 unreachable=1 failed=0 skipped=0", ] hosts = parse_recap(lines) assert hosts["web01"] == {"ok": 5, "changed": 2, "unreachable": 0, "failed": 0, "skipped": 1} assert hosts["db01"]["unreachable"] == 1 assert hosts["db01"]["ok"] == 3 def test_parse_recap_ignores_lines_before_recap(): lines = [ "TASK [whatever] ****", "ok: [web01]", "PLAY RECAP ****", "web01 : ok=1 changed=0 unreachable=0 failed=0 skipped=0", ] assert set(parse_recap(lines)) == {"web01"} def test_parse_recap_empty_without_recap(): assert parse_recap(["TASK [x] ***", "ok: [h]"]) == {} class _Stream: def __init__(self, chunks): self.chunks = list(chunks) async def read(self, _n): return self.chunks.pop(0) if self.chunks else b"" def _read_lines(chunks, monkeypatch, limit=8): import asyncio from steward.ansible import executor monkeypatch.setattr(executor, "_MAX_LINE_BYTES", limit) async def _go(): return [line async for line in executor._lines(_Stream(chunks))] return asyncio.run(_go()) def test_lines_split_across_chunks(monkeypatch): assert _read_lines([b"ab", b"c\nde", b"f\n", b"tail"], monkeypatch) == [b"abc", b"def", b"tail"] def test_a_line_past_the_limit_is_cut_and_the_rest_dropped(monkeypatch): # #5156: a long line can't stall or break the reader; the next line is intact. got = _read_lines([b"0123456789", b"abcdef", b"gh\nnext\n"], monkeypatch) assert got == [b"0123456789"[:8] + b" [line cut at 8 bytes]", b"next"] def test_cancel_run_flags_untracked_run(): rid = "nonexistent-run-id" assert cancel_run(rid) is True assert rid in _cancelled _cancelled.discard(rid)