feat(kvb-check-ansible): rank where an Ansible run spends its time
New skill with ansible_tasks.py: gaps, durations, kinds, kind and task rank a run's log by wall-clock gaps or profile_tasks lines; profile-report merges cProfile dumps of the controller and its forked workers (sitecustomize hook); bench-carrier and the bench playbook measure per-task overhead against variable size. kvb-code-tune hands its Ansible timing to it. 🎯 Fix: 92 % 💥 Break: 2 % 💸 Cost: new ⏱️ Time: new 🛡️ Security: 100 % 🏆 Score: 90 🚦 Gate: cost new, time new: cleared by the operator's commit word for kvb-check-ansible (2026-11-07) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8b0c287ada
commit
05cc891998
17 files changed
+1212
No files matched your search
@@ -0,0 +1,125 @@
|
||||
"""Shared paths, script modules, synthetic logs and the CLI test case of the kvb-check-ansible tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
SKILL = Path(__file__).resolve().parents[1] / "skills" / "kvb-check-ansible"
|
||||
sys.path.insert(0, str(SKILL / "scripts"))
|
||||
|
||||
import ansible_bench
|
||||
import ansible_log
|
||||
import ansible_tables
|
||||
|
||||
__all__ = [
|
||||
"BENCH",
|
||||
"MIDNIGHT_LOG",
|
||||
"PROFILE_DIR",
|
||||
"PROFILE_LOG",
|
||||
"SCRIPT",
|
||||
"WALL_KINDS",
|
||||
"WALL_LOG",
|
||||
"LogCase",
|
||||
"ansible_bench",
|
||||
"ansible_log",
|
||||
"ansible_tables",
|
||||
]
|
||||
|
||||
SCRIPT = SKILL / "scripts" / "ansible_tasks.py"
|
||||
PROFILE_DIR = SKILL / "scripts" / "profile"
|
||||
BENCH = SKILL / "bench"
|
||||
STARS = " " + "*" * 40
|
||||
|
||||
WALL_LOG = "\n".join(
|
||||
[
|
||||
"2026-11-07T06:45:40.0000000Z PLAY [deploy]" + STARS,
|
||||
"2026-11-07T06:45:40.5000000Z TASK [setup : install]" + STARS,
|
||||
"2026-11-07T06:45:43.0000000Z \x1b[0;33mchanged: [host]\x1b[0m",
|
||||
"2026-11-07T06:45:44.0000000Z TASK [setup : check]" + STARS,
|
||||
"2026-11-07T06:45:44.1000000Z skipping: [host]",
|
||||
"2026-11-07T06:45:44.2Z TASK [setup : install]" + STARS,
|
||||
"2026-11-07T06:45:44.5000000Z skipping: [host] => (item=a)",
|
||||
"2026-11-07T06:45:45.0000000Z ok: [host] => (item=b)",
|
||||
"2026-11-07T06:45:45.700000000Z TASK [setup : include]" + STARS,
|
||||
"2026-11-07T06:45:45.8000000Z included: /x/y.yml for host",
|
||||
"2026-11-07T06:45:46.0 TASK [setup : flush]" + STARS,
|
||||
"2026-11-07T06:45:46.5000000Z [WARNING]: nothing to flush",
|
||||
"2026-11-07T06:45:46.8000000Z RUNNING HANDLER [setup : restart]" + STARS,
|
||||
"2026-11-07T06:45:48.0000000Z fatal: [host]: FAILED! => {}",
|
||||
"2026-11-07T06:45:48.0000000Z PLAY RECAP" + STARS,
|
||||
"2026-11-07T06:45:48.0000000Z host : ok=2 changed=1 skipped=1 failed=1",
|
||||
]
|
||||
) + "\n"
|
||||
|
||||
PROFILE_LINE = "Saturday 07 November 2026 06:45:40 +0000 ({}) 0:00:00.020 " + "*" * 20
|
||||
PROFILE_LOG = "\n".join(
|
||||
[
|
||||
"PLAY [deploy]" + STARS,
|
||||
"",
|
||||
"TASK [Gathering Facts]" + STARS,
|
||||
PROFILE_LINE.format("0:00:00.020"),
|
||||
"ok: [host]",
|
||||
"TASK [setup : install]" + STARS,
|
||||
PROFILE_LINE.format("0:00:02.000"),
|
||||
"changed: [host]",
|
||||
"TASK [setup : check]" + STARS,
|
||||
PROFILE_LINE.format("0:00:03.500"),
|
||||
"skipping: [host]",
|
||||
"TASK [setup : install]" + STARS,
|
||||
PROFILE_LINE.format("0:00:00.100"),
|
||||
"included: /x/y.yml for host",
|
||||
"RUNNING HANDLER [setup : restart]" + STARS,
|
||||
PROFILE_LINE.format("0:01:01.500"),
|
||||
"ok: [host]",
|
||||
"PLAY RECAP" + STARS,
|
||||
"host : ok=3 changed=1 skipped=1",
|
||||
"TASKS RECAP" + STARS,
|
||||
PROFILE_LINE.format("0:00:00.400"),
|
||||
"=" * 40,
|
||||
"setup : install ----------------------- 63.50s",
|
||||
]
|
||||
) + "\n"
|
||||
|
||||
MIDNIGHT_LOG = chr(0xFEFF) + "2026-11-07T23:59:59.5Z TASK [late]" + STARS + "\n2026-11-08T00:00:00.25Z PLAY RECAP" + STARS + "\n"
|
||||
|
||||
WALL_KINDS = [
|
||||
"kind\tcount\tsum\tmean\t<0.25\t<0.5\t<1\t<2\t>=2",
|
||||
"ran\t3\t6.2\t2.0667\t0\t0\t0\t2\t1",
|
||||
"other\t1\t0.8\t0.8000\t0\t0\t1\t0\t0",
|
||||
"included\t1\t0.3\t0.3000\t0\t1\t0\t0\t0",
|
||||
"skipped\t1\t0.2\t0.2000\t1\t0\t0\t0\t0",
|
||||
]
|
||||
|
||||
|
||||
class LogCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.base = Path(tmp.name)
|
||||
self.wall = self.write("wall.log", WALL_LOG)
|
||||
self.profile = self.write("profile.log", PROFILE_LOG)
|
||||
|
||||
def write(self, name: str, text: str) -> Path:
|
||||
path = self.base / name
|
||||
path.write_text(text, encoding="utf-8")
|
||||
return path
|
||||
|
||||
def run_script(self, *args: str, env: dict | None = None) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCRIPT), *args], capture_output=True, text=True, check=False, env=env
|
||||
)
|
||||
|
||||
def lines(self, *args: str) -> list[str]:
|
||||
result = self.run_script(*args)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return result.stdout.splitlines()
|
||||
|
||||
def report(self, *args: str) -> dict:
|
||||
result = self.run_script(*args, "--json")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
return json.loads(result.stdout)
|
||||
Reference in new issue
Block a user