mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
[CI] replace E1 pixel races with observed protocol wait controls
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// E1: real transport wait boundaries, with a peer that cannot complete until
|
||||
// either the client parks or its emission has already returned. No GPU or
|
||||
// scheduling-delay assumption decides whether the wait was observed.
|
||||
#if MGTEST_HAVE_FORK
|
||||
namespace {
|
||||
struct E1BoundaryPeer : Codec::WireVerbSink {
|
||||
BudgetBell& bell;
|
||||
std::atomic<Bool> clientReturned{false};
|
||||
std::atomic<Uint32> seen{0};
|
||||
MGPWireOp expected;
|
||||
explicit E1BoundaryPeer(BudgetBell& value, MGPWireOp op) : bell(value), expected(op) {}
|
||||
|
||||
Bool Apply(MGPWireOp op) {
|
||||
if (op != expected) ::_exit(84);
|
||||
++seen;
|
||||
while (!bell.entered.load() && !clientReturned.load()) std::this_thread::yield();
|
||||
return true;
|
||||
}
|
||||
Bool OnGenerateMipmap(const MGPMipPlan&) override { return Apply(MGPWireOp::GenerateMipmap); }
|
||||
Bool OnClear(const MGPClear&) override { return Apply(MGPWireOp::Clear); }
|
||||
void Install() {
|
||||
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) {
|
||||
auto& decoder = Srv::ServerSessionInstance().Applier().*PeerMember(DecoderTag{});
|
||||
decoder.SetVerbSink(static_cast<E1BoundaryPeer*>(self));
|
||||
return MOBILEGL_OK;
|
||||
}, this) != MOBILEGL_OK) ::_exit(82);
|
||||
}
|
||||
};
|
||||
|
||||
void CheckE1Boundary(Bool serverCap, MGPWireOp op, Bool expectWait, const char* diagnostic) {
|
||||
const auto child = RunInChild([=] {
|
||||
::alarm(15);
|
||||
const char* barrier = std::getenv("MOBILEGL_IPC_VERB_BARRIER");
|
||||
if (barrier && std::strcmp(barrier, "0") && std::strcmp(barrier, "1")) ::_exit(80);
|
||||
MG_Config::Ipc.VerbBarrier = barrier ? static_cast<Uint32>(barrier[0] - '0') : 1;
|
||||
MG_Config::Ipc.BatchWaits = 1;
|
||||
MG_Config::Ipc.RunAhead = 1;
|
||||
MG_Config::Ipc.SpinUs = 0;
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
const Uint64 caps = serverCap ? static_cast<Uint64>(kCapRunAheadApply) : 0;
|
||||
Srv::ServerSessionInstance().SetCapabilityBits(caps);
|
||||
Srv::ServerSessionInstance().SetConsumedSubsystems(kMGPipeSubsystemsMigratedAtP4a);
|
||||
AdoptSnapshot(MakeSnapshot(kMGPipeSubsystemsMigratedAtP4a, caps));
|
||||
auto& session = ClientSessionInstance();
|
||||
if (session.Start(MG_Config::TransportMode::InProcess, {}) != MOBILEGL_OK) ::_exit(81);
|
||||
auto& producer = session.Producer();
|
||||
BudgetBell bell(producer.SelfDoorbell());
|
||||
E1BoundaryPeer peer(bell, op);
|
||||
peer.Install();
|
||||
const Uint64 before = session.Control()->submittedSeq.load();
|
||||
producer.*PeerMember(ProducerBellTag{}) = &bell;
|
||||
const MGPMipPlan mip{};
|
||||
const MGPClear clear{};
|
||||
const Uint64 seq = op == MGPWireOp::GenerateMipmap
|
||||
? session.EmitAndWait(op, &mip, sizeof(mip), nullptr, 0, nullptr, 0, nullptr)
|
||||
: session.EmitAndWait(op, &clear, sizeof(clear), nullptr, 0, nullptr, 0, nullptr);
|
||||
// Observe BEFORE allowing the peer to complete a non-waiting emission.
|
||||
const Bool parked = bell.entered.load();
|
||||
const Bool appliedBeforeReturn = session.Control()->appliedSeq.load() >= seq;
|
||||
peer.clientReturned.store(true);
|
||||
producer.*PeerMember(ProducerBellTag{}) = bell.original;
|
||||
if (session.WaitForApplied(seq, 2000) != Transport::SessionWait::Reached) ::_exit(85);
|
||||
if (seq <= before || peer.seen.load() != 1 || session.Control()->appliedSeq.load() < seq) ::_exit(86);
|
||||
const Bool correct = expectWait ? parked && appliedBeforeReturn : !parked && !appliedBeforeReturn;
|
||||
std::printf("E1 observation: op=%s cap=%u barrier=%u parked=%u applied_before_return=%u emitted=1 peer_seen=1\n",
|
||||
op == MGPWireOp::GenerateMipmap ? "GenerateMipmap" : "Clear",
|
||||
static_cast<unsigned>(serverCap), MG_Config::Ipc.VerbBarrier,
|
||||
static_cast<unsigned>(parked), static_cast<unsigned>(appliedBeforeReturn));
|
||||
std::fflush(stdout);
|
||||
session.Stop();
|
||||
::_exit(correct ? 0 : 101);
|
||||
});
|
||||
ASSERT_TRUE(WIFEXITED(child.Status)) << DescribeStatus(child) << '\n' << child.Log;
|
||||
const int code = WEXITSTATUS(child.Status);
|
||||
ASSERT_TRUE(code == 0 || code == 101) << "E1 probe/setup failed: " << DescribeStatus(child) << '\n' << child.Log;
|
||||
EXPECT_EQ(code, 0) << diagnostic;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
TEST(RemoteWaitBoundaryControl, AppliedClassWaitsWithRunAheadCap) {
|
||||
#if MGTEST_HAVE_FORK
|
||||
ASSERT_EQ(MGPipeWaitClassFor(MGPWireOp::GenerateMipmap), kWaitApplied);
|
||||
CheckE1Boundary(true, MGPWireOp::GenerateMipmap, true,
|
||||
"E1 wait boundary: GenerateMipmap returned before apply");
|
||||
#else
|
||||
GTEST_SKIP() << "E1 requires POSIX child isolation";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(RemoteWaitBoundaryControl, MissingServerCapKeepsClearLockstep) {
|
||||
#if MGTEST_HAVE_FORK
|
||||
ASSERT_EQ(MGPipeWaitClassFor(MGPWireOp::Clear), kWaitNone);
|
||||
CheckE1Boundary(false, MGPWireOp::Clear, true,
|
||||
"E1 wait boundary: Clear without server cap returned before apply");
|
||||
#else
|
||||
GTEST_SKIP() << "E1 requires POSIX child isolation";
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST(RemoteWaitBoundaryControl, ServerCapAllowsClearToRunAhead) {
|
||||
#if MGTEST_HAVE_FORK
|
||||
ASSERT_EQ(MGPipeWaitClassFor(MGPWireOp::Clear), kWaitNone);
|
||||
CheckE1Boundary(true, MGPWireOp::Clear, false,
|
||||
"E1 positive boundary: Clear with server cap waited for apply");
|
||||
#else
|
||||
GTEST_SKIP() << "E1 requires POSIX child isolation";
|
||||
#endif
|
||||
}
|
||||
@@ -2710,6 +2710,8 @@ TEST(RemoteRunAhead, OnlyTheTextureHalfOfResourceSubDataWantsItsReply) {
|
||||
}
|
||||
|
||||
|
||||
#include "RemoteClientE1Controls.inc"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path path =
|
||||
|
||||
@@ -87,6 +87,12 @@ expect FAILED "the knob leaves the selection green" -- run_split green
|
||||
expect FAILED "the baseline is already red" -- run_split red-baseline
|
||||
# P5 is complete: losing the runtime implementation must no longer disarm the gate.
|
||||
expect FAILED "every split entry skipped (implementation lost)" -- run_split all-skipped
|
||||
expect FAILED "E1 missing boundary probes" -- run_split e1-empty
|
||||
expect FAILED "E1 own baseline is already red" -- run_split e1-red-baseline
|
||||
expect FAILED "E1 restored boundary remains red" -- run_split e1-restore-red
|
||||
expect FAILED "E1 record never reaches the peer" -- run_split e1-no-peer
|
||||
expect FAILED "E1 observed no real emission" -- run_split e1-no-emit
|
||||
expect FAILED "E1 incorrectly waits every record" -- run_split e1-waitall
|
||||
|
||||
echo
|
||||
echo "=== the retrace lane's pull-library control (scripts/ci/retrace_pull_library_control.sh)"
|
||||
|
||||
@@ -13,12 +13,14 @@ set -u
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
WORK="$(mktemp -d)" || exit 1
|
||||
HELPER="${HERE}/../../MobileGL/MG_IntegrationTest/Harness/split_log_paths.py"
|
||||
trap 'cp "${WORK}/split.orig" "${HERE}/split_negative_controls.sh"; cp "${WORK}/retrace.orig" "${HERE}/retrace_pull_library_control.sh"; cp "${WORK}/dropdraw.orig" "${HERE}/retrace_drop_draw_control.sh"; cp "${WORK}/helper.orig" "${HELPER}"; rm -rf "${WORK}"' EXIT
|
||||
WAIT_HELPER="${HERE}/wait_boundary_negative_control.py"
|
||||
trap 'cp "${WORK}/split.orig" "${HERE}/split_negative_controls.sh"; cp "${WORK}/retrace.orig" "${HERE}/retrace_pull_library_control.sh"; cp "${WORK}/dropdraw.orig" "${HERE}/retrace_drop_draw_control.sh"; cp "${WORK}/helper.orig" "${HELPER}"; cp "${WORK}/wait.orig" "${WAIT_HELPER}"; rm -rf "${WORK}"' EXIT
|
||||
|
||||
cp "${HERE}/split_negative_controls.sh" "${WORK}/split.orig"
|
||||
cp "${HERE}/retrace_pull_library_control.sh" "${WORK}/retrace.orig"
|
||||
cp "${HERE}/retrace_drop_draw_control.sh" "${WORK}/dropdraw.orig"
|
||||
cp "${HELPER}" "${WORK}/helper.orig"
|
||||
cp "${WAIT_HELPER}" "${WORK}/wait.orig"
|
||||
|
||||
echo "=== baseline: the smoke test must be GREEN before anything is perturbed"
|
||||
if ! bash "${HERE}/control_smoke_test.sh" > "${WORK}/before.log" 2>&1; then
|
||||
@@ -31,9 +33,9 @@ tail -1 "${WORK}/before.log"
|
||||
echo
|
||||
echo "=== perturbation: remove the evidence check from both controls"
|
||||
python3 - "${HERE}/split_negative_controls.sh" "${HERE}/retrace_pull_library_control.sh" \
|
||||
"${HERE}/retrace_drop_draw_control.sh" <<'PY' || exit 1
|
||||
"${HERE}/retrace_drop_draw_control.sh" "${WAIT_HELPER}" <<'PY' || exit 1
|
||||
import sys
|
||||
split, retrace, dropdraw = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
split, retrace, dropdraw, wait = sys.argv[1:]
|
||||
# (file, [(needle, the prefix the line must start with)]). The draw-drop control has THREE
|
||||
# evidence checks rather than one, because "the picture went red" has three different ways of
|
||||
# being somebody else's red: the SSIM never fell, the library never said the knob armed, and the
|
||||
@@ -49,16 +51,17 @@ split, retrace, dropdraw = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
# nothing was ever silently proved - but the one control it was most about was never perturbed.
|
||||
# Reproduced on p5/joint@e61d0012 before this line changed.
|
||||
#
|
||||
# AND THE SPLIT CONTROL HAS THREE EVIDENCE CHECKS, NOT ONE, so all three are reverted. E1's is a
|
||||
# read of the entry's own private library log, E3(a)'s ctest-output regex is the `elif` above,
|
||||
# and E3(a)'s private-log half is ID-65's addition. Perturbing only one of the three leaves the
|
||||
# AND THE SPLIT CONTROL HAS THREE EVIDENCE CHECKS, NOT ONE. E1 checks each boundary
|
||||
# probe's own observation and assertion; E3(a) checks its assertion and private log.
|
||||
# All three are reverted: perturbing only one leaves the
|
||||
# other two catching the smoke test's "unrelated failure" case, and the case never flips - which
|
||||
# is what this script measured the first time the perturbation actually applied.
|
||||
rules = [
|
||||
(split, [('LINE', '"${log_helper}" assertion', ('if ! ', 'elif ! ')),
|
||||
('SUBST', '"[A-Za-z_][A-Za-z_0-9]*"\\}\' || exit 1', '"[A-Za-z_][A-Za-z_0-9]*"\\}\' || true'),
|
||||
('SUBST', '"${private_evidence}" "${name}" || exit 1',
|
||||
'"${private_evidence}" "${name}" || true')]),
|
||||
(wait, [('SUBST', 'if not own_negative(text, observation, diagnostic):',
|
||||
'if False: # E1 evidence check removed by red-check')]),
|
||||
(retrace, [('LINE', 'grep -qF "${EVIDENCE}"', ('if ! ',))]),
|
||||
(dropdraw, [('LINE', 'awk -v a="${ssim}"', ('if ! ',)),
|
||||
('LINE', '[ -z "${armed_line}" ]', ('if ',)),
|
||||
@@ -134,8 +137,9 @@ echo
|
||||
cp "${WORK}/split.orig" "${HERE}/split_negative_controls.sh"
|
||||
cp "${WORK}/retrace.orig" "${HERE}/retrace_pull_library_control.sh"
|
||||
cp "${WORK}/dropdraw.orig" "${HERE}/retrace_drop_draw_control.sh"
|
||||
cp "${WORK}/wait.orig" "${WAIT_HELPER}"
|
||||
echo "=== ID-62 perturbation: remove only the skip check"
|
||||
python3 - "${HELPER}" <<'PY' || exit 1
|
||||
python3 - "${HELPER}" "${WAIT_HELPER}" <<'PY' || exit 1
|
||||
from pathlib import Path
|
||||
import sys
|
||||
path = Path(sys.argv[1])
|
||||
@@ -144,6 +148,12 @@ needle = ' if skipped:\n'
|
||||
if text.count(needle) != 1:
|
||||
raise SystemExit('expected exactly one skip check')
|
||||
path.write_text(text.replace(needle, ' if False: # skip check removed by red-check\n'))
|
||||
path = Path(sys.argv[2])
|
||||
text = path.read_text()
|
||||
needle = ' if case.find("skipped") is not None or case.get("status") in {"notrun", "disabled"}:\n'
|
||||
if text.count(needle) != 1:
|
||||
raise SystemExit('expected exactly one E1 skip check')
|
||||
path.write_text(text.replace(needle, ' if False: # E1 skip check removed by red-check\n'))
|
||||
PY
|
||||
bash "${HERE}/control_smoke_test.sh" > "${WORK}/skips.log" 2>&1
|
||||
rc=$?
|
||||
|
||||
@@ -33,8 +33,10 @@
|
||||
# CONTROL", "is the E3(a) NEGATIVE CONTROL"), and it is tempting to grep for that. It is not
|
||||
# evidence: it is written at config load, by every process in the run, whatever happens next. A
|
||||
# setup abort would carry it too. It proves the knob was READ, never that the knob caused the red.
|
||||
# Only the failing case's own diagnostic does that. ID-53 gives each Split entry a private
|
||||
# library log: E1 reads its Fatal there; E3(a) reads the scenario assertion in ctest output.
|
||||
# Only the failing case's own diagnostic does that. E1 now observes deterministic transport
|
||||
# waits in dedicated CPU tests; E3(a) retains its scenario assertion and private library log.
|
||||
# P5e ID-122: ordinary run-ahead draws need not fail with VERB_BARRIER=0, and the old
|
||||
# overlap Fatal cannot fire with the default BATCH_WAITS=1. Pixel failures are not E1 evidence.
|
||||
#
|
||||
# Usage: split_negative_controls.sh [--self-test]
|
||||
# CTEST ctest binary (default: ctest)
|
||||
@@ -139,20 +141,14 @@ run_control() {
|
||||
cat "${out}"
|
||||
|
||||
# Inspect JUnit before exit status or private Fatal: a skipped pre-flight can carry both.
|
||||
label='E3(a)'
|
||||
[ "${evidence}" != private-barrier-fatal ] || label=E1
|
||||
python3 "${log_helper}" results "${manifest}" "${filter}" "${result}" "${label}" || exit 1
|
||||
python3 "${log_helper}" results "${manifest}" "${filter}" "${result}" 'E3(a)' || exit 1
|
||||
|
||||
if [ "${control_rc}" -eq 0 ]; then
|
||||
echo "::error::${name} left ${matched} split entries GREEN, so the knob it turns is not load-bearing and the gate it controls proves nothing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# E1's MGLOG_F sink is the private file, never ctest's status or transcript.
|
||||
if [ "${evidence}" = "private-barrier-fatal" ]; then
|
||||
python3 "${log_helper}" evidence "${manifest}" "${filter}" \
|
||||
'Fatal\{BarrierViolation, "[A-Za-z_][A-Za-z_0-9]*"\}' || exit 1
|
||||
elif ! python3 "${log_helper}" assertion "${manifest}" "${filter}" "${result}" "${evidence}"; then
|
||||
if ! python3 "${log_helper}" assertion "${manifest}" "${filter}" "${result}" "${evidence}"; then
|
||||
echo "::error::${name} FAILED: red lacks its persistent-map push diagnostic. Required: ${evidence}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -168,26 +164,15 @@ run_control() {
|
||||
echo "${name} turned ${matched} selected entries red, and the red carries the scenario's own diagnostic, as it must"
|
||||
}
|
||||
|
||||
# E1: c1 ClientSession::EmitAndWait emits MGLOG_F Fatal{BarrierViolation, "<slot>"}.
|
||||
# This asserts an observed overlap with the applier, which is timing-dependent.
|
||||
# Without that Fatal in a fresh selected file E1 fails; pixel/status fallbacks do not count.
|
||||
# Keep the ID-53-approved SmallRing selection as well as the default lane.
|
||||
# ...MINUS TheServerStampedAVerbBoundaryOnThisDrawingFrame, and that exclusion is load-bearing
|
||||
# rather than tidying (P5e, ID-122). That case is package gl's ID-115 POSITIVE CONTROL: it reads
|
||||
# a PipeStats window out of the lane's own private log, so it runs ONLY in the strict-arming
|
||||
# lane and skips everywhere else BY DESIGN, whatever this knob is set to. E1's selection began
|
||||
# matching it the moment gl added it, and ID-62 makes a SKIPPED SELECTED entry a hard failure -
|
||||
# correctly, because a control that silently loses its subjects proves nothing. E1 then reported
|
||||
# "the knob killed the pre-flight, not the entry" while the knob was in fact working: four other
|
||||
# selected entries went red with the abort in the same run. So the bug was in WHO E1 selects,
|
||||
# not in what it asserts, and the fix excludes a case that can never run here rather than
|
||||
# teaching E1 to tolerate skips - which would have thrown ID-62 away.
|
||||
run_control "negative control E1 (MOBILEGL_IPC_VERB_BARRIER=0)" \
|
||||
'DirectGLES\.Split\.(SmallRing\.)?(Triangle|ClearThenReadPixels)' \
|
||||
'private-barrier-fatal' \
|
||||
'' \
|
||||
'TheServerStampedAVerbBoundaryOnThisDrawingFrame' \
|
||||
MOBILEGL_IPC_VERB_BARRIER=0
|
||||
# E1: the default arm must actually park for a kWaitApplied record and for a
|
||||
# kWaitNone record whose server has no run-ahead cap. Removing the verb barrier
|
||||
# must fail those exact return-before-apply assertions. A third positive case
|
||||
# proves that a cap-authorized kWaitNone record does not wait. The peer releases
|
||||
# on an observed Park or on the client's return, not on a sleep or pixel race.
|
||||
# The helper demands green baseline, two named reds, and green restoration;
|
||||
# skipped/missing cases, timeouts and unrelated failures never count as evidence.
|
||||
python3 "$(dirname "$0")/wait_boundary_negative_control.py" \
|
||||
--ctest "${CTEST}" --out "${CONTROL_TMPDIR}/e1" || exit 1
|
||||
|
||||
# E3(a): PersistentMapTracker::PushBlocksFor stops at blockBytes == 0 - deliberately, because 0
|
||||
# is the negative control and not "unlimited". Two independent halves are now required:
|
||||
|
||||
+7
-11
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
# Exercise the production control, including the private-file read (ID-53 / R-16).
|
||||
# Exercise E1's per-test boundary evidence and E3(a)'s private-file read (R-16).
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
WORK=$(mktemp -d "${TMPDIR:-/tmp}/split-private-log.XXXXXX")
|
||||
@@ -14,25 +14,21 @@ for mode in missing-fatal stdout-fatal stale-fatal evidence e3-unrelated e3-no-p
|
||||
bash "${HERE}/split_negative_controls.sh" > "${WORK}/${mode}.out" 2>&1 || rc=$?
|
||||
if [ "${mode}" = evidence ]; then
|
||||
[ "${rc}" = 0 ] && grep -q "negative control E3(a).*scenario's own diagnostic" "${WORK}/${mode}.out" || {
|
||||
cat "${WORK}/${mode}.out"; echo "NOT OK private-file Fatal must PASS"; exit 1;
|
||||
cat "${WORK}/${mode}.out"; echo "NOT OK own boundary/private-file evidence must PASS"; exit 1;
|
||||
}
|
||||
grep -qFx "private-log evidence: DirectGLES.Split.ClearThenReadPixelsScenario.ClearWithNoDrawIsVisibleToDefaultFramebufferReadPixels: ${WORK}/${mode}/entry.log" "${WORK}/${mode}.out" || {
|
||||
grep -qFx "private-log evidence: DirectGLES.Split.PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw: ${WORK}/${mode}/pmap.log" "${WORK}/${mode}.out" || {
|
||||
cat "${WORK}/${mode}.out"; echo "NOT OK private-file evidence line must name the selected entry and path"; exit 1;
|
||||
}
|
||||
else
|
||||
message='E1 FAILED: selected private logs lack expected Fatal'
|
||||
message='red lacks its own observed wait-boundary failure'
|
||||
[ "${mode}" != e3-unrelated ] || message='FAILED: red lacks its persistent-map push diagnostic'
|
||||
case "${mode}" in
|
||||
e3-no-private) message='no selected private log carries /MGPipe: persistent-map push disabled' ;;
|
||||
skipped-selection) message='SplitLogPaths FAILED: E1 control: the knob killed the pre-flight, not the entry - 1 selected entries skipped' ;;
|
||||
skipped-selection|notrun-selection) message='selected testcase skipped or did not run' ;;
|
||||
e3-skipped-selection) message='SplitLogPaths FAILED: E3(a) control: the knob killed the pre-flight, not the entry - 1 selected entries skipped' ;;
|
||||
notrun-selection|missing-selection) message='SplitLogPaths FAILED: E1 control: 1 selected entries did not run' ;;
|
||||
missing-selection) message='selected testcase names are missing, duplicated or unexpected' ;;
|
||||
esac
|
||||
if [[ "${mode}" = *-selection ]]; then
|
||||
match=(-qFx)
|
||||
else
|
||||
match=(-qF)
|
||||
fi
|
||||
match=(-qF)
|
||||
[ "${rc}" != 0 ] && grep "${match[@]}" "${message}" "${WORK}/${mode}.out" || {
|
||||
cat "${WORK}/${mode}.out"; echo "NOT OK ${mode}: control must report FAILED for its own reason"; exit 1;
|
||||
}
|
||||
|
||||
Vendored
+70
@@ -52,10 +52,80 @@ for a in "$@"; do
|
||||
[ "$a" = "--show-only=json-v1" ] && json_requested=1
|
||||
[ "$a" = "-N" ] && listing_requested=1
|
||||
if [ "$prev" = "--output-junit" ]; then junit="$a"; fi
|
||||
if [ "$prev" = "-R" ]; then selector="$a"; fi
|
||||
prev="$a"
|
||||
done
|
||||
listing_requested="${listing_requested:-0}"
|
||||
|
||||
# E1's CPU-only boundary probes use their own testcase results. Keep observations
|
||||
# inside each testcase so stdout-only and stale evidence remain rejected.
|
||||
if [[ "${selector:-}" == *RemoteWaitBoundaryControl* ]]; then
|
||||
python3 - "${mode}" "${junit}" "${selector}" <<'PY'
|
||||
import os, re, sys
|
||||
import xml.etree.ElementTree as ET
|
||||
mode, path, selector = sys.argv[1:]
|
||||
barrier = int(os.environ['MOBILEGL_IPC_VERB_BARRIER'])
|
||||
negative = barrier == 0
|
||||
prefix = 'RemoteWaitBoundaryControl.'
|
||||
specs = [
|
||||
('AppliedClassWaitsWithRunAheadCap', 'GenerateMipmap', 1, True,
|
||||
'E1 wait boundary: GenerateMipmap returned before apply'),
|
||||
('MissingServerCapKeepsClearLockstep', 'Clear', 0, True,
|
||||
'E1 wait boundary: Clear without server cap returned before apply'),
|
||||
('ServerCapAllowsClearToRunAhead', 'Clear', 1, False, ''),
|
||||
]
|
||||
root = ET.Element('testsuite')
|
||||
failures = 0
|
||||
for index, (short, op, cap, waits, diagnostic) in enumerate(specs):
|
||||
name = prefix + short
|
||||
if not re.search(selector, name):
|
||||
continue
|
||||
if mode == 'e1-empty' or (negative and mode == 'missing-selection' and index == 1):
|
||||
continue
|
||||
case = ET.SubElement(root, 'testcase', name=name, status='run')
|
||||
waited = int(waits and barrier == 1)
|
||||
observation = (f'E1 observation: op={op} cap={cap} barrier={barrier} parked={waited} '
|
||||
f'applied_before_return={waited} emitted=1 peer_seen=1')
|
||||
failed = negative and mode != 'green'
|
||||
if mode == 'e1-red-baseline' and 'baseline' in path:
|
||||
failed = True
|
||||
if mode == 'e1-restore-red' and 'restored' in path:
|
||||
failed = True
|
||||
if mode == 'e1-waitall' and not waits:
|
||||
failed = True
|
||||
observation = observation.replace('parked=0', 'parked=1').replace('applied_before_return=0', 'applied_before_return=1')
|
||||
if negative and mode == 'skipped-selection':
|
||||
ET.SubElement(case, 'skipped')
|
||||
failed = False
|
||||
if negative and mode == 'notrun-selection':
|
||||
case.set('status', 'notrun')
|
||||
failed = False
|
||||
if failed:
|
||||
ET.SubElement(case, 'failure', message='control red')
|
||||
failures += 1
|
||||
text = observation + '\n' + (diagnostic if negative else '')
|
||||
if negative:
|
||||
if mode == 'unrelated' or mode == 'wrong-fatal':
|
||||
text = observation + '\nUNRELATED_CONTROL_FAILURE'
|
||||
elif mode == 'missing-fatal' or (mode == 'partial-fatal' and index == 1):
|
||||
text = diagnostic
|
||||
elif mode == 'stdout-fatal':
|
||||
print(text) # Must not qualify as this testcase's own evidence.
|
||||
text = 'UNRELATED_CONTROL_FAILURE'
|
||||
elif mode == 'stale-fatal':
|
||||
text = text.replace('barrier=0', 'barrier=1')
|
||||
elif mode == 'e1-no-peer':
|
||||
text = text.replace('peer_seen=1', 'peer_seen=0')
|
||||
elif mode == 'e1-no-emit':
|
||||
text = text.replace('emitted=1', 'emitted=0')
|
||||
ET.SubElement(case, 'system-out').text = text
|
||||
ET.ElementTree(root).write(path, encoding='utf-8', xml_declaration=True)
|
||||
print(f'E1 stub: {len(root)} selected, {failures} failed')
|
||||
sys.exit(8 if failures else 0)
|
||||
PY
|
||||
exit $?
|
||||
fi
|
||||
|
||||
emit_listing() {
|
||||
echo "Test project /stub"
|
||||
if [ "${mode}" = "retrace-noselect" ] || [ "${mode}" = "dropdraw-noselect" ]; then
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""E1: green/red/restored-green proof of real ClientSession wait boundaries.
|
||||
|
||||
The peer holds apply until the actual transport parks or the emit call returns.
|
||||
VERB_BARRIER=0 must fail only the two owed waits; ordinary run-ahead draws need
|
||||
not fail. Each arm is checked from its own CTest JUnit testcase, never a global
|
||||
grep, stale library log, arbitrary nonzero exit, or timing-dependent pixel.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
PREFIX = "RemoteWaitBoundaryControl."
|
||||
CASES = {
|
||||
"AppliedClassWaitsWithRunAheadCap": ("GenerateMipmap", 1, True,
|
||||
"E1 wait boundary: GenerateMipmap returned before apply"),
|
||||
"MissingServerCapKeepsClearLockstep": ("Clear", 0, True,
|
||||
"E1 wait boundary: Clear without server cap returned before apply"),
|
||||
"ServerCapAllowsClearToRunAhead": ("Clear", 1, False, ""),
|
||||
}
|
||||
|
||||
|
||||
def own_negative(text, expected_observation, diagnostic):
|
||||
return expected_observation in text and diagnostic in text
|
||||
|
||||
|
||||
def inspect(path, selected, barrier, negative, rc):
|
||||
root = ET.parse(path).getroot()
|
||||
cases = root.findall(".//testcase")
|
||||
names = [case.get("name") for case in cases]
|
||||
if len(names) != len(set(names)) or set(names) != set(selected):
|
||||
raise ValueError("selected testcase names are missing, duplicated or unexpected")
|
||||
for case in cases:
|
||||
name = case.get("name")
|
||||
if case.find("skipped") is not None or case.get("status") in {"notrun", "disabled"}:
|
||||
raise ValueError(f"{name}: selected testcase skipped or did not run")
|
||||
failed = case.find("failure") is not None or case.find("error") is not None
|
||||
op, cap, waits, diagnostic = CASES[name.removeprefix(PREFIX)]
|
||||
waited = int(waits and barrier == 1)
|
||||
observation = (f"E1 observation: op={op} cap={cap} barrier={barrier} parked={waited} "
|
||||
f"applied_before_return={waited} emitted=1 peer_seen=1")
|
||||
text = "\n".join(case.itertext())
|
||||
if negative:
|
||||
if not failed or rc == 0:
|
||||
raise ValueError(f"{name}: disabled barrier did not fail the owed wait")
|
||||
if not own_negative(text, observation, diagnostic):
|
||||
raise ValueError(f"{name}: red lacks its own observed wait-boundary failure")
|
||||
elif failed or rc != 0 or observation not in text:
|
||||
raise ValueError(f"{name}: baseline/restored arm did not execute its green boundary proof")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--ctest", default=os.environ.get("CTEST", "ctest"))
|
||||
parser.add_argument("--out", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
all_cases = [PREFIX + name for name in CASES]
|
||||
negative_cases = [PREFIX + name for name, (_, _, waits, _) in CASES.items() if waits]
|
||||
ok = True
|
||||
for arm, barrier, negative, selected in (
|
||||
("baseline", 1, False, all_cases),
|
||||
("control", 0, True, negative_cases),
|
||||
("restored", 1, False, all_cases),
|
||||
):
|
||||
# Never run a negative arm against a failed baseline. A failed negative
|
||||
# still proceeds through restoration and must not consume stale XML.
|
||||
if arm == "control" and not ok:
|
||||
continue
|
||||
result = args.out / f"e1-{arm}.xml"
|
||||
result.unlink(missing_ok=True)
|
||||
env = os.environ.copy()
|
||||
env["MOBILEGL_IPC_VERB_BARRIER"] = str(barrier)
|
||||
selector = "^(" + "|".join(re.escape(name) for name in selected) + ")$"
|
||||
command = [args.ctest, "-L", "unit", "-R", selector, "--no-tests=error",
|
||||
"--output-on-failure", "--output-junit", str(result)]
|
||||
try:
|
||||
run = subprocess.run(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, timeout=90)
|
||||
(args.out / f"e1-{arm}.out").write_text(run.stdout, encoding="utf-8")
|
||||
print(run.stdout, end="", flush=True)
|
||||
inspect(result, selected, barrier, negative, run.returncode)
|
||||
print(f"E1 {arm}: {'EXPECTED_RED' if negative else 'GREEN'} ({len(selected)} cases)", flush=True)
|
||||
except (OSError, ValueError, ET.ParseError, subprocess.TimeoutExpired) as exc:
|
||||
print(f"E1 FAILED: {arm}: {exc}", file=sys.stderr, flush=True)
|
||||
ok = False
|
||||
if ok:
|
||||
print("negative control E1: owed waits failed by their own observed boundary assertions; baseline and restoration green")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user