- Output is read in 64 KiB chunks and split into lines here; a line past 64 KiB is cut and marked instead of raising out of readline's limit. - ansible-playbook runs in its own session; whatever ends the read (finish, an error, the task being cancelled) SIGTERMs then SIGKILLs its whole group, so no ssh or worker it forked outlives the run. - A run whose task is cancelled (shutdown) records 'cancelled' with finished_at before re-raising, instead of staying 'running'. - A cancel that lands after the process exited 0 no longer turns a successful run into 'cancelled'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
2.1 KiB
Python
64 lines
2.1 KiB
Python
"""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)
|