Compress release backend test selectors

This commit is contained in:
rcourtman 2026-08-21 17:04:18 +01:00
parent 418402bf9e
commit 1327dddad5
6 changed files with 139 additions and 25 deletions

View file

@ -604,11 +604,15 @@ upgrade, update, release, or artifact-selection behavior.
deterministic manifest proving a complete, disjoint partition. Each
partition must preserve the exact order emitted by that binary; sorting or
hash distribution is not equivalent while legacy tests still exercise
package-global state. Each shard must split only at deterministic contiguous
batch boundaries whose encoded `-test.run` regex stays below the configured
per-argument byte ceiling, so a memory-driven one-shard fallback cannot
exceed the operating system's exec limit before tests begin. Two API shards
and the remaining Go packages may then
package-global state. The planner must prefix-compress the exact test-name
regex and keep a shard in one test-binary process whenever that encoded
argument fits below the configured byte ceiling. That ceiling must never
exceed 120,000 bytes, retaining explicit headroom below Linux's 131,072-byte
per-argument limit. A shard may split only at deterministic contiguous
batch boundaries when its compressed exact-name regex exceeds the configured
ceiling. Tests at those boundaries must initialize their own package-global
prerequisites rather than inheriting state from a prior process. Two API
shards and the remaining Go packages may then
execute concurrently with isolated data directories. A failed shard must
fail the complete backend gate and terminate every descendant test process;
sharding must never select a coverage subset or leave orphan race binaries
@ -1612,6 +1616,19 @@ deterministic contiguous batch by encoded regex bytes as well as test count.
That failed run and its inert private staging child were cancelled; no public
tag or GitHub release was created, and the corrected candidate must run from a
new exact source SHA.
Release run `32497250921` then proved that naïve byte-bounded alternation was
not semantically sufficient: its third fresh test-binary process reached
`TestEstablishSession` without the package-global session store historically
initialized earlier in the ordered suite. Prefix compression reduces the exact
3,736-test expression from 182,429 bytes to 110,384 bytes, so the one-shard
fallback needs only two bounded processes at the default 65,536-byte ceiling
instead of three naïve alternation batches. The two session-establishment tests
now initialize their own persistent session store, removing the boundary-order
dependency instead of relying on a larger process. The planner still records
the complete test-name digest, proves each compressed expression is an exact
ordered partition, and rejects any configured ceiling above 120,000 bytes.
That failed run and its private child were cancelled before publication; no
public tag or release was created.
The publish body keeps its `Highlights` section within the governed three-item
limit while preserving the packet's operator-facing summary of read-only
observer coverage, estate-first platform search and facets, and real delivery-

View file

@ -65,9 +65,9 @@ source commit that passed qualification.
- Candidate qualification now runs even when intentionally disabled native
signing jobs are skipped, while still failing closed on a required signing
failure.
- Backend qualification now byte-bounds deterministic API test batches, so a
memory-driven one-shard fallback cannot exceed the operating system's exec
argument limit before any tests run.
- Backend qualification prefix-compresses the exact API test selector, keeping
a memory-driven one-shard fallback in one ordered process when safe while
still byte-bounding deterministic batches below the operating system limit.
## Security

View file

@ -1604,6 +1604,7 @@ func TestHandleStorageSummaryCharts_AggregatesCapacityAcrossPools(t *testing.T)
}
func TestEstablishSession(t *testing.T) {
InitPersistentAuthStores(t.TempDir())
router := &Router{}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
@ -1619,6 +1620,7 @@ func TestEstablishSession(t *testing.T) {
}
func TestEstablishOIDCSession(t *testing.T) {
InitPersistentAuthStores(t.TempDir())
router := &Router{}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import os
import pathlib
import re
import subprocess
import sys
import tempfile
@ -12,7 +13,7 @@ import unittest
ROOT = pathlib.Path(__file__).resolve().parents[3]
sys.path.insert(0, str(ROOT / "scripts"))
from shard_go_tests import build_plan, write_plan
from shard_go_tests import _test_regex, build_plan, write_plan
class ReleasePreflightTest(unittest.TestCase):
@ -149,10 +150,12 @@ class ReleasePreflightTest(unittest.TestCase):
build_plan(["TestDuplicate", "TestDuplicate"], 2, 10)
with self.assertRaisesRegex(ValueError, "cannot exceed"):
build_plan(["TestOnly"], 2, 10)
with self.assertRaisesRegex(ValueError, "safe per-argument ceiling"):
build_plan(["TestOnly"], 1, 10, max_regex_bytes=120_001)
def test_api_shard_plan_bounds_one_shard_regex_argument_bytes(self) -> None:
test_names = [
f"TestReleaseIntegrationCase{index:04d}{'X' * 64}"
f"Test{index:04d}{index * 7919:08d}{index * 104729:010d}{'X' * 64}"
for index in range(3736)
]
max_regex_bytes = 64 * 1024
@ -187,6 +190,30 @@ class ReleasePreflightTest(unittest.TestCase):
max_regex_bytes=8,
)
def test_api_shard_regex_compresses_shared_names_without_changing_membership(
self,
) -> None:
test_names = [
"TestHandleCharts_Success",
"TestHandleCharts_MethodNotAllowed",
"TestHandleStorageCharts_Success",
"TestHandleStorageCharts_MethodNotAllowed",
"TestRouterPublicPathsInventory",
]
regex = _test_regex(test_names)
naive = "^(?:" + "|".join(test_names) + ")$"
self.assertLess(len(regex.encode()), len(naive.encode()))
compiled = re.compile(regex)
for test_name in test_names:
self.assertIsNotNone(compiled.fullmatch(test_name))
for excluded in [
"TestHandleCharts",
"TestHandleCharts_Failure",
"TestRouterPublicPathsInventoryExtra",
]:
self.assertIsNone(compiled.fullmatch(excluded))
def test_release_workflow_uses_independent_pve_bundle_and_backend_lanes(self) -> None:
workflow = (ROOT / ".github/workflows/create-release.yml").read_text()
backend = (ROOT / "scripts/run-release-backend-tests.sh").read_text()

View file

@ -61,6 +61,10 @@ if [[ ! "$MAX_REGEX_BYTES" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: --max-regex-bytes must be a positive integer." >&2
exit 2
fi
if [ "$MAX_REGEX_BYTES" -gt 120000 ]; then
echo "Error: --max-regex-bytes cannot exceed the safe 120000-byte per-argument ceiling." >&2
exit 2
fi
for command_name in go python3 getconf awk pgrep; do
if ! command -v "$command_name" >/dev/null 2>&1; then

View file

@ -12,38 +12,88 @@ from typing import Sequence
TEST_NAME_PATTERN = re.compile(r"^Test[A-Za-z0-9_]+$")
MAX_SAFE_REGEX_BYTES = 120_000
DEFAULT_MAX_REGEX_BYTES = 64 * 1024
class _RegexTrie(dict[str, "_RegexTrie"]):
terminal: bool
def __init__(self) -> None:
super().__init__()
self.terminal = False
def _digest(names: Sequence[str]) -> str:
payload = "".join(f"{name}\n" for name in names).encode()
return hashlib.sha256(payload).hexdigest()
def _test_regex(names: Sequence[str]) -> str:
return "^(?:" + "|".join(re.escape(name) for name in names) + ")$"
root = _RegexTrie()
for name in names:
node = root
for character in name:
node = node.setdefault(character, _RegexTrie())
node.terminal = True
def render(node: _RegexTrie) -> str:
suffix_groups: dict[str, list[str]] = {}
for character, child in sorted(node.items()):
suffix_groups.setdefault(render(child), []).append(character)
alternatives: list[str] = []
for suffix, characters in sorted(suffix_groups.items()):
if len(characters) == 1:
prefix = re.escape(characters[0])
else:
prefix = (
"["
+ "".join(re.escape(value) for value in characters)
+ "]"
)
alternatives.append(prefix + suffix)
if node.terminal:
if not alternatives:
return ""
body = (
alternatives[0]
if len(alternatives) == 1
else "(?:" + "|".join(alternatives) + ")"
)
return "(?:" + body + ")?"
if len(alternatives) == 1:
return alternatives[0]
return "(?:" + "|".join(alternatives) + ")"
return "^(?:" + render(root) + ")$"
def _batch_names(
names: Sequence[str], batch_size: int, max_regex_bytes: int
) -> list[list[str]]:
batches: list[list[str]] = []
current: list[str] = []
for name in names:
candidate = [*current, name]
candidate_bytes = len(_test_regex(candidate).encode())
if current and (len(candidate) > batch_size or candidate_bytes > max_regex_bytes):
batches.append(current)
current = [name]
candidate_bytes = len(_test_regex(current).encode())
else:
current = candidate
if candidate_bytes > max_regex_bytes:
offset = 0
while offset < len(names):
if len(_test_regex([names[offset]]).encode()) > max_regex_bytes:
raise ValueError(
f"top-level Go test name exceeds max regex bytes: {name}"
f"top-level Go test name exceeds max regex bytes: {names[offset]}"
)
if current:
batches.append(current)
low = offset + 1
high = min(len(names), offset + batch_size)
best = low
while low <= high:
midpoint = (low + high) // 2
encoded_bytes = len(_test_regex(names[offset:midpoint]).encode())
if encoded_bytes <= max_regex_bytes:
best = midpoint
low = midpoint + 1
else:
high = midpoint - 1
batches.append(list(names[offset:best]))
offset = best
return batches
@ -59,6 +109,11 @@ def build_plan(
raise ValueError("batch_size must be at least 1")
if max_regex_bytes < 1:
raise ValueError("max_regex_bytes must be at least 1")
if max_regex_bytes > MAX_SAFE_REGEX_BYTES:
raise ValueError(
f"max_regex_bytes cannot exceed the safe per-argument ceiling "
f"of {MAX_SAFE_REGEX_BYTES}"
)
if not test_names:
raise ValueError("the Go package did not expose any top-level tests")
if shard_count > len(test_names):
@ -92,6 +147,15 @@ def build_plan(
ordered_names = list(names)
assigned.extend(ordered_names)
batches = _batch_names(ordered_names, batch_size, max_regex_bytes)
for batch in batches:
pattern = re.compile(_test_regex(batch))
matched_names = [
name for name in canonical if pattern.fullmatch(name) is not None
]
if matched_names != batch:
raise RuntimeError(
"generated test regex is not an exact ordered partition"
)
shard_records.append(
{
"index": shard_index,