test grouping

This commit is contained in:
Dobromir Popov
2026-07-11 22:11:21 +03:00
parent c195b5ce78
commit 7d259d7c9b
9 changed files with 1424 additions and 19 deletions

View File

@@ -298,8 +298,13 @@
<h2>Tests &amp; suites</h2>
<div class="testing-controls">
<input id="testing-filter" type="search" placeholder="filter tests…" oninput="renderTestTargets()" aria-label="Filter tests">
<select id="testing-tag-filter" onchange="renderTestTargets()" aria-label="filter by test tag">
<option value="">all tags</option>
</select>
<button class="small" type="button" data-test-target="suite:all">Run all tests</button>
<button class="small" type="button" onclick="reloadTestTargets()">re-collect</button>
</div>
<div id="testing-tag-actions" class="testing-tags"></div>
<div id="testing-targets" class="empty">admin login required</div>
</section>
<section data-tab="testing" data-admin-only class="wide">
@@ -1016,7 +1021,7 @@ function renderConsole(data) {
// ---- testing tab (opt-in tracker test runner, dashboard-test-runner US-002) ----
let testCollection = { tests: [], suites: [] };
let testCollection = { tests: [], test_metadata: [], suites: [], tags: [] };
let testRun = null;
let testRunStarting = false;
@@ -1122,21 +1127,39 @@ function renderTestTargets() {
const el = $("testing-targets");
if (!el) return;
const filter = ($("testing-filter")?.value || "").trim().toLowerCase();
const selectedTag = ($("testing-tag-filter")?.value || "").trim().toLowerCase();
const metadata = testCollection.test_metadata || testCollection.tests.map(id => ({ id, description: id, tags: [] }));
const suites = (testCollection.suites || [])
.map(s => ({ id: s.id, label: `${s.name} (${(s.paths || []).join(", ")})`, suite: true }));
const tests = (testCollection.tests || []).map(t => ({ id: t, label: t, suite: false }));
.map(s => ({ id: s.id, label: `${s.name} (${(s.paths || []).join(", ")})`, description: "Approved test suite", tags: ["suite"], suite: true }));
const tests = metadata.map(t => ({ ...t, label: t.id, suite: false }));
const targets = [...suites, ...tests]
.filter(t => !filter || t.label.toLowerCase().includes(filter));
.filter(t => !filter || `${t.label} ${t.description} ${(t.tags || []).join(" ")}`.toLowerCase().includes(filter))
.filter(t => !selectedTag || t.suite || (t.tags || []).includes(selectedTag));
const disabled = testRunActive() ? " disabled" : "";
const tagFilter = $("testing-tag-filter");
if (tagFilter) {
const value = tagFilter.value;
tagFilter.innerHTML = '<option value="">all tags</option>' +
(testCollection.tags || []).map(t => `<option value="${esc(t.name)}">${esc(t.name)} (${esc(t.count)})</option>`).join("");
tagFilter.value = value;
}
const tagActions = $("testing-tag-actions");
if (tagActions) {
tagActions.innerHTML = (testCollection.tags || []).map(t =>
`<button class="small" type="button" data-test-target="${esc(t.id)}"${disabled}>Run ${esc(t.name)} (${esc(t.count)})</button>`
).join("");
}
if (!targets.length) {
el.className = "empty";
el.innerHTML = filter ? "no targets match the filter" : "no tests collected";
el.innerHTML = filter || selectedTag ? "no tests match the filter" : "no tests collected";
return;
}
const disabled = testRunActive() ? " disabled" : "";
el.className = "testing-list";
el.innerHTML = targets.map(t =>
`<div class="testing-row">` +
`<span class="testing-target">${t.suite ? '<span class="pill">suite</span> ' : ""}${esc(t.label)}</span>` +
`<span class="testing-target"><strong>${t.suite ? '<span class="pill">suite</span> ' : ""}${esc(t.label)}</strong>` +
`<br><span class="dim">${esc(t.description || "")}</span>` +
`${(t.tags || []).map(tag => ` <span class="pill">${esc(tag)}</span>`).join("")}</span>` +
`<button class="small" type="button" data-test-target="${esc(t.id)}"${disabled}>Run</button>` +
`</div>`
).join("");
@@ -1183,7 +1206,12 @@ async function loadTestTargets(refresh) {
$("testing-targets").innerHTML = "test targets unavailable";
return false;
}
testCollection = { tests: result.data.tests || [], suites: result.data.suites || [] };
testCollection = {
tests: result.data.tests || [],
test_metadata: result.data.test_metadata || [],
suites: result.data.suites || [],
tags: result.data.tags || [],
};
renderTestTargets();
return true;
}
@@ -1214,9 +1242,7 @@ async function pollTestRunIfActive() {
}
function bindTestingControls() {
const list = $("testing-targets");
if (!list) return;
list.addEventListener("click", event => {
document.addEventListener("click", event => {
const button = event.target.closest("[data-test-target]");
if (!button || button.disabled) return;
event.preventDefault();

View File

@@ -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: