verifaied 0.32.0.dev80__tar.gz → 0.33.0.dev81__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/PKG-INFO +62 -4
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/README.md +61 -3
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/pyproject.toml +1 -1
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/cli.py +20 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/client.py +21 -0
- verifaied-0.33.0.dev81/src/verifaied/lanes.py +108 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/repo_config.py +222 -4
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/testcmd.py +456 -32
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_browser_settings_parity.py +29 -5
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_client.py +45 -0
- verifaied-0.33.0.dev81/tests/test_lanes.py +118 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_repo_config.py +333 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_testcmd.py +879 -5
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/uv.lock +1 -1
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/.gitignore +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/LICENSE +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/__init__.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/__main__.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/agent_mcp.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/agent_steps.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/analyzer.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/audit.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/collect_browser.js +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/collect_browser.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/config.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/failure_text.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/instrument.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/instrument_snippets.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/instrumenter.js +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/instrumenter.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/prompts.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/proxy.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/record.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/recorder_driver.js +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/recorder_observer.js +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/recorder_picker.js +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/recorder_refs.js +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/recorder_spec.js +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/replay_harvest.ts +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/run_reporter.js +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/runner_dir.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/skill/SKILL.md +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/skill/reference/commands.md +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/skillcmd.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/source_index.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/stepcheck.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/subprocess_util.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/suite_report.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/uploader.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/src/verifaied/varcmd.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/__init__.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/conftest.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_agent_mcp.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_audit.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_caption_parity.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_check.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_check_done.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_clear_manual.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_cli.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_collect_browser.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_config.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_console_label_parity.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_instrument.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_instrument_detect.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_instrument_snippets.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_instrumenter.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_pause_parity.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_prompt_parity.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_proxy.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_record.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_replay_harvest_runner.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_skill_parity.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_skillcmd.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_source_index.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_stepcheck_parity.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_suite_report.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_uploader.py +0 -0
- {verifaied-0.32.0.dev80 → verifaied-0.33.0.dev81}/tests/test_varcmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: verifaied
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.33.0.dev81
|
|
4
4
|
Summary: Find what's untested in your code — locally, no account required
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/verifaied/
|
|
6
6
|
Author: Kyle Richards
|
|
@@ -479,6 +479,10 @@ verifaied test run "Settings/API tokens" --video watch
|
|
|
479
479
|
# On a busy CI machine: replay a failed test once more, from a fresh reset.
|
|
480
480
|
verifaied test run --all --retries 1
|
|
481
481
|
|
|
482
|
+
# Replay four at a time — one per copy of the app the environment
|
|
483
|
+
# declares (see "Running several at once" below).
|
|
484
|
+
verifaied test run --all --parallel 4
|
|
485
|
+
|
|
482
486
|
# What's recorded, and how each last went on this branch.
|
|
483
487
|
verifaied test list
|
|
484
488
|
|
|
@@ -820,6 +824,45 @@ origin — scheme and host, no path — and a spec with a relative location
|
|
|
820
824
|
in a repo that names none is refused up front, rather than dying on
|
|
821
825
|
Playwright's "invalid URL" halfway through a run.
|
|
822
826
|
|
|
827
|
+
#### Running several at once
|
|
828
|
+
|
|
829
|
+
A reset before every run is what makes a flow repeatable, and it is also
|
|
830
|
+
what stops two runs sharing one app: each would wipe the other's data
|
|
831
|
+
mid-flow. So running side by side needs one copy of the app per extra
|
|
832
|
+
run, each with its own reset — a **lane**:
|
|
833
|
+
|
|
834
|
+
```toml
|
|
835
|
+
[browser-tests.env.local]
|
|
836
|
+
base_url = "http://localhost:3000"
|
|
837
|
+
reset = "make db-reset"
|
|
838
|
+
|
|
839
|
+
# One more copy of the app, for a second run beside the first.
|
|
840
|
+
[[browser-tests.env.local.lanes]]
|
|
841
|
+
base_url = "http://localhost:3100"
|
|
842
|
+
reset = "make db-reset SLOT=1"
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
That environment runs two at once. An environment with **no** reset (a
|
|
846
|
+
deployed one using `unique` variables) shares its one app between up to
|
|
847
|
+
eight runs; any other runs one at a time. Lanes are checked when the file
|
|
848
|
+
is read: each needs its own `base_url`, a `reset` exactly when its
|
|
849
|
+
environment has one, and no two may point at the same app.
|
|
850
|
+
|
|
851
|
+
How many to run is asked for separately, because it is about the
|
|
852
|
+
batch rather than the machine:
|
|
853
|
+
|
|
854
|
+
- **From the web**, set **Tests run at once** on the repository's
|
|
855
|
+
Settings page (Browser tests → Running tests), or `parallel_runs = 4`
|
|
856
|
+
under `[browser-tests]`. A studio then replays a Run all or a folder's
|
|
857
|
+
Run that many at a time, headless, up to what its environment can
|
|
858
|
+
isolate — and the Browser Tests page says so when it is fewer.
|
|
859
|
+
Recordings and single runs always take turns.
|
|
860
|
+
- **From the terminal**, `verifaied test run --all --parallel 4` (or
|
|
861
|
+
`--folder`). Only the flag does this: the web setting never changes how
|
|
862
|
+
a scripted run behaves. Asking for more than the environment has lanes
|
|
863
|
+
for runs what it can and says why; `--parallel` with a single test or
|
|
864
|
+
with `--proxy` is refused.
|
|
865
|
+
|
|
823
866
|
### Variables — the data that differs, and the data that must be fresh
|
|
824
867
|
|
|
825
868
|
A login is one flow with three passwords. A "create a token called
|
|
@@ -1054,12 +1097,23 @@ cloud can stop it. A request that fails is reported with the reason and
|
|
|
1054
1097
|
the studio carries on — one bad pre-step chain is not a reason for it to
|
|
1055
1098
|
die.
|
|
1056
1099
|
|
|
1100
|
+
**Several at once.** With **Tests run at once** above 1 on the
|
|
1101
|
+
repository's Settings page and an environment that can isolate them
|
|
1102
|
+
(see *Running several at once* above), the studio starts each headless
|
|
1103
|
+
run of a batch on a lane of its own and goes straight back for the next.
|
|
1104
|
+
A recording, an edit or a headed run queued behind the batch waits until
|
|
1105
|
+
the lanes are empty, and is never overtaken by the rest of the batch.
|
|
1106
|
+
The pill says how many it runs at once, and the page shows the studio's
|
|
1107
|
+
own sentence when the machine runs fewer than asked.
|
|
1108
|
+
|
|
1057
1109
|
**Stopping a run from the web.** While a run is replaying, the spinning
|
|
1058
1110
|
Run button on the page turns into **Stop**. The studio hears it on its next
|
|
1059
1111
|
beat, ends the replay's whole process group (SIGTERM, then SIGKILL), and
|
|
1060
1112
|
records the run as failed with "Stopped from the web"; its video and trace
|
|
1061
|
-
are not kept.
|
|
1062
|
-
|
|
1113
|
+
are not kept. **Stop runs** in the page's header ends the whole batch at
|
|
1114
|
+
once — every run in progress, and everything still queued. This uses POSIX
|
|
1115
|
+
process groups (`os.killpg`), so it needs macOS or Linux — the studio does
|
|
1116
|
+
not support Windows.
|
|
1063
1117
|
|
|
1064
1118
|
**Branches.** The branch is re-detected on every beat, so switching
|
|
1065
1119
|
branches under a running studio moves what the web shows and what the next
|
|
@@ -1154,7 +1208,11 @@ run; the **last** one is the verdict, the coverage and the video and trace
|
|
|
1154
1208
|
the test keeps (a failed attempt that is retried keeps its verdict and its
|
|
1155
1209
|
failure text but uploads no video or trace). A refusal — a missing spec, a
|
|
1156
1210
|
variable this machine cannot supply, a reset that failed — is never
|
|
1157
|
-
retried, and neither is a clean run whose coverage upload failed (`2`).
|
|
1211
|
+
retried, and neither is a clean run whose coverage upload failed (`2`).
|
|
1212
|
+
`--parallel <n>` (1–8, default 1, `--all`/`--folder` only) replays up to
|
|
1213
|
+
`n` at once, one per lane of the environment — see *Running several at
|
|
1214
|
+
once*; retries stay in the lane that owns the test, and Ctrl-C stops every
|
|
1215
|
+
lane and exits `130`. `studio` takes `--no-replay` and takes no test name at all —
|
|
1158
1216
|
what it records is whatever the web asks for.
|
|
1159
1217
|
|
|
1160
1218
|
`sync` takes `--repo`, `--branch` (the branch a new test is stamped with),
|
|
@@ -433,6 +433,10 @@ verifaied test run "Settings/API tokens" --video watch
|
|
|
433
433
|
# On a busy CI machine: replay a failed test once more, from a fresh reset.
|
|
434
434
|
verifaied test run --all --retries 1
|
|
435
435
|
|
|
436
|
+
# Replay four at a time — one per copy of the app the environment
|
|
437
|
+
# declares (see "Running several at once" below).
|
|
438
|
+
verifaied test run --all --parallel 4
|
|
439
|
+
|
|
436
440
|
# What's recorded, and how each last went on this branch.
|
|
437
441
|
verifaied test list
|
|
438
442
|
|
|
@@ -774,6 +778,45 @@ origin — scheme and host, no path — and a spec with a relative location
|
|
|
774
778
|
in a repo that names none is refused up front, rather than dying on
|
|
775
779
|
Playwright's "invalid URL" halfway through a run.
|
|
776
780
|
|
|
781
|
+
#### Running several at once
|
|
782
|
+
|
|
783
|
+
A reset before every run is what makes a flow repeatable, and it is also
|
|
784
|
+
what stops two runs sharing one app: each would wipe the other's data
|
|
785
|
+
mid-flow. So running side by side needs one copy of the app per extra
|
|
786
|
+
run, each with its own reset — a **lane**:
|
|
787
|
+
|
|
788
|
+
```toml
|
|
789
|
+
[browser-tests.env.local]
|
|
790
|
+
base_url = "http://localhost:3000"
|
|
791
|
+
reset = "make db-reset"
|
|
792
|
+
|
|
793
|
+
# One more copy of the app, for a second run beside the first.
|
|
794
|
+
[[browser-tests.env.local.lanes]]
|
|
795
|
+
base_url = "http://localhost:3100"
|
|
796
|
+
reset = "make db-reset SLOT=1"
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
That environment runs two at once. An environment with **no** reset (a
|
|
800
|
+
deployed one using `unique` variables) shares its one app between up to
|
|
801
|
+
eight runs; any other runs one at a time. Lanes are checked when the file
|
|
802
|
+
is read: each needs its own `base_url`, a `reset` exactly when its
|
|
803
|
+
environment has one, and no two may point at the same app.
|
|
804
|
+
|
|
805
|
+
How many to run is asked for separately, because it is about the
|
|
806
|
+
batch rather than the machine:
|
|
807
|
+
|
|
808
|
+
- **From the web**, set **Tests run at once** on the repository's
|
|
809
|
+
Settings page (Browser tests → Running tests), or `parallel_runs = 4`
|
|
810
|
+
under `[browser-tests]`. A studio then replays a Run all or a folder's
|
|
811
|
+
Run that many at a time, headless, up to what its environment can
|
|
812
|
+
isolate — and the Browser Tests page says so when it is fewer.
|
|
813
|
+
Recordings and single runs always take turns.
|
|
814
|
+
- **From the terminal**, `verifaied test run --all --parallel 4` (or
|
|
815
|
+
`--folder`). Only the flag does this: the web setting never changes how
|
|
816
|
+
a scripted run behaves. Asking for more than the environment has lanes
|
|
817
|
+
for runs what it can and says why; `--parallel` with a single test or
|
|
818
|
+
with `--proxy` is refused.
|
|
819
|
+
|
|
777
820
|
### Variables — the data that differs, and the data that must be fresh
|
|
778
821
|
|
|
779
822
|
A login is one flow with three passwords. A "create a token called
|
|
@@ -1008,12 +1051,23 @@ cloud can stop it. A request that fails is reported with the reason and
|
|
|
1008
1051
|
the studio carries on — one bad pre-step chain is not a reason for it to
|
|
1009
1052
|
die.
|
|
1010
1053
|
|
|
1054
|
+
**Several at once.** With **Tests run at once** above 1 on the
|
|
1055
|
+
repository's Settings page and an environment that can isolate them
|
|
1056
|
+
(see *Running several at once* above), the studio starts each headless
|
|
1057
|
+
run of a batch on a lane of its own and goes straight back for the next.
|
|
1058
|
+
A recording, an edit or a headed run queued behind the batch waits until
|
|
1059
|
+
the lanes are empty, and is never overtaken by the rest of the batch.
|
|
1060
|
+
The pill says how many it runs at once, and the page shows the studio's
|
|
1061
|
+
own sentence when the machine runs fewer than asked.
|
|
1062
|
+
|
|
1011
1063
|
**Stopping a run from the web.** While a run is replaying, the spinning
|
|
1012
1064
|
Run button on the page turns into **Stop**. The studio hears it on its next
|
|
1013
1065
|
beat, ends the replay's whole process group (SIGTERM, then SIGKILL), and
|
|
1014
1066
|
records the run as failed with "Stopped from the web"; its video and trace
|
|
1015
|
-
are not kept.
|
|
1016
|
-
|
|
1067
|
+
are not kept. **Stop runs** in the page's header ends the whole batch at
|
|
1068
|
+
once — every run in progress, and everything still queued. This uses POSIX
|
|
1069
|
+
process groups (`os.killpg`), so it needs macOS or Linux — the studio does
|
|
1070
|
+
not support Windows.
|
|
1017
1071
|
|
|
1018
1072
|
**Branches.** The branch is re-detected on every beat, so switching
|
|
1019
1073
|
branches under a running studio moves what the web shows and what the next
|
|
@@ -1108,7 +1162,11 @@ run; the **last** one is the verdict, the coverage and the video and trace
|
|
|
1108
1162
|
the test keeps (a failed attempt that is retried keeps its verdict and its
|
|
1109
1163
|
failure text but uploads no video or trace). A refusal — a missing spec, a
|
|
1110
1164
|
variable this machine cannot supply, a reset that failed — is never
|
|
1111
|
-
retried, and neither is a clean run whose coverage upload failed (`2`).
|
|
1165
|
+
retried, and neither is a clean run whose coverage upload failed (`2`).
|
|
1166
|
+
`--parallel <n>` (1–8, default 1, `--all`/`--folder` only) replays up to
|
|
1167
|
+
`n` at once, one per lane of the environment — see *Running several at
|
|
1168
|
+
once*; retries stay in the lane that owns the test, and Ctrl-C stops every
|
|
1169
|
+
lane and exits `130`. `studio` takes `--no-replay` and takes no test name at all —
|
|
1112
1170
|
what it records is whatever the web asks for.
|
|
1113
1171
|
|
|
1114
1172
|
`sync` takes `--repo`, `--branch` (the branch a new test is stamped with),
|
|
@@ -48,6 +48,8 @@ from verifaied.record import DEFAULT_INTERVAL, DEFAULT_PORT, run_record
|
|
|
48
48
|
from verifaied.repo_config import (
|
|
49
49
|
CONFIG_PATH,
|
|
50
50
|
ENV_KEY,
|
|
51
|
+
MAX_PARALLEL_RUNS,
|
|
52
|
+
MIN_PARALLEL_RUNS,
|
|
51
53
|
SECTION,
|
|
52
54
|
RepoConfigError,
|
|
53
55
|
VideoProfile,
|
|
@@ -2064,6 +2066,18 @@ def test_run_command(
|
|
|
2064
2066
|
"is the verdict and coverage recorded. Refusals are never retried."
|
|
2065
2067
|
),
|
|
2066
2068
|
),
|
|
2069
|
+
parallel: int = typer.Option(
|
|
2070
|
+
1,
|
|
2071
|
+
"--parallel",
|
|
2072
|
+
min=MIN_PARALLEL_RUNS,
|
|
2073
|
+
max=MAX_PARALLEL_RUNS,
|
|
2074
|
+
help=(
|
|
2075
|
+
"Replay up to N tests at once (--all/--folder only). Needs an "
|
|
2076
|
+
"environment with no reset, or one that declares "
|
|
2077
|
+
"[[browser-tests.env.<name>.lanes]]; otherwise runs one at a time "
|
|
2078
|
+
"and says why."
|
|
2079
|
+
),
|
|
2080
|
+
),
|
|
2067
2081
|
) -> None:
|
|
2068
2082
|
"""Replay a recorded test, capturing video, trace and coverage.
|
|
2069
2083
|
|
|
@@ -2093,6 +2107,11 @@ def test_run_command(
|
|
|
2093
2107
|
each from a fresh reset. The last attempt is the verdict and the
|
|
2094
2108
|
coverage the test keeps; a refusal is never retried.
|
|
2095
2109
|
|
|
2110
|
+
``--parallel N`` replays up to N tests of an ``--all`` or ``--folder``
|
|
2111
|
+
run at once, one per lane of the environment — each lane its own copy
|
|
2112
|
+
of the app with its own reset. Only this flag turns it on: the
|
|
2113
|
+
Settings page's "Tests run at once" is for the studio.
|
|
2114
|
+
|
|
2096
2115
|
Exit codes:
|
|
2097
2116
|
|
|
2098
2117
|
0 every test replayed cleanly
|
|
@@ -2125,6 +2144,7 @@ def test_run_command(
|
|
|
2125
2144
|
proxy_port=proxy_port,
|
|
2126
2145
|
video=video.value if video is not None else None,
|
|
2127
2146
|
retries=retries,
|
|
2147
|
+
parallel=parallel,
|
|
2128
2148
|
)
|
|
2129
2149
|
except (RecordedTestError, UploaderError) as e:
|
|
2130
2150
|
err_console.print(f"[red]VerifAIed error[/red]: {e}")
|
|
@@ -723,6 +723,10 @@ def post_recorder_heartbeat(
|
|
|
723
723
|
environment: str | None = None,
|
|
724
724
|
busy: bool = False,
|
|
725
725
|
working: UUID | None = None,
|
|
726
|
+
working_ids: list[UUID] | None = None,
|
|
727
|
+
take_kinds: list[str] | None = None,
|
|
728
|
+
lanes: int | None = None,
|
|
729
|
+
lanes_note: str | None = None,
|
|
726
730
|
timeout: float = 30.0,
|
|
727
731
|
) -> dict[str, Any]:
|
|
728
732
|
"""Say a studio is listening, and collect what to record next.
|
|
@@ -761,6 +765,15 @@ def post_recorder_heartbeat(
|
|
|
761
765
|
becoming a status code because presence must keep working when the
|
|
762
766
|
branch allowance is spent. A service that predates any of this sends
|
|
763
767
|
none of the three, which reads as nothing to say.
|
|
768
|
+
|
|
769
|
+
The lane fields are a studio replaying several headless runs at once:
|
|
770
|
+
``working_ids`` names every run in flight, ``take_kinds`` narrows what
|
|
771
|
+
this beat may be handed to another headless run, and ``lanes`` /
|
|
772
|
+
``lanes_note`` say how many it runs at once and why not more. Each is
|
|
773
|
+
sent only when given, so a studio running one at a time beats exactly
|
|
774
|
+
as it always did. The answer's ``parallel_runs`` is what the
|
|
775
|
+
repository's Settings page asks for; a service that predates lanes
|
|
776
|
+
leaves the key out.
|
|
764
777
|
"""
|
|
765
778
|
request_url = f"{api_url.rstrip('/')}/repositories/{repo_id}/recorder-heartbeat"
|
|
766
779
|
body: dict[str, Any] = {"branch": branch, "closing": closing}
|
|
@@ -774,6 +787,14 @@ def post_recorder_heartbeat(
|
|
|
774
787
|
# Which run this busy studio is on, so the service does not stop it
|
|
775
788
|
# as a run whose studio went away. Sent only when named.
|
|
776
789
|
body["working"] = str(working)
|
|
790
|
+
if working_ids:
|
|
791
|
+
body["working_ids"] = [str(i) for i in working_ids]
|
|
792
|
+
if take_kinds is not None:
|
|
793
|
+
body["take_kinds"] = take_kinds
|
|
794
|
+
if lanes is not None:
|
|
795
|
+
body["lanes"] = lanes
|
|
796
|
+
if lanes_note is not None:
|
|
797
|
+
body["lanes_note"] = lanes_note
|
|
777
798
|
try:
|
|
778
799
|
response = _http().post(
|
|
779
800
|
request_url,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Several headless replays at once, one per lane.
|
|
2
|
+
|
|
3
|
+
A lane is one copy of the app a replay can run against without another
|
|
4
|
+
replay's reset wiping it (``lane_capacity`` / ``lane_env`` in
|
|
5
|
+
``repo_config.py`` decide how many there are and where each points).
|
|
6
|
+
This module is only the bookkeeping: which lane is free, which request
|
|
7
|
+
each lane is on, and how a stop reaches the one thread that can act on
|
|
8
|
+
it. It knows nothing about replays, so the studio and ``test run
|
|
9
|
+
--parallel`` share it and its tests need no browser.
|
|
10
|
+
|
|
11
|
+
Each lane runs on a daemon thread of its own, and removes itself from
|
|
12
|
+
the pool when its work returns or raises — a lane that died must never
|
|
13
|
+
read as busy forever.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
from collections.abc import Callable, Iterable
|
|
20
|
+
from typing import Protocol
|
|
21
|
+
from uuid import UUID
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LaneStop(Protocol):
|
|
25
|
+
"""What a lane's work is handed to be stopped by (``RunStop``)."""
|
|
26
|
+
|
|
27
|
+
def set(self) -> None: ...
|
|
28
|
+
|
|
29
|
+
def interrupt(self, message: str) -> None: ...
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _Lane:
|
|
33
|
+
def __init__(self, request_id: UUID, stop: LaneStop) -> None:
|
|
34
|
+
self.request_id = request_id
|
|
35
|
+
self.stop = stop
|
|
36
|
+
self.thread: threading.Thread | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LanePool:
|
|
40
|
+
"""The lanes in flight, keyed by lane number."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, make_stop: Callable[[], LaneStop]) -> None:
|
|
43
|
+
self._make_stop = make_stop
|
|
44
|
+
self._lanes: dict[int, _Lane] = {}
|
|
45
|
+
self._lock = threading.Lock()
|
|
46
|
+
|
|
47
|
+
def in_flight(self) -> list[UUID]:
|
|
48
|
+
"""The requests being worked on, in lane order."""
|
|
49
|
+
with self._lock:
|
|
50
|
+
return [self._lanes[i].request_id for i in sorted(self._lanes)]
|
|
51
|
+
|
|
52
|
+
def free_lane(self, size: int) -> int | None:
|
|
53
|
+
"""The lowest free lane below ``size``, or None when all are busy."""
|
|
54
|
+
with self._lock:
|
|
55
|
+
return next((i for i in range(size) if i not in self._lanes), None)
|
|
56
|
+
|
|
57
|
+
def start(
|
|
58
|
+
self,
|
|
59
|
+
request_id: UUID,
|
|
60
|
+
lane: int,
|
|
61
|
+
work: Callable[[LaneStop, int], None],
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Run ``work(stop, lane)`` on a thread of its own in ``lane``."""
|
|
64
|
+
entry = _Lane(request_id, self._make_stop())
|
|
65
|
+
|
|
66
|
+
def run() -> None:
|
|
67
|
+
try:
|
|
68
|
+
work(entry.stop, lane)
|
|
69
|
+
finally:
|
|
70
|
+
with self._lock:
|
|
71
|
+
del self._lanes[lane]
|
|
72
|
+
|
|
73
|
+
with self._lock:
|
|
74
|
+
if lane in self._lanes:
|
|
75
|
+
raise ValueError(f"lane {lane} is already running")
|
|
76
|
+
self._lanes[lane] = entry
|
|
77
|
+
entry.thread = threading.Thread(
|
|
78
|
+
target=run, name=f"verifaied-lane-{lane}", daemon=True
|
|
79
|
+
)
|
|
80
|
+
entry.thread.start()
|
|
81
|
+
|
|
82
|
+
def stop(self, ids: Iterable[object]) -> None:
|
|
83
|
+
"""Stop the lanes working on any of ``ids``; others are left alone."""
|
|
84
|
+
wanted = {str(i) for i in ids}
|
|
85
|
+
with self._lock:
|
|
86
|
+
lanes = list(self._lanes.values())
|
|
87
|
+
for entry in lanes:
|
|
88
|
+
if str(entry.request_id) in wanted:
|
|
89
|
+
entry.stop.set()
|
|
90
|
+
|
|
91
|
+
def interrupt_all(self, message: str) -> None:
|
|
92
|
+
"""Stop every lane, closing each with ``message``."""
|
|
93
|
+
with self._lock:
|
|
94
|
+
lanes = list(self._lanes.values())
|
|
95
|
+
for entry in lanes:
|
|
96
|
+
entry.stop.interrupt(message)
|
|
97
|
+
|
|
98
|
+
def join_all(self, poll: float = 0.5) -> None:
|
|
99
|
+
"""Wait for every lane in flight to finish.
|
|
100
|
+
|
|
101
|
+
In short waits rather than one, so a Ctrl-C reaches the waiting
|
|
102
|
+
thread at once instead of after the slowest lane.
|
|
103
|
+
"""
|
|
104
|
+
with self._lock:
|
|
105
|
+
threads = [e.thread for e in self._lanes.values() if e.thread]
|
|
106
|
+
for thread in threads:
|
|
107
|
+
while thread.is_alive():
|
|
108
|
+
thread.join(poll)
|