fix(e2e): stop the harness reporting checks it never made - #595
Draft
latekvo wants to merge 8 commits into
Draft
Conversation
The validation tier reported 88 green cases while asserting nothing beyond "exit code was non-zero", and any unbound variable in a phase killed the run before it could write a report or remove its ~500MB sandbox. assert_reject looked for zod issues on stdout, but the CLI prints them on stderr and leaves stdout empty on a rejection, so the structured branch never matched once. Both of its arms called pass(), so the miss was invisible and the zod-path argument every case passes in was decorative: a tool that simply exited 1 for every input would have scored the same 88/88. It now reads either stream, requires the issue to name the asserted field when the output carries issues at all, and refuses to count timeout(1)'s 124-127 as a rejection - those mean the schema never judged the input. That check, once it could fail, exposed two more: the describe parser matched --[a-zA-Z0-9-]+ and so truncated every snake_case flag at the underscore (--device_id -> device), making the generated arguments carry keys no schema knows; and missing-required asserted invalid_type for enum flags, which zod reports as invalid_value. 87 of 88 cases now verify the field they name. Also: - run-e2e.sh had no EXIT trap, so an abort lost the report and leaked the sandbox. Report generation, teardown and the exit code move into one, with a completion flag - without it an aborted run exits 0 on "0 failures". - run_one sourced each phase over the last one's run_phase, so a phase with a syntax error silently re-ran the previous phase under the new phase's name. - An unknown --phase selected nothing and exited 0, reporting success for a release it never tested. - 90-cleanup gated stop-all-simulator-servers and stop-metro on ARGENT_TOOLS_URL, which the harness never sets, so neither ran and every run left a simulator-server and Metro alive; teardown-complete passed regardless. It now gates on the server answering and verifies it stopped. - 30-android read $b before the guard that protects it, aborting the tier whenever the first screenshot failed. The copy it guarded was redundant. - capture_screenshot left the previous call's SHOT_SIZE/SHOT_PATH in place, so a capture producing no file reported the last successful one's size. - usage() printed a pinned line range that had drifted into the code. Verified: introspection+validation 179 pass / 0 fail / 5 skip against swmansion-argent-0.17.0.tgz (was 177/0/5, of which 88 asserted nothing). assert_reject checked against right-field, wrong-field, wrong-code, no-path and unreachable-binary inputs; every one of those cases passes on the old code. Abort path checked by injecting an unbound variable into a phase: report written, sandbox removed, exit 1 - and exit 0 with the completion flag mutated away.
Findings from a review pass over the android and chromium phases, each reproduced before being fixed. --android-avd drove the whole Android tier and then handed the RN tier nothing: the booted serial stayed in a `local`, and 50-rn-bluesky resolves its device from E2E_ANDROID_SERIAL, so all 24 debugger/profiler/network tools blanket-skipped with "no Android device" on a run that had one. It is exported now, with a separate flag deciding teardown so the meaning of "we booted this" does not ride on that variable. The Android teardown claimed to leave an injected device alone and then issued the same stop-simulator-server call as the self-booted branch, discarded the result, and recorded a skip saying it had not. On a shared machine that tore down the allocator's device service immediately before the RN tier drove that same serial. The chromium display gate accepted an installed xvfb-run as a substitute for DISPLAY, but nothing wraps the Electron spawn in it, so a headless box with it installed cleared the gate and failed boot-device on a missing X server - a red release gate for the environment the gate exists to skip. It requires DISPLAY now; running the harness under xvfb-run supplies one. README updated to match. The port-owner fallback in the chromium teardown matched ":$port" as a substring and then took the first pid anywhere in `ss -ltnp` output, so with a listener on :12345 present it would kill that process while cleaning up :1234. It now compares the port as the whole last field of the local address and reads the pid from that same line. Checked against synthetic colliding-port output and against two live listeners. Also: - The chromium teardown recorded electron-stopped unconditionally, including when it killed nothing. It waits for the process to go, escalates to SIGKILL, and fails if the pid survives. - chromium-tabs select/close got no record at all when `new` failed for a reason outside the tolerated string, so the report still counted the tool covered off `list`. Both arms record the skips now. - The RN tier's stop-metro sat after every early return, so a failed debugger-connect left Metro running; and it ran unconditionally, so a developer's own Metro on :8081 was killed by a harness that never started it. It now stops only a Metro this phase started, on both paths. - _shot_ok recorded its byte count as the case name rather than the detail, unlike the fail beside it, giving every run a different case string.
…n survive an abort The validation tier's type parser resolved every flag to `unknown`, because awk has no `\b` word boundary - `/\bnumber\b/` is a backspace-number-backspace literal that matches nothing. So `model_number_flags` was always empty and the bad-type matrix never produced a single case, while `_build_args` handed the string "x" to every required numeric, boolean and object field. Reading the type from its own column (and adding the integer arm the CLI emits) takes the parsed kinds from 270 unknown / 21 enum / 1 array to 131 string, 68 number, 28 boolean, 25 integer, 21 enum, 16 array, 3 object, 2 unknown - the last two being `any`, which is correct. bad-type goes from 0 cases to 43. `-json` is how the CLI renders an object field (`--selector-json <json>`); the schema key is the name without it, so three required fields were being sent under a key no schema knows. flow-add-step declares its own `args` property, which makes `--args` that field's value rather than the whole-payload escape hatch, so run_tool cannot address its fields at all. It is skipped with that reason instead of failing on a payload the tool never received. Teardown now runs from the EXIT trap rather than the happy path, and runs before the sandbox is removed: $E2E_HOME holds the detached tool-server's pid file and that server has no idle timeout, so deleting the sandbox first stranded a process `argent server stop` could no longer find - along with any Electron, Metro and fixture server the tiers spawned. The trap is installed after lib/common.sh is sourced, since the handler calls into it. Also: - `--android-avd` asked boot-device for 840s inside run_tool's 120s timeout, so the documented flag could never succeed; and nothing shut the emulator down afterwards (the tool-server only reaps devices Lens booted, not boot-device). - The introspection describe loop recorded nothing at all when describe failed - no pass, no fail - and the validation tier then reported the tool as having no flags. The flags round-trip asserted the flag name appears in `argent flags`, which is registry-driven and prints every flag whether or not anything was stored; it asserts the state token now. - The post-uninstall restore hardcoded --omit=optional and the sandbox prefix, dropping electron on a full run (the chromium tier then skipped itself as "electron not installed") and, under --system, restoring the machine's real global argent into the sandbox instead of putting it back. - A value-taking flag with no value died on an unbound $2 instead of printing usage; two runs starting in the same second shared one results file. - Dropped the unused artifact_path helper and corrected the comment claiming the server is targeted via ARGENT_TOOLS_URL, which the harness never sets. Verified against swmansion-argent-0.17.0.tgz: introspection+validation 221 pass / 0 fail / 6 skip, with 130 of 131 rejection cases now naming the field they were built to exercise (previously 0 of 88 did - every one passed on "exit code was non-zero" alone). Abort path re-checked by injecting an unbound variable into a phase: cleanup phase runs, report written, sandbox removed, exit 1, PARTIAL reported - and exit 0 with the completion flag mutated away.
Nine pass/fail/skip calls passed their message as the third argument, which _record() takes as the case name — so the detail column was empty and the case string changed every run. The chromium boot failure was the visible cost: a failed boot recorded case=" " with no detail at all, because RT_OUT was empty and the only field carrying diagnostics was the one being used as an id. Found by running the android and chromium tiers against a live emulator.
A tool call killed by timeout(1) writes nothing, so RT_OUT is empty exactly when the operator most needs to know what happened: a hung boot-device recorded a failure whose detail was the empty string, indistinguishable from a tool that answered with nothing. Nine sites formatted that detail by hand; they now share rt_detail(), which names the timeout (124), a command timeout could not run (125-127), and a silent non-zero exit. Found by running the chromium tier against a live Electron: the CDP port was bound but never answered, and the harness reported the failure with no text at all.
A display-manager session switch leaves $DISPLAY pointing at a server the process holds no cookie for. The gate only tested that the variable was non-empty, so the tier ran anyway: Electron bound its CDP port and never answered on it, and the run recorded an opaque readiness timeout instead of 'no display' — a red release gate for an environment the gate exists to skip. Probe the display with xdpyinfo or xset when either is installed. When neither is, the skip reason says the display was assumed usable rather than implying it was checked.
Four calls in the RN tier fail on every healthy build, each confirmed against the tool's own source: - profiler-stack-query and profiler-combined-report read the native trace and throw "No Android trace loaded. Run native-profiler-stop → native-profiler- analyze first." They sat in the react block, thirteen lines before any native profiling starts. Moved into the native block, after analyze. - profiler-load load_native throws without session_id. It now takes the newest id from `list` mode output, and skips rather than fails when there is none. - profiler-commit-query by_time_range throws without time_range_ms. Moving the first two also closed a hole they had opened: both were listed in RN_TOOLS and in the failure-arm skip list but had no case on the success path, so a successful react-profiler-start left them with no pass, fail or skip at all. Both arms of the native block now record them. Verified against the tools' source contracts (the throw sites and their required params), not against a live device: this box could not hold an emulator up long enough to reach the RN tier - see the PR body.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found while running the full E2E harness against the packed 0.17.0 bundle for the release check. All of these are harness bugs, not product bugs.
The core problem
The harness reported results it had not actually measured.
assert_rejectlooked for zod issues on stdout, but the CLI prints them on stderr and leaves stdout empty on a rejection. Both arms of the check calledpass(), so the miss was invisible: the zod-path argument every case passes in was decorative, and a CLI that simply exited 1 for every input would have scored the same 88/88. All 88 validation cases asserted nothing beyond "exit code was non-zero."Once that check could fail, it exposed the rest:
\bword boundary./\bnumber\b/matches nothing, so every flag kind resolved tounknown,model_number_flagswas always empty, and the bad-type matrix never emitted a single case - while_build_argshanded the string"x"to every required numeric/boolean/object field.--[a-zA-Z0-9-]+, truncating every snake_case flag at the underscore (--device_id→device), and kept the-jsondisplay suffix (--selector-json) that is not part of the schema key. Both produced payloads carrying keys no schema knows.missing-requiredassertedinvalid_typefor enum flags, which zod reports asinvalid_value.Parsed kinds went from 270 unknown / 21 enum / 1 array to 131 string, 68 number, 28 boolean, 25 integer, 21 enum, 16 array, 3 object, 2 unknown (the last two are
any, which is correct).Teardown and the exit code
run-e2e.shhad notrap. An unbound variable inside a phase is fatal to the whole shell -run_phase || warndoes not catch it - so an abort skipped the cleanup phase, the report, andrm -rf $E2E_WORK, leaking a ~500MB sandbox plus a tool-server, Electron, Metro and a fixture http server. Report generation, teardown and the exit code now live in an EXIT trap, with a completion flag: without it an aborted run exits 0 on "0 failures", which is verified below by mutation.Teardown runs before the sandbox is removed -
$E2E_HOMEholds the detached tool-server's pid file and that server is started with no idle timeout, so deleting the sandbox first strands a processargent server stopcan no longer find.90-cleanup.shgatedstop-all-simulator-serversandstop-metroonARGENT_TOOLS_URL, which the harness never sets (ensure_serverdeliberately does not pin it), so neither ever ran and every run left a simulator-server and Metro alive - whileteardown-completepassed regardless.Other findings fixed
--android-avdcould never work:boot-devicewas asked for 840s insiderun_tool's 120s timeout, and the booted serial stayed in alocal, so the RN tier blanket-skipped all 24 debugger/profiler tools for want of a device that was right there. Nothing shut the emulator down either - the tool-server only reaps devices Lens booted, not onesboot-devicestarted.argent rundoes no client-side validation, so every call failed with a transport error that no one distinguished from a rejection. It refuses to run instead, and no longer countstimeout(1)'s 124-127 as a rejection.--omit=optionaland the sandbox prefix - dropping electron on a full run (the chromium tier then skipped itself as "electron not installed"), and under--systemrestoring the machine's real globalargentinto the sandbox instead of putting it back.stop-simulator-servercall as the self-booted branch, discarded the result, and recorded a skip saying it had not - tearing down the allocator's device service immediately before the RN tier drove that same serial.:$portas a substring and took the first pid anywhere inss -ltnpoutput, so cleaning up:1234would kill a process listening on:12345.stop-metrosat after every early return (a faileddebugger-connectleft Metro running) and ran unconditionally, killing a developer's own Metro on :8081 that the harness never started.xvfb-runas a substitute forDISPLAY, but nothing wraps the Electron spawn in it - so a headless box with it installed cleared the gate and failedboot-device, turning the gate red for exactly the environment it exists to skip.--phaseselected nothing and exited 0;-hprinted 7 lines of source code; a value-taking flag with no value died on an unbound$2; two runs in the same second shared one results file.flow-add-stepdeclares its ownargsproperty, which makes--argsthat field's value rather than the whole-payload escape hatch, sorun_toolcannot address its fields. Skipped with that reason rather than failing on a payload the tool never received.Verification
Against
swmansion-argent-0.17.0.tgz,introspection,validation:assert_rejectchecked against right-field, wrong-field, wrong-code, no-path and unreachable-binary inputs. Every one of those cases passes on the un-fixed code.PARTIALreported. With the completion flag mutated to always-set, the same abort exits 0, which is what pins it.\b, the-jsonsuffix, the underscore truncation, thessport collision and thetimeout124-127 contract were each reproduced standalone before being fixed.Found by running against a live Android emulator
The android tier executed end-to-end for the first time: 23 pass / 1 fail / 10 skip, including the
screenshot-diffpair that the original unbound-variable bug aborted on. Three further defects only surfaced by running:pass/fail/skipcalls passed their message as the third argument, which_record()takes as the case name — so the detail column was empty and the case string changed every run. The chromium boot failure was the visible cost: it recordedcase=" "with no diagnostic whatsoever.timeout(1)writes nothing, soRT_OUTis empty exactly when the operator most needs to know what happened. A hungboot-devicewas recorded as a failure with a blank detail. All nine ad-hoc detail formatters now sharert_detail(), which names the timeout (124) and a commandtimeoutcould not run (125-127).$DISPLAYwas non-empty, not that it was usable. A display-manager session switch on this box left$DISPLAYpointing at a server the process holds no cookie for; Electron then bound its CDP port and never answered on it (verified: the TCP connection is accepted, the HTTP response never arrives), so the tier hard-failed instead of skipping.Moving
profiler-stack-queryandprofiler-combined-reportinto the native block also closed a hole their removal had opened: both were listed inRN_TOOLSand in the failure-arm skips but had no case on the success path, so a successfulreact-profiler-startleft them with no pass, fail or skip at all.Not covered here
detected a hanging thread 'QEMU2 main loop') whenever this machine's load average climbs, which it does to 90-120 on 16 cores from concurrent work. Four attempts: it survived the android tier at load ~10, and was starved each time by a competingnpm installor by Metro's own bundler startup. The four RN profiler fixes are verified against the tools' source contracts (the throw sites and their required params), not against a live run.boot-device, for the display reason above.assert_okcannot fail for calls whose result is a constanttrue; the cookie/storage set→get pair never verifies the round-trip value; a tier-level skip writes one row soreport.py's coverage matrix silently drops the tools that tier would have recorded;report.pyrenders an empty or partial log as a clean green report.