Commit Graph
5 Commits
Author SHA1 Message Date
Vito 3ebd5e4166 ci(browser): a hung browser step fails with its running tests named and its traces kept (#425) (#429)
* ci(browser): stop a hung browser run before the job limit and name it

A browser job killed at its 20-minute limit keeps no log and skips its
upload steps (merge-queue run 40723857746, firefox shard 2/2), so nothing
showed which test hung.

- Each browser test step gets timeout-minutes 18, below the job's 20, so
  the step fails and the always() uploads still run.
- Under CI, Playwright's globalTimeout stops the run first:
  PM_TEST_GLOBAL_TIMEOUT_MINUTES 17 for page workflows, 15 for coverage
  workflows, a fixed 3 for the viewer suite. Locally there is no limit
  unless the variable is set. Each test keeps the 30 s default.
- running-tests-reporter.mjs prints the tests still running when the run
  ends; Playwright's own reporters cannot, since those tests never end.
- A blob report goes to tools/vendor/test-results/blob/, inside the
  uploaded traces path.
- ci-workflow.test.mjs pins globalTimeout < step limit < job limit and the
  always() uploads; no existing limit is raised.

Closes #425

* ci(browser): rely on the step limit, report the running tests on SIGTERM

Maintainer ruling on #425: no Playwright globalTimeout. The 18-minute
step limit under the unchanged 20-minute job limit stays, as do the
always() uploads and the CI blob report.

- Drop globalTimeout and PM_TEST_GLOBAL_TIMEOUT_MINUTES with its helper
  and tests.
- A step that reaches its limit gets SIGTERM, which ended Playwright with
  no report and no blob. The running-tests reporter now catches the first
  SIGTERM, prints the tests still running and sends Playwright its own
  SIGINT, so the run ends interrupted and the blob report is written. A
  second SIGTERM kills as usual.
- Test paths are relative to the suite's rootDir instead of the cwd, so
  `npm run --prefix` no longer prints ../../ prefixes.
- The workflow comment and TESTING.md say the guarantee assumes an npm
  cache hit and that coverage shard 1 is tight in its 18-minute step.

* ci(browser): forward SIGTERM and SIGINT from frontend-coverage.mjs to its child

In the coverage job npm signals only frontend-coverage.mjs. It blocked in
spawnSync with no listener, so Playwright never saw a step timeout. It now
runs each step's child with async spawn, in its own process group, and
forwards SIGTERM and SIGINT to the running child. Step order, exit codes
and the "Command failed (N)" message are unchanged.

Locally, in the pinned image, `timeout -s TERM` on test:coverage printed
the still-running tests and wrote both blob reports. TESTING.md records
that a SIGTERM to npm alone stops at npm's `sh -c`, and that delivery
through `docker exec` in CI is not yet observed.

Refs #425

* ci(browser): load the running-tests reporter by absolute path

A config imported from another directory (the workflow-artifacts test copies
it to a temp root) resolved './running-tests-reporter.mjs' against that root
under CI and failed to load it. Resolve it from this module instead.
2026-10-28 18:38:33 +00:00
Vito 3f085d0724 ci: full matrix once per PR in the merge queue; Windows/macOS non-blocking (#420) (#421)
* ci: run the full matrix once per PR, in the merge queue (#420)

Pull requests now run a fast subset: Python 3.12 on ubuntu-latest, the
coverage gate, black/flake8, frontend modules, bandit and the Chromium/touch
frontend coverage shards plus merge. The merge queue, pushes to main and
manual runs keep the full matrix: Linux/Windows/macOS x Python 3.12-3.14,
real ComfyUI compatibility and the Firefox/WebKit page workflows.

A dropped matrix leg never reports a check, so the new required "all tests"
gate requires every test.yml job to succeed outside pull requests and real
ComfyUI to be skipped only on pull requests; viewerjs does the same for the
browser shards. branch-protection.json drops the eight legs a pull request
no longer runs and adds "all tests".

* docs(ci): ruleset edit keeps every rule and integration id; find merge-queue runs

* ci: move Windows and macOS out of the blocking path (#420)

The maintainer deferred Windows and macOS to the end of the redesign.
The merge queue now runs Linux on Python 3.12-3.14, real ComfyUI, the
frontend coverage shards and the Firefox/WebKit page workflows; pull
requests keep the Linux 3.12 subset. The six Windows/macOS legs move to
the new portability.yml, which runs on push to main, nightly and on
demand, keeps the 27-minute limit and is never a required check. Red
runs there are fixed at their cause before the cutover (#334).

* docs(ci): required checks live in the ruleset and the classic protection (#420)
2026-10-28 12:44:06 +00:00
Vito d0339128e4 test: isolate API fixture databases from other suites; CI lint pins, failing bandit hook, npm cache (#346, #352, #409) (#411)
* ci: pin black and flake8, let the bandit hook fail, cache npm in the JS job (#352)

The lint job installs black==26.10.1 and flake8==7.4.1 from
requirements-dev.txt (the versions CI resolved on main today), so a new
release cannot turn CI red with no code change.

The local bandit hook ran 'bash -c ... "$@"; true': it could never
fail, and bash bound the first staged file to $0 so it was never
scanned. It now runs 'bandit -lll -q' directly, failing on high
severity exactly like code-quality.yml.

The frontend modules job's setup-node caches npm keyed on
tools/vendor/package-lock.json.

* test(history): wait on the writer's state, not short wall-clock pauses (#352)

Four 0.08 s pauses before asserting that the worker had not probed yet
now wait until the idle worker has run a whole loop pass at the current
manual clock value (three clock reads), so a slow runner cannot end the
pause before the worker decides. A mutation that probes 20 ms early now
fails two tests deterministically.

The real-clock bounded close no longer asserts close(0.02) returned in
under 0.2 s wall time; it asserts the in-flight write is still blocked
and the worker alive, like the output writer test. The manual-clock
bound stays exact. The last 0.1 s non-blocking submit check uses the
1 s NON_BLOCKING_CEILING from #351.

* test(api): give each API fixture database its own temp folder (#346)

Every PromptManagerAPI sweeps its database's folder at startup for the
stages, uploads and safety-backup copies an interrupted restore left.
The API test fixtures created their database directly in the system
temp folder, so any other test process starting an API there (another
worktree's suite, a pre-push hook, an agent's targeted run) deleted
this process's in-flight restore files. The restore, epoch and
retirement tests then failed with 'staged restore changed', 'unable to
open database file' or 503 instead of 409. Busy machines fail because
they are running other suites, not because they are slow: the same
CPU load with a private TMPDIR passes.

private_temp_db() puts the fixture database in a new folder removed at
cleanup (with any safety backups a restore left beside it). A
regression test sweeps a stand-in shared folder in the middle of a
multipart restore, as another suite's startup would.

* test(history): count worker parks, hold saturation blocks until released (#352)

wait_for_worker_pass now counts the idle worker's parks on its wake
event (two parks from now = one whole pass begun after the call)
instead of assuming a fixed number of clock reads per pass, which a
future clock read would have made vacuous. The real-clock bounded
close keeps a loose upper bound (0.02 s + NON_BLOCKING_CEILING).

The two saturation tests (history and output writer) blocked the
worker with release.wait(3) while they queued 1023 more items. On a
loaded runner the fill took longer than 3 s, the worker unblocked on
its own and drained part of the queue (pending 868..1017 != 1024),
and cleanup then reported a leaked worker. The block now holds until
the test releases it; 60 s is only a hang guard, and cleanup always
sets the release. Under one core with 8 busy loops: 8/8 runs failed
before, 6/6 pass after.

* test(maintenance): wait on duplicate jobs with a disk-bound hang guard (#409)

On Windows py3.13 CI (PR #405) a one-file duplicate cleanup missed its
10 s WAIT. The test already waits on the job's own task; the 10 s bound
acted as a deadline on disk-bound work. In the same 50 s window the
following tests' setups took up to 9 s against 0.1-0.6 s around it
while the test bodies stayed fast, which is consistent with a
process-wide disk I/O stall on the runner rather than a lease, handle,
executor or job stall (the log has no thread dump to show more).

tests/disk_waits.py holds the one disk-bound hang guard (90 s per wait,
under the 120 s per-test watchdog), shared with RESTORE_SECONDS (#382).
Waits on find and cleanup job tasks in the duplicate-files tests use
it; gates and server-state polls keep WAIT. The keeper-validation test
turns the safety backup off, since it does not test backups.
2026-10-28 05:18:00 +00:00
Vito 5772de8d27 ci: bound every job and make hangs name themselves (#350) (#351)
* test: run the suite through a watchdog that names a hang (#350)

tests/run_suite.py discovers the same tests as unittest discover -s tests,
with the same sys.path and exit codes (5 when none ran), writes
'start: <test id>' to stderr before each test and arms
faulthandler.dump_traceback_later: 120 s per test (PM_TEST_HANG_LIMIT) and
120 s after the run (PM_TEST_SHUTDOWN_LIMIT), never cancelled. A hang in a
test, its teardown, its event-loop shutdown or interpreter exit prints every
thread's stack and exits non-zero instead of running to the job limit.
The pre-push hook now runs it too.

* ci: bound every job and run the Python suite through the watchdog (#350)

Every job in test.yml and code-quality.yml gets timeout-minutes =
ceil(1.5 x slowest of three green main runs) + 3, min 5, with the runs
and times written above jobs:. The matrix and the coverage gate run
python -m tests.run_suite. ci-workflow.test.mjs checks every job in the
three CI workflows has a limit and that the matrix and coverage use the
runner. Job names are unchanged.

* docs: document the test runner, time limits and following a push (#350)

* fix(test): keep unittest's warning filters and tighten the runner's edges (#350)

Review follow-ups: the runner now shows warnings as unittest.main does
(DeprecationWarning and ResourceWarning were hidden), rejects non-ASCII
digit limits with the one-line exit-2 error, and starts each 'start:' line
on its own line at default verbosity. The docs now say that class and module
fixtures count toward the test before them, that the shutdown limit covers
interpreter exit, and that exit 5 on zero tests is 3.12+ behaviour applied
on every version.

* test: decode the runner's stderr as UTF-8 on Windows too (#350)

A child Python writes a piped stderr in the ANSI codepage on Windows, so the
non-ASCII limit value in the usage-error test came back as U+FFFD. The
helper now sets PYTHONIOENCODING=utf-8 for the child.

* test(history): give the writer's non-blocking checks a 1 s ceiling (#350)

py3.13 on macos-latest failed on PR #351 with writer.close(0) taking
0.186 s against a 0.1 s ceiling while another thread held the commit. The
blocked path these checks rule out waits up to 3 s (release.wait(3)), so a
1 s ceiling still tells returning at once from waiting for the release,
without failing on a loaded shared runner (TESTING.md, #242).

* test(restore): wait for the cancelled preview's worker before teardown (#350)

py3.13 on ubuntu-latest failed on PR #351 (run 40334794505) with
'Files still open at teardown: prompts.db, prompts.db-shm' in
test_cancel_while_reading_the_live_library. Cancelling the preview cannot
stop its executor thread, which goes on to open the live library; the test
returned without waiting, so that open could land between teardown's
close_all_instances() and assert_closed(). The test now waits for the
worker, as test_cancel_while_staging already does.
2026-10-26 07:35:21 -07:00
Vito f95f66d76a ci(browser): run page workflows with parallel workers and CI shards (#274)
* test(browser): one test server per Playwright worker

The page workflows started one global webServer and ran with one worker.
Each worker now owns its own tests/browser/server.py on an OS-chosen port,
with its own temp root and database, through a shared `test` in
tests/browser/fixtures.mjs. baseURL, and so page, request and
browser.newContext, point at that worker's server. A server that dies
fails the running test with its stderr tail and the next test gets a
fresh one. The worker count comes from PM_TEST_WORKERS, else 1 under CI,
else a quarter of the CPUs up to 8.

Chromium workflows in the pinned image: 510 tests in 606 s with one
worker on main, 511 (one new isolation spec) in 119 s with eight.

* test(browser): test servers bind an OS-chosen port themselves

tests/browser/server.py accepts --port 0: it binds 127.0.0.1:0 before
building the app, holds that socket for its whole life and prints
PM_TEST_SERVER_PORT=<n> to stdout. startBrowserServer defaults to port 0
and reads the announced port, so no port is reserved and released before
the server binds and two launchers can no longer race for one. The
"address already in use" retry and free-port.mjs are gone; --port N still
works and is checked against the announcement.

* ci(coverage): shard and merge modes for the frontend coverage run

frontend-coverage.mjs now plans its steps from the command line.
No arguments still runs everything, then reports and checks.
--shard I/N runs one slice of the chromium+touch workflows into
coverage/js/raw without wiping it or reporting; shard 1 also runs the
node units and the viewer suite. --merge-only reports and checks over raw
coverage the shards wrote, with --update-baseline as before, and refuses
an empty or missing raw dir. npm scripts: test:coverage:shard and
coverage:merge.

* ci(viewerjs): run browser suites with workers and shards

Frontend coverage runs as three shards (PM_TEST_WORKERS=3 each) that upload
their raw V8 files as coverage-raw-N; a merge job downloads them, fails unless
all three arrived with raw files and no clashing names, then runs
coverage:merge. Firefox and WebKit page workflows run as two shards each with
three workers. The required viewerjs context keeps its name and now needs the
shards, the merge and the browser shards, failing unless every one succeeded.

* ci(viewerjs): cache npm downloads and size timeouts from the first sharded run

The first sharded run lost a webkit shard to its 12-minute limit after all
267 tests passed, and npm ci stalled for 5 minutes in six of eight jobs. Each
npm ci now restores npm's download cache keyed on the vendor lockfile, and
browser job timeouts are the measured times plus at least 50%, never under 20
minutes. The workflow comment records the measurements.

* test(browser): wait for rendering frames, not timers or luck, under load

Two Library workflows failed in CI's webkit shard 2/2 (run 39714840809)
under parallel load:

- "library infinite loading ..." counted cards right after the last
  page hid "Load more". That render queues the window update for the
  next frame, so a slow frame left the 120 cards of the all-rows
  layout in place (received 120, expected < 80). It now waits for the
  next frame, which runs after every callback the page queued.
- "row measurement preserves anchor ..." settled with a 60 ms timer. A
  scroll event is delivered in a rendering frame, so on a starved
  renderer the timer ended before the scroll's frame and no card was in
  view (null anchor). settleLibrary now waits for two consecutive
  native frames with no Library frame queued, checked inside the frame
  callback.

Before/after: a scratch copy that delays every frame by 120 ms (as a
starved runner) failed the old helpers 19 of 40 runs, at the CI lines,
and passed the new ones 40 of 40 on chromium and webkit. Throttled to
1 CPU with 4 workers, the original spec failed 1 of 42 for each test.

The Tags viewer workflow closed the viewer as soon as its image was
visible, before ViewerJS rendered the title, so CI's coverage never ran
the page's title callback (tags-page.js functions 26/46 = 56.52%, one
function above the floor of 56; locally 27/46). The test now asserts the
title names the shown file, which always runs the callback.

* test(browser): stable under parallel workers

Document how to run the browser suites in parallel and sharded, with the
measured times, and what keeps specs stable beside each other: wait for
a rendering frame, a response or a visible state, never for time.

With b665aa6 the full workflows suite passed on chromium, touch, firefox
and webkit with 8 workers, each test 3 times (6132 passed, 0 failed,
24 minutes), and once at CI size (docker --cpus=4, 3 workers: 2044
passed, 25.5 minutes).

The npm download cache removed the npm ci stall: 0 to 2 s in every job
of run 39722172529, against about 300 s in most jobs before. The
workflow's timing comment now records that run; the whole workflow took
8m54s against about 33 minutes before sharding. Shard imbalance (shard 1
carries the concept-comparison specs) is documented, not tuned.

* test(browser): server stderr on failures, no orphan servers, waits on state

Final review fixes for the per-worker test servers (#274):

- A failed test gets the stderr its server wrote during that test
  attached as `server-stderr`, so an aiohttp traceback behind a 500
  reaches the CI report. The launcher keeps the newest 256 KB with a
  stream position per test (stderrMark/stderrSince), and the fixture
  attaches it only when the test did not end as expected.
- The port announcement counts only as a complete line; a chunk ending
  mid-number was read as a shorter port.
- server.py --exit-on-stdin-eof shuts down gracefully (database closed,
  temp root removed) when stdin closes. The launcher holds that pipe,
  so a SIGKILLed worker leaves no server behind, on every OS.
- settled() gives up after 600 frames with the observation stats
  instead of hanging to the test timeout.
- The Classic text-sync test waits for the page's 50 ms timer to
  install serializeValue instead of sleeping 60 ms. With that timer run
  100 ms late (a scratch copy), the old wait failed 20/20 and the poll
  passed 20/20; the spec passed 240/240 at 8 workers x10 and 240/240
  at 1 CPU with 4 workers.
- The workflow test checks each npm cache key names its job's pinned
  image tag.

Every new unit test failed before its change (missing export, the old
regex timing out on the split announcement, an unknown server flag, a
hung settle, a mismatched cache key).

* docs(testing): server lifetime, stderr attachments and the final measured CI times

TESTING.md describes the stdin-EOF shutdown and the server-stderr
attachment, states that the npm cache key names the job's image tag
(now tested), quotes run 39725176320 at cbc8a61 (8m38s) and explains the
20-minute browser job limit. The workflow timing comment quotes the same
run.

* fix(browser): server shutdown and its tests on Windows, Python < 3.13 and CI

Three CI failures on ed809f7:

- Windows: server.py exited 1 on stdin EOF because create_app works with
  the temp root as its cwd, and Windows cannot remove a process's cwd.
  main() now restores the original cwd before the root is removed.
- Python 3.10 to 3.12: the lifetime test's cleanup called communicate()
  after closing stdin, which flushes the closed pipe (ValueError); 3.13
  tolerates it. It now waits and closes the pipes itself, and the exit
  assertion shows the server's stderr. Under 3.12: errors=2 before, OK
  after.
- Tooling (CI container): the SIGKILLed-worker tests treated an exited
  server as alive, because the container's PID 1 does not reap orphans
  and kill(pid, 0) succeeds on a zombie. Reproduced locally with docker
  without --init (2 failed); a zombie now counts as exited (2 passed).
2026-10-21 23:10:57 -07:00