test grouping
This commit is contained in:
@@ -20,6 +20,7 @@ Security posture (dashboard-test-runner US-001):
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -61,6 +62,44 @@ PYTHON_ENV_VAR = "MESHNET_PYTHON"
|
||||
|
||||
_NODE_ID_RE = re.compile(r"^[\w./\[\]:,= @-]+$")
|
||||
|
||||
_MODULE_TAGS: dict[str, tuple[str, ...]] = {
|
||||
"dashboard": ("dashboard", "http"),
|
||||
"accounts": ("auth", "accounts", "http"),
|
||||
"auth_boundary": ("auth", "security", "http"),
|
||||
"billing": ("billing", "payments", "http"),
|
||||
"contracts": ("contracts", "settlement"),
|
||||
"settlement": ("billing", "settlement"),
|
||||
"tracker": ("tracker", "routing"),
|
||||
"routing": ("tracker", "routing", "http"),
|
||||
"dynamic_routing": ("tracker", "routing", "performance"),
|
||||
"node": ("node", "startup"),
|
||||
"model": ("node", "model"),
|
||||
"kv_cache": ("node", "model", "cache"),
|
||||
"real_": ("real-inference", "node", "model"),
|
||||
"two_node": ("integration", "inference"),
|
||||
"openai": ("gateway", "sdk", "http"),
|
||||
"meshnet_sdk": ("sdk", "gateway", "http"),
|
||||
"gossip": ("relay", "gossip", "network"),
|
||||
"relay": ("relay", "network"),
|
||||
"wallet": ("wallet", "security", "auth"),
|
||||
"toploc": ("audit", "calibration"),
|
||||
"forfeiture": ("security", "billing"),
|
||||
"fraud": ("security", "billing"),
|
||||
"test_runner": ("dashboard", "test-runner"),
|
||||
}
|
||||
|
||||
_FUNCTION_TAGS: dict[str, tuple[str, ...]] = {
|
||||
"auth": ("auth", "security"),
|
||||
"wallet": ("wallet", "security"),
|
||||
"route": ("routing",),
|
||||
"stream": ("streaming",),
|
||||
"cache": ("cache",),
|
||||
"performance": ("performance",),
|
||||
"benchmark": ("performance",),
|
||||
"persist": ("persistence",),
|
||||
"gossip": ("gossip", "network"),
|
||||
}
|
||||
|
||||
|
||||
class TestRunnerError(Exception):
|
||||
"""Base class; carries the HTTP status the handler should send."""
|
||||
@@ -116,6 +155,28 @@ def _test_python() -> str:
|
||||
return sys.executable
|
||||
|
||||
|
||||
def _function_description(path: Path, function_name: str) -> str:
|
||||
"""Return a concise source docstring, falling back to the test name."""
|
||||
try:
|
||||
tree = ast.parse(path.read_text())
|
||||
base_name = function_name.split("[")[0]
|
||||
function = next(
|
||||
(node for node in ast.walk(tree)
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
|
||||
and node.name == base_name),
|
||||
None,
|
||||
)
|
||||
if function is not None:
|
||||
doc = ast.get_docstring(function)
|
||||
if doc:
|
||||
return " ".join(doc.split())
|
||||
except (OSError, SyntaxError):
|
||||
pass
|
||||
words = re.sub(r"([a-z])([A-Z])", r"\\1 \\2", function_name.split("[")[0])
|
||||
words = words.replace("_", " ").strip()
|
||||
return words[:1].upper() + words[1:] if words else "Test"
|
||||
|
||||
|
||||
class TestRunManager:
|
||||
"""Collects pytest node IDs and runs one fixed target at a time."""
|
||||
|
||||
@@ -133,6 +194,7 @@ class TestRunManager:
|
||||
self.run_timeout = run_timeout
|
||||
self._lock = threading.Lock()
|
||||
self._collected: set[str] = set()
|
||||
self._metadata: dict[str, dict] = {}
|
||||
self._collected_at: float | None = None
|
||||
self._run: dict | None = None
|
||||
self._process: subprocess.Popen | None = None
|
||||
@@ -193,16 +255,45 @@ class TestRunManager:
|
||||
node_ids = [n for n in node_ids if not _REAL_INFERENCE_PATTERN.search(n.split("::", 1)[0])]
|
||||
with self._lock:
|
||||
self._collected = set(node_ids)
|
||||
self._metadata = {node_id: self._test_metadata(node_id) for node_id in node_ids}
|
||||
self._collected_at = time.time()
|
||||
return self._collection_snapshot_locked()
|
||||
|
||||
def _test_metadata(self, node_id: str) -> dict:
|
||||
module_path, function_name = node_id.split("::", 1)
|
||||
module_name = Path(module_path).stem
|
||||
tags: set[str] = set()
|
||||
for key, values in _MODULE_TAGS.items():
|
||||
if key in module_name:
|
||||
tags.update(values)
|
||||
lowered = function_name.lower()
|
||||
for key, values in _FUNCTION_TAGS.items():
|
||||
if key in lowered:
|
||||
tags.update(values)
|
||||
if not tags:
|
||||
tags.add("general")
|
||||
return {
|
||||
"id": node_id,
|
||||
"description": _function_description(self.repo_root / module_path, function_name),
|
||||
"tags": sorted(tags),
|
||||
}
|
||||
|
||||
def _collection_snapshot_locked(self) -> dict:
|
||||
tag_counts: dict[str, int] = {}
|
||||
for metadata in self._metadata.values():
|
||||
for tag in metadata["tags"]:
|
||||
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
||||
return {
|
||||
"tests": sorted(self._collected),
|
||||
"test_metadata": [self._metadata[node_id] for node_id in sorted(self._collected)],
|
||||
"suites": [
|
||||
{"id": f"suite:{name}", "name": name, "paths": paths}
|
||||
for name, paths in sorted(self.suites().items())
|
||||
],
|
||||
"tags": [
|
||||
{"id": f"tag:{tag}", "name": tag, "count": count}
|
||||
for tag, count in sorted(tag_counts.items())
|
||||
],
|
||||
"collected_at": self._collected_at,
|
||||
}
|
||||
|
||||
@@ -216,7 +307,21 @@ class TestRunManager:
|
||||
if target.startswith("-") or not _NODE_ID_RE.match(target):
|
||||
raise UnknownTargetError("target must be a collected test node ID or an approved suite")
|
||||
|
||||
if target.startswith("suite:"):
|
||||
if target == "suite:all":
|
||||
with self._lock:
|
||||
pytest_args = sorted(self._collected)
|
||||
if not pytest_args:
|
||||
raise UnknownTargetError("no collected tests are available")
|
||||
elif target.startswith("tag:"):
|
||||
tag = target.removeprefix("tag:").strip().lower()
|
||||
with self._lock:
|
||||
pytest_args = sorted(
|
||||
node_id for node_id, metadata in self._metadata.items()
|
||||
if tag in metadata["tags"]
|
||||
)
|
||||
if not pytest_args:
|
||||
raise UnknownTargetError(f"unknown or empty test tag: {tag!r}")
|
||||
elif target.startswith("suite:"):
|
||||
suite_name = target.removeprefix("suite:")
|
||||
paths = self.suites().get(suite_name)
|
||||
if paths is None:
|
||||
|
||||
Reference in New Issue
Block a user