testpulse 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
testpulse/__init__.py ADDED
@@ -0,0 +1,110 @@
1
+ """TestPulse — time your tests, optimize your outcomes."""
2
+
3
+ from testpulse.api import (
4
+ VERSION,
5
+ AggregatedCiStats,
6
+ CiUploadError,
7
+ ConfidenceBand,
8
+ DurationPercentiles,
9
+ ExcludedTestRisk,
10
+ FlakinessEstimate,
11
+ HistoryCitation,
12
+ LLMCompletionError,
13
+ LLMConfig,
14
+ OpenAICompatibleClient,
15
+ RegretHit,
16
+ ReportFilter,
17
+ ReportSnapshot,
18
+ RiskLevel,
19
+ SkipRiskReport,
20
+ TestDecisionExplanation,
21
+ aggregate_risk_level,
22
+ beta_smoothed_pass_rate,
23
+ build_aggregated_ci_stats,
24
+ build_report_snapshot,
25
+ build_test_explanation,
26
+ client_from_env,
27
+ compute_skip_risk_report,
28
+ confidence_band,
29
+ distribution_name,
30
+ enhance_explanations_with_llm,
31
+ estimate_from_counts,
32
+ explain_recommendation,
33
+ explain_with_optional_llm,
34
+ export_ci_stats_json,
35
+ flakiness_estimates,
36
+ flakiness_for_nodeid,
37
+ flaky_probability,
38
+ format_explanations_text,
39
+ gather_citations,
40
+ hash_nodeid,
41
+ import_package_name,
42
+ llm_config_from_env,
43
+ matches_filter,
44
+ nodeid_class,
45
+ recommend_pytest_command,
46
+ regret_hits,
47
+ regret_risk,
48
+ render_report_view,
49
+ run_interactive_report,
50
+ tagline,
51
+ upload_ci_stats,
52
+ upload_ci_stats_from_env,
53
+ write_ci_stats_file,
54
+ )
55
+
56
+ __all__ = [
57
+ "VERSION",
58
+ "__version__",
59
+ "AggregatedCiStats",
60
+ "CiUploadError",
61
+ "ConfidenceBand",
62
+ "DurationPercentiles",
63
+ "ExcludedTestRisk",
64
+ "FlakinessEstimate",
65
+ "HistoryCitation",
66
+ "LLMCompletionError",
67
+ "LLMConfig",
68
+ "OpenAICompatibleClient",
69
+ "RegretHit",
70
+ "ReportFilter",
71
+ "ReportSnapshot",
72
+ "RiskLevel",
73
+ "SkipRiskReport",
74
+ "TestDecisionExplanation",
75
+ "aggregate_risk_level",
76
+ "beta_smoothed_pass_rate",
77
+ "build_aggregated_ci_stats",
78
+ "build_report_snapshot",
79
+ "build_test_explanation",
80
+ "client_from_env",
81
+ "confidence_band",
82
+ "compute_skip_risk_report",
83
+ "distribution_name",
84
+ "enhance_explanations_with_llm",
85
+ "estimate_from_counts",
86
+ "explain_recommendation",
87
+ "explain_with_optional_llm",
88
+ "export_ci_stats_json",
89
+ "flaky_probability",
90
+ "flakiness_estimates",
91
+ "flakiness_for_nodeid",
92
+ "format_explanations_text",
93
+ "gather_citations",
94
+ "hash_nodeid",
95
+ "import_package_name",
96
+ "llm_config_from_env",
97
+ "matches_filter",
98
+ "nodeid_class",
99
+ "recommend_pytest_command",
100
+ "regret_hits",
101
+ "regret_risk",
102
+ "render_report_view",
103
+ "run_interactive_report",
104
+ "tagline",
105
+ "upload_ci_stats",
106
+ "upload_ci_stats_from_env",
107
+ "write_ci_stats_file",
108
+ ]
109
+
110
+ __version__ = VERSION
@@ -0,0 +1,3 @@
1
+ """Baked-in distribution name (set at build time)."""
2
+
3
+ BUILTIN_DISTRIBUTION_NAME = 'testpulse'
testpulse/_entry.py ADDED
@@ -0,0 +1,13 @@
1
+ """Console script entry that bootstraps deployment env before running CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from testpulse.config import ensure_pypi_package_name_env
6
+
7
+
8
+ def cli() -> None:
9
+ """``testtime`` entry point: ensure ``PYPI_PACKAGE_NAME`` then run the CLI."""
10
+ ensure_pypi_package_name_env()
11
+ from testpulse.cli import cli as run_cli
12
+
13
+ run_cli()
testpulse/api.py ADDED
@@ -0,0 +1,121 @@
1
+ """Stable public API for TestPulse.
2
+
3
+ Import from ``testpulse`` or ``testpulse.api``; symbols here remain backward compatible
4
+ across minor releases.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from testpulse.ci_stats import (
10
+ AggregatedCiStats,
11
+ CiUploadError,
12
+ DurationPercentiles,
13
+ build_aggregated_ci_stats,
14
+ export_ci_stats_json,
15
+ hash_nodeid,
16
+ upload_ci_stats,
17
+ upload_ci_stats_from_env,
18
+ write_ci_stats_file,
19
+ )
20
+ from testpulse.citations import HistoryCitation, gather_citations
21
+ from testpulse.config import distribution_name, import_package_name
22
+ from testpulse.explanations import (
23
+ TestDecisionExplanation,
24
+ build_test_explanation,
25
+ enhance_explanations_with_llm,
26
+ explain_recommendation,
27
+ explain_with_optional_llm,
28
+ format_explanations_text,
29
+ )
30
+ from testpulse.flakiness import (
31
+ ConfidenceBand,
32
+ FlakinessEstimate,
33
+ beta_smoothed_pass_rate,
34
+ confidence_band,
35
+ estimate_from_counts,
36
+ flakiness_estimates,
37
+ flakiness_for_nodeid,
38
+ flaky_probability,
39
+ )
40
+ from testpulse.llm_client import (
41
+ LLMCompletionError,
42
+ LLMConfig,
43
+ OpenAICompatibleClient,
44
+ client_from_env,
45
+ llm_config_from_env,
46
+ )
47
+ from testpulse.recommend import recommend_pytest_command
48
+ from testpulse.regret_hits import RegretHit, regret_hits
49
+ from testpulse.report import (
50
+ ReportFilter,
51
+ ReportSnapshot,
52
+ build_report_snapshot,
53
+ matches_filter,
54
+ nodeid_class,
55
+ render_report_view,
56
+ run_interactive_report,
57
+ )
58
+ from testpulse.risk import (
59
+ ExcludedTestRisk,
60
+ RiskLevel,
61
+ SkipRiskReport,
62
+ aggregate_risk_level,
63
+ compute_skip_risk_report,
64
+ regret_risk,
65
+ )
66
+
67
+ __all__ = [
68
+ "VERSION",
69
+ "AggregatedCiStats",
70
+ "CiUploadError",
71
+ "ConfidenceBand",
72
+ "DurationPercentiles",
73
+ "ExcludedTestRisk",
74
+ "FlakinessEstimate",
75
+ "HistoryCitation",
76
+ "LLMCompletionError",
77
+ "LLMConfig",
78
+ "OpenAICompatibleClient",
79
+ "RegretHit",
80
+ "ReportFilter",
81
+ "ReportSnapshot",
82
+ "RiskLevel",
83
+ "SkipRiskReport",
84
+ "TestDecisionExplanation",
85
+ "aggregate_risk_level",
86
+ "beta_smoothed_pass_rate",
87
+ "build_aggregated_ci_stats",
88
+ "build_report_snapshot",
89
+ "build_test_explanation",
90
+ "client_from_env",
91
+ "confidence_band",
92
+ "compute_skip_risk_report",
93
+ "distribution_name",
94
+ "enhance_explanations_with_llm",
95
+ "estimate_from_counts",
96
+ "explain_recommendation",
97
+ "explain_with_optional_llm",
98
+ "export_ci_stats_json",
99
+ "flaky_probability",
100
+ "flakiness_estimates",
101
+ "flakiness_for_nodeid",
102
+ "format_explanations_text",
103
+ "gather_citations",
104
+ "hash_nodeid",
105
+ "import_package_name",
106
+ "llm_config_from_env",
107
+ "matches_filter",
108
+ "nodeid_class",
109
+ "recommend_pytest_command",
110
+ "regret_hits",
111
+ "regret_risk",
112
+ "render_report_view",
113
+ "run_interactive_report",
114
+ "tagline",
115
+ "upload_ci_stats",
116
+ "upload_ci_stats_from_env",
117
+ "write_ci_stats_file",
118
+ ]
119
+
120
+ VERSION = "0.1.0"
121
+ tagline = "Time your tests, optimize your outcomes."
@@ -0,0 +1,148 @@
1
+ """Local user store and password hashing (stdlib only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import hmac
7
+ import os
8
+ import secrets
9
+ import sqlite3
10
+ import time
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+
15
+ def _auth_db_path() -> Path:
16
+ override = os.environ.get("TESTPULSE_AUTH_DB", "").strip()
17
+ if override:
18
+ return Path(override).expanduser()
19
+ return Path(".testpulse/auth.sqlite3")
20
+
21
+
22
+ def _hash_password(password: str, *, salt: bytes | None = None) -> str:
23
+ salt_bytes = salt or secrets.token_bytes(16)
24
+ digest = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt_bytes, 120_000)
25
+ return f"{salt_bytes.hex()}:{digest.hex()}"
26
+
27
+
28
+ def _verify_password(password: str, stored: str) -> bool:
29
+ try:
30
+ salt_hex, digest_hex = stored.split(":", 1)
31
+ except ValueError:
32
+ return False
33
+ salt = bytes.fromhex(salt_hex)
34
+ expected = bytes.fromhex(digest_hex)
35
+ actual = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 120_000)
36
+ return hmac.compare_digest(actual, expected)
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class UserRecord:
41
+ id: int
42
+ email: str
43
+
44
+
45
+ class AuthStore:
46
+ """SQLite-backed users for the optional HTTP API."""
47
+
48
+ def __init__(self, path: Path | None = None) -> None:
49
+ self._path = path or _auth_db_path()
50
+ self._path.parent.mkdir(parents=True, exist_ok=True)
51
+ self._conn = sqlite3.connect(self._path, check_same_thread=False)
52
+ self._conn.execute(
53
+ """
54
+ CREATE TABLE IF NOT EXISTS users (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ email TEXT NOT NULL UNIQUE,
57
+ password_hash TEXT NOT NULL,
58
+ created_at REAL NOT NULL
59
+ )
60
+ """
61
+ )
62
+ self._conn.commit()
63
+
64
+ def close(self) -> None:
65
+ self._conn.close()
66
+
67
+ def create_user(self, email: str, password: str) -> UserRecord:
68
+ normalized = email.strip().lower()
69
+ if not normalized or not password:
70
+ msg = "email and password are required"
71
+ raise ValueError(msg)
72
+ try:
73
+ cursor = self._conn.execute(
74
+ "INSERT INTO users (email, password_hash, created_at) VALUES (?, ?, ?)",
75
+ (normalized, _hash_password(password), time.time()),
76
+ )
77
+ except sqlite3.IntegrityError as exc:
78
+ msg = "email already registered"
79
+ raise ValueError(msg) from exc
80
+ self._conn.commit()
81
+ row_id = cursor.lastrowid
82
+ if not isinstance(row_id, int):
83
+ msg = "failed to create user"
84
+ raise RuntimeError(msg)
85
+ return UserRecord(id=row_id, email=normalized)
86
+
87
+ def authenticate(self, email: str, password: str) -> UserRecord | None:
88
+ normalized = email.strip().lower()
89
+ row = self._conn.execute(
90
+ "SELECT id, email, password_hash FROM users WHERE email = ?",
91
+ (normalized,),
92
+ ).fetchone()
93
+ if row is None:
94
+ return None
95
+ user_id, stored_email, password_hash = row
96
+ if not _verify_password(password, password_hash):
97
+ return None
98
+ return UserRecord(id=int(user_id), email=str(stored_email))
99
+
100
+ def get_user(self, user_id: int) -> UserRecord | None:
101
+ row = self._conn.execute(
102
+ "SELECT id, email FROM users WHERE id = ?",
103
+ (user_id,),
104
+ ).fetchone()
105
+ if row is None:
106
+ return None
107
+ return UserRecord(id=int(row[0]), email=str(row[1]))
108
+
109
+
110
+ def jwt_secret() -> str | None:
111
+ secret = os.environ.get("JWT_SECRET", "").strip()
112
+ return secret or None
113
+
114
+
115
+ def issue_token(user_id: int, *, ttl_seconds: int = 3600) -> str:
116
+ secret = jwt_secret()
117
+ if secret is None:
118
+ msg = "JWT_SECRET is not configured"
119
+ raise RuntimeError(msg)
120
+ expires = int(time.time()) + ttl_seconds
121
+ payload = f"{user_id}:{expires}"
122
+ signature = hmac.new(
123
+ secret.encode("utf-8"),
124
+ payload.encode("utf-8"),
125
+ hashlib.sha256,
126
+ ).hexdigest()
127
+ return f"{payload}:{signature}"
128
+
129
+
130
+ def verify_token(token: str) -> int | None:
131
+ secret = jwt_secret()
132
+ if secret is None:
133
+ return None
134
+ try:
135
+ user_part, expires_part, signature = token.split(":", 2)
136
+ payload = f"{user_part}:{expires_part}"
137
+ except ValueError:
138
+ return None
139
+ expected = hmac.new(
140
+ secret.encode("utf-8"),
141
+ payload.encode("utf-8"),
142
+ hashlib.sha256,
143
+ ).hexdigest()
144
+ if not hmac.compare_digest(expected, signature):
145
+ return None
146
+ if int(expires_part) < int(time.time()):
147
+ return None
148
+ return int(user_part)
@@ -0,0 +1 @@
1
+ PYPI_PACKAGE_NAME=testpulse
testpulse/ci_stats.py ADDED
@@ -0,0 +1,166 @@
1
+ """Aggregated, de-identified CI stats export and optional upload."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import urllib.error
9
+ import urllib.request
10
+ from dataclasses import asdict, dataclass
11
+ from datetime import UTC, datetime
12
+ from pathlib import Path
13
+ from typing import Literal
14
+
15
+ from testpulse.flakiness import flakiness_estimates
16
+ from testpulse.store import Store
17
+
18
+ SCHEMA_VERSION = 1
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class DurationPercentiles:
23
+ """Aggregate duration percentiles in milliseconds."""
24
+
25
+ p50: float
26
+ p90: float
27
+ p95: float
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class AggregatedCiStats:
32
+ """De-identified timing/outcome aggregates safe for CI upload."""
33
+
34
+ schema_version: int
35
+ generated_at: str
36
+ run_count: int
37
+ recorded_result_rows: int
38
+ outcome_totals: dict[str, int]
39
+ duration_ms: DurationPercentiles
40
+ flaky_bucket_totals: dict[str, int]
41
+ unique_tests_hashed: int
42
+
43
+
44
+ def _stats_salt() -> str:
45
+ return os.environ.get("TESTPULSE_STATS_SALT", "testpulse-ci").strip()
46
+
47
+
48
+ def hash_nodeid(nodeid: str) -> str:
49
+ """Return a stable hashed identifier (no raw nodeid)."""
50
+ digest = hashlib.sha256(f"{_stats_salt()}:{nodeid}".encode()).hexdigest()
51
+ return digest[:16]
52
+
53
+
54
+ def _percentile(values: list[float], pct: float) -> float:
55
+ if not values:
56
+ return 0.0
57
+ ordered = sorted(values)
58
+ index = int(round((pct / 100.0) * (len(ordered) - 1)))
59
+ return float(ordered[index])
60
+
61
+
62
+ def _flaky_bucket(probability: float) -> Literal["low", "medium", "high"]:
63
+ if probability < 0.25:
64
+ return "low"
65
+ if probability < 0.55:
66
+ return "medium"
67
+ return "high"
68
+
69
+
70
+ def build_aggregated_ci_stats(store: Store) -> AggregatedCiStats:
71
+ """Aggregate local store metrics without logs, secrets, or raw nodeids."""
72
+ runs = store.list_runs()
73
+ timing = store.timing_stats()
74
+ durations = [row.avg_duration_ms for row in timing]
75
+ outcome_totals = {"passed": 0, "failed": 0, "skipped": 0, "other": 0}
76
+ for row in store.outcome_counts():
77
+ _ = hash_nodeid(row.nodeid)
78
+ total = row.passes + row.fails
79
+ outcome_totals["passed"] += row.passes
80
+ outcome_totals["failed"] += row.fails
81
+ if total == 0:
82
+ outcome_totals["other"] += 1
83
+
84
+ flaky_buckets = {"low": 0, "medium": 0, "high": 0}
85
+ for estimate in flakiness_estimates(store):
86
+ bucket = _flaky_bucket(estimate.flaky_probability)
87
+ flaky_buckets[bucket] += 1
88
+
89
+ recorded_rows = sum(row.runs for row in timing)
90
+ return AggregatedCiStats(
91
+ schema_version=SCHEMA_VERSION,
92
+ generated_at=datetime.now(tz=UTC).isoformat(),
93
+ run_count=len(runs),
94
+ recorded_result_rows=recorded_rows,
95
+ outcome_totals=outcome_totals,
96
+ duration_ms=DurationPercentiles(
97
+ p50=_percentile(durations, 50),
98
+ p90=_percentile(durations, 90),
99
+ p95=_percentile(durations, 95),
100
+ ),
101
+ flaky_bucket_totals=flaky_buckets,
102
+ unique_tests_hashed=len(timing),
103
+ )
104
+
105
+
106
+ def export_ci_stats_json(store: Store) -> str:
107
+ """Serialize de-identified stats for GitHub Actions artifacts or upload."""
108
+ payload = asdict(build_aggregated_ci_stats(store))
109
+ return json.dumps(payload, indent=2, sort_keys=True)
110
+
111
+
112
+ def write_ci_stats_file(path: Path | str, store: Store) -> Path:
113
+ """Write de-identified stats JSON to ``path``."""
114
+ target = Path(path)
115
+ target.parent.mkdir(parents=True, exist_ok=True)
116
+ target.write_text(export_ci_stats_json(store), encoding="utf-8")
117
+ return target
118
+
119
+
120
+ class CiUploadError(RuntimeError):
121
+ """Raised when CI stats upload fails."""
122
+
123
+
124
+ def upload_ci_stats(
125
+ store: Store,
126
+ *,
127
+ endpoint_url: str,
128
+ token: str,
129
+ timeout_seconds: float = 30.0,
130
+ ) -> None:
131
+ """POST de-identified stats JSON to an HTTPS endpoint (optional GitHub workflow step).
132
+
133
+ The caller supplies ``endpoint_url`` and ``token`` (for example repository webhook or
134
+ GitHub API proxy). No test logs or secrets are included in the payload.
135
+ """
136
+ payload = export_ci_stats_json(store).encode("utf-8")
137
+ request = urllib.request.Request(
138
+ endpoint_url,
139
+ data=payload,
140
+ method="POST",
141
+ headers={
142
+ "Authorization": f"Bearer {token}",
143
+ "Content-Type": "application/json",
144
+ "User-Agent": "TestPulse-ci-stats",
145
+ },
146
+ )
147
+ try:
148
+ with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
149
+ if response.status >= 300:
150
+ msg = f"upload failed with status {response.status}"
151
+ raise CiUploadError(msg)
152
+ except urllib.error.HTTPError as exc:
153
+ detail = exc.read().decode("utf-8", errors="replace")
154
+ raise CiUploadError(detail or str(exc)) from exc
155
+ except urllib.error.URLError as exc:
156
+ raise CiUploadError(str(exc)) from exc
157
+
158
+
159
+ def upload_ci_stats_from_env(store: Store) -> bool:
160
+ """Upload stats when upload URL and token environment variables are set."""
161
+ url = os.environ.get("TESTPULSE_STATS_UPLOAD_URL", "").strip()
162
+ token = os.environ.get("TESTPULSE_STATS_UPLOAD_TOKEN", "").strip()
163
+ if not url or not token:
164
+ return False
165
+ upload_ci_stats(store, endpoint_url=url, token=token)
166
+ return True
testpulse/citations.py ADDED
@@ -0,0 +1,115 @@
1
+ """Local SQLite citation records for explainable recommendations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from testpulse.heuristics import static_pytest_targets
8
+ from testpulse.store import Store
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class HistoryCitation:
13
+ """Pointer to a single local history record used in an explanation."""
14
+
15
+ cite_id: str
16
+ table: str
17
+ record_key: str
18
+ summary: str
19
+
20
+
21
+ def nodeid_matches_target(target: str, nodeid: str) -> bool:
22
+ """Return whether ``nodeid`` equals or lives under pytest target ``target``."""
23
+ return nodeid == target or nodeid.startswith(f"{target}::")
24
+
25
+
26
+ def related_changed_paths(target: str, changed_paths: list[str]) -> list[str]:
27
+ """Return changed files that statically map to ``target``."""
28
+ related: list[str] = []
29
+ for path in changed_paths:
30
+ if target in static_pytest_targets(path):
31
+ related.append(path)
32
+ return related
33
+
34
+
35
+ def gather_citations(
36
+ store: Store,
37
+ changed_paths: list[str],
38
+ target: str,
39
+ *,
40
+ max_citations: int = 5,
41
+ ) -> tuple[HistoryCitation, ...]:
42
+ """Collect cite-able rows from the SQLite store for one pytest target."""
43
+ citations: list[HistoryCitation] = []
44
+ index = 1
45
+
46
+ for path in related_changed_paths(target, changed_paths):
47
+ citations.append(
48
+ HistoryCitation(
49
+ cite_id=f"C{index}",
50
+ table="heuristic",
51
+ record_key=path,
52
+ summary=f"Static map from changed file {path!r} to {target!r}",
53
+ )
54
+ )
55
+ index += 1
56
+
57
+ linked_nodeids = {target}
58
+ for nodeid, _score in store.cooccurrence_candidates(changed_paths):
59
+ if nodeid_matches_target(target, nodeid):
60
+ linked_nodeids.add(nodeid)
61
+
62
+ seen_links: set[str] = set()
63
+ for nodeid in sorted(linked_nodeids):
64
+ for path in store.linking_paths_for_test(changed_paths, nodeid):
65
+ key = f"{path}|{nodeid}"
66
+ if key in seen_links:
67
+ continue
68
+ seen_links.add(key)
69
+ citations.append(
70
+ HistoryCitation(
71
+ cite_id=f"C{index}",
72
+ table="change_test_links",
73
+ record_key=key,
74
+ summary=f"Learned co-occurrence between {path!r} and {nodeid!r}",
75
+ )
76
+ )
77
+ index += 1
78
+
79
+ counts = next(
80
+ (row for row in store.outcome_counts() if nodeid_matches_target(target, row.nodeid)),
81
+ None,
82
+ )
83
+ if counts is not None:
84
+ citations.append(
85
+ HistoryCitation(
86
+ cite_id=f"C{index}",
87
+ table="test_results",
88
+ record_key=counts.nodeid,
89
+ summary=(
90
+ f"Recorded history for {counts.nodeid!r}: "
91
+ f"{counts.passes} pass(es), {counts.fails} fail(s)"
92
+ ),
93
+ )
94
+ )
95
+ index += 1
96
+
97
+ timing = next(
98
+ (row for row in store.timing_stats() if nodeid_matches_target(target, row.nodeid)),
99
+ None,
100
+ )
101
+ if timing is not None:
102
+ citations.append(
103
+ HistoryCitation(
104
+ cite_id=f"C{index}",
105
+ table="test_results",
106
+ record_key=f"{target}:timing",
107
+ summary=(
108
+ f"Average duration {timing.avg_duration_ms:.1f} ms "
109
+ f"over {timing.runs} run(s)"
110
+ ),
111
+ )
112
+ )
113
+ index += 1
114
+
115
+ return tuple(citations[:max_citations])