"""A run's play recap in words (#5329, #5330): what the runs list says under a run's status and what the run page says beside each host. A run's `results` is {hosts: {name: {ok, changed, unreachable, failed, skipped}}, failures: [...]}, parsed from the play recap. Model-free, so it is unit-tested without a database. """ from __future__ import annotations NAMES_SHOWN = 2 def _n(counts: dict | None, key: str) -> int: return int((counts or {}).get(key) or 0) def _names(names: list[str]) -> str: """"a", "a and b", "a, b and 3 more".""" if len(names) <= NAMES_SHOWN: return " and ".join(names) return f"{', '.join(names[:NAMES_SHOWN])} and {len(names) - NAMES_SHOWN} more" def run_recap(results: dict | None, check: bool = False) -> list[tuple[str, str | None]]: """A run's outcome across its hosts as (text, tone) parts, the worst first: "failed on web-1" (crit), "db-2 unreachable" (crit), "3 hosts", "2 changed" ("would change" for a dry run, which changed nothing). Empty when the run left no recap (it is still going, or died first).""" hosts = (results or {}).get("hosts") or {} if not hosts: return [] failed = sorted(h for h, c in hosts.items() if _n(c, "failed")) unreachable = sorted(h for h, c in hosts.items() if _n(c, "unreachable") and h not in failed) changed = sum(1 for c in hosts.values() if _n(c, "changed")) parts: list[tuple[str, str | None]] = [] if failed: parts.append((f"failed on {_names(failed)}", "crit")) if unreachable: parts.append((f"{_names(unreachable)} unreachable", "crit")) parts.append((f"{len(hosts)} host{'s' if len(hosts) != 1 else ''}", None)) if changed: parts.append((f"{changed} would change" if check else f"{changed} changed", None)) elif not failed and not unreachable: parts.append(("nothing to change" if check else "nothing changed", "muted")) return parts def host_recap(counts: dict | None, check: bool = False) -> list[tuple[str, str | None]]: """One host's task counts as (text, tone) parts, zeros left out and the worst first: "unreachable", "1 failed", "3 ok", "1 changed", "2 skipped".""" parts: list[tuple[str, str | None]] = [] if _n(counts, "unreachable"): parts.append(("unreachable", "crit")) if _n(counts, "failed"): parts.append((f"{_n(counts, 'failed')} failed", "crit")) if _n(counts, "ok"): parts.append((f"{_n(counts, 'ok')} ok", None)) if _n(counts, "changed"): word = "would change" if check else "changed" parts.append((f"{_n(counts, 'changed')} {word}", None)) if _n(counts, "skipped"): parts.append((f"{_n(counts, 'skipped')} skipped", "muted")) return parts