mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-25 21:20:00 +00:00
Retain bounded API shard failure identities beside timings
Publish failed and unfinished source-test names in the existing shard artifact without waiting for other workflow jobs. Keep logs, timings, test selection and failure exits unchanged; record unknown completion and omitted unsafe or excess identities explicitly. Pin privacy, bounds, pipefail and timing-refresh compatibility, and align the existing shard contract with current compile admission. Change-source: pulse-maintainer
This commit is contained in:
@@ -17,6 +17,12 @@ holding the whole test binary's run time, the same shape as
|
||||
.github/scripts/internal-api-test-seconds.txt, so
|
||||
refresh-internal-api-test-seconds.py can rebuild the shard weights from CI.
|
||||
|
||||
A bounded <seconds-file>.failures.json sidecar names failed and unfinished
|
||||
source-level tests without test output or parameterised subtest labels. The
|
||||
existing timing-artifact upload makes it readable as soon as the shard ends,
|
||||
even while another workflow job is still running. It is an observation index,
|
||||
not a passing verdict: absent package completion and omitted names are explicit.
|
||||
|
||||
The exit status is non-zero when any test or package failed, but the caller
|
||||
must still run with `set -o pipefail` so a `go test` failure that produced no
|
||||
JSON (for example a vet or build error) fails the step.
|
||||
@@ -25,10 +31,26 @@ JSON (for example a vet or build error) fails the step.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
MAX_FAILURE_IDENTIFIERS = 256
|
||||
MAX_IDENTIFIER_LENGTH = 256
|
||||
SOURCE_TEST_NAME = re.compile(r"(?:Test|Fuzz|Example)[A-Za-z0-9_]*")
|
||||
|
||||
|
||||
def bounded_identifiers(names: set[str]) -> dict[str, object]:
|
||||
"""Never export output, subtest parameters or unsafe source names."""
|
||||
safe = sorted(name for name in names
|
||||
if len(name) <= MAX_IDENTIFIER_LENGTH
|
||||
and SOURCE_TEST_NAME.fullmatch(name))
|
||||
retained = safe[:MAX_FAILURE_IDENTIFIERS]
|
||||
return {"names": retained, "observed_count": len(names),
|
||||
"omitted_count": len(names) - len(retained)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print(f"usage: {sys.argv[0]} <seconds-file>", file=sys.stderr)
|
||||
@@ -39,6 +61,10 @@ def main() -> int:
|
||||
seconds: dict[str, float] = {}
|
||||
package_seconds = None
|
||||
failed = False
|
||||
failed_tests: set[str] = set()
|
||||
package_terminal_action = None
|
||||
package_terminal_count = 0
|
||||
non_json_lines = 0
|
||||
out = sys.stdout
|
||||
|
||||
for raw in sys.stdin:
|
||||
@@ -47,6 +73,7 @@ def main() -> int:
|
||||
except ValueError:
|
||||
event = None
|
||||
if not isinstance(event, dict):
|
||||
non_json_lines += 1
|
||||
out.write(raw)
|
||||
out.flush()
|
||||
continue
|
||||
@@ -62,6 +89,9 @@ def main() -> int:
|
||||
if text:
|
||||
out.write(text)
|
||||
out.flush()
|
||||
if action in ("pass", "fail", "skip"):
|
||||
package_terminal_action = action
|
||||
package_terminal_count += 1
|
||||
if action == "fail":
|
||||
failed = True
|
||||
if action in ("pass", "fail") and isinstance(event.get("Elapsed"), (int, float)):
|
||||
@@ -79,6 +109,7 @@ def main() -> int:
|
||||
lines = held.pop(top, [])
|
||||
if action == "fail":
|
||||
failed = True
|
||||
failed_tests.add(top)
|
||||
out.write("".join(lines))
|
||||
out.flush()
|
||||
elapsed = event.get("Elapsed")
|
||||
@@ -97,6 +128,23 @@ def main() -> int:
|
||||
for name, value in seconds.items():
|
||||
handle.write(f"{name} {value:.2f}\n")
|
||||
|
||||
# Upload alongside the unchanged timing text; never include raw output or
|
||||
# parameterised subtest names. The CI artifact supplies run/source binding.
|
||||
index = {
|
||||
"schema_version": 1,
|
||||
"recorder_exit_code": 1 if failed else 0,
|
||||
"failed_top_level_tests": bounded_identifiers(failed_tests),
|
||||
"unfinished_top_level_tests": bounded_identifiers(running),
|
||||
# Only the final package action matters; retain contrary earlier
|
||||
# actions as a count instead of exporting an unbounded event stream.
|
||||
"package_terminal_action": package_terminal_action,
|
||||
"package_terminal_count": package_terminal_count,
|
||||
"non_json_line_count": non_json_lines,
|
||||
}
|
||||
with open(sys.argv[1] + ".failures.json", "w", encoding="utf-8") as handle:
|
||||
json.dump(index, handle, indent=2, sort_keys=True)
|
||||
handle.write("\n")
|
||||
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user