verifaied 0.21.1.dev45__tar.gz → 0.23.0.dev47__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.21.1.dev45 → verifaied-0.23.0.dev47}/PKG-INFO +1 -1
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/pyproject.toml +1 -1
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/cli.py +38 -16
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/client.py +5 -4
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/record.py +21 -17
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/testcmd.py +96 -23
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_record.py +4 -3
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_testcmd.py +167 -6
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/uv.lock +1 -1
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/.gitignore +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/LICENSE +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/README.md +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/__init__.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/__main__.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/analyzer.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/audit.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/config.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/failure_text.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/instrument.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/instrument_snippets.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/instrumenter.js +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/instrumenter.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/prompts.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/proxy.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/recorder_driver.js +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/recorder_observer.js +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/recorder_picker.js +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/recorder_refs.js +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/recorder_spec.js +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/repo_config.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/run_reporter.js +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/source_index.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/subprocess_util.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/suite_report.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/uploader.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/src/verifaied/varcmd.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/__init__.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/conftest.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_audit.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_check.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_check_done.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_clear_manual.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_cli.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_client.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_config.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_instrument.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_instrument_detect.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_instrument_snippets.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_instrumenter.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_prompt_parity.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_proxy.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_repo_config.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_reporter_parity.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_source_index.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_suite_report.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_uploader.py +0 -0
- {verifaied-0.21.1.dev45 → verifaied-0.23.0.dev47}/tests/test_varcmd.py +0 -0
|
@@ -142,10 +142,12 @@ def upload_command(
|
|
|
142
142
|
False,
|
|
143
143
|
"--browser",
|
|
144
144
|
help=(
|
|
145
|
-
"Upload as browser
|
|
146
|
-
"
|
|
147
|
-
"coverage.
|
|
148
|
-
"
|
|
145
|
+
"Upload as browser coverage (the independent 'which code was "
|
|
146
|
+
"exercised in a browser' dimension) instead of test-suite "
|
|
147
|
+
"coverage. Counts as this branch's suite run, replacing what the "
|
|
148
|
+
"last wrapped run contributed and nothing else. Accepts Istanbul "
|
|
149
|
+
"(frontend harvest) or pytest-cov (coverage-instrumented "
|
|
150
|
+
"backend); incompatible with --junit."
|
|
149
151
|
),
|
|
150
152
|
),
|
|
151
153
|
commit_sha: str | None = typer.Option(
|
|
@@ -523,9 +525,10 @@ def clear_manual_command(
|
|
|
523
525
|
accumulation and start fresh: every function goes back to Unexercised,
|
|
524
526
|
but the Manual Recording view stays visible.
|
|
525
527
|
|
|
526
|
-
|
|
527
|
-
`verifaied collect -- <cmd>`) has no clear:
|
|
528
|
-
|
|
528
|
+
Browser coverage (`verifaied upload --browser`, `verifaied test run`, or
|
|
529
|
+
`verifaied collect -- <cmd>`) has no clear: it is the union of what each
|
|
530
|
+
producer last reached, and every producer replaces its own contribution
|
|
531
|
+
on its next run.
|
|
529
532
|
|
|
530
533
|
Exit codes:
|
|
531
534
|
0 cleared
|
|
@@ -809,13 +812,14 @@ def record_command(
|
|
|
809
812
|
arrived.
|
|
810
813
|
|
|
811
814
|
The two modes feed two different dimensions, because they answer
|
|
812
|
-
different questions. Wrapping a command uploads as
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
815
|
+
different questions. Wrapping a command uploads as this branch's
|
|
816
|
+
**suite** contribution to its Browser coverage: a suite run is a
|
|
817
|
+
complete snapshot of itself, so each wrapped run **replaces** the last
|
|
818
|
+
one's contribution — and nothing else's, so replaying a single recorded
|
|
819
|
+
test never withdraws it. There is nothing to clear. A bare session
|
|
820
|
+
uploads as **manual**: a person covers a flow, stops, comes back, so
|
|
821
|
+
those **accumulate** on the Manual Recording page until
|
|
822
|
+
``verifaied clear-manual`` starts a fresh pass.
|
|
819
823
|
|
|
820
824
|
For a repeatable, named flow, use ``verifaied test record`` instead —
|
|
821
825
|
it drives Playwright's recorder through verifAIed's own driver, saves
|
|
@@ -1728,12 +1732,20 @@ def test_run_command(
|
|
|
1728
1732
|
metavar="[[<feature path>/]<name>]",
|
|
1729
1733
|
help=(
|
|
1730
1734
|
'Which test to run, e.g. "Settings/API tokens". The feature path '
|
|
1731
|
-
"is optional and may nest. Omit with --all."
|
|
1735
|
+
"is optional and may nest. Omit with --all or --folder."
|
|
1732
1736
|
),
|
|
1733
1737
|
),
|
|
1734
1738
|
run_all: bool = typer.Option(
|
|
1735
1739
|
False, "--all", help="Run every recorded test, in order, and report the worst."
|
|
1736
1740
|
),
|
|
1741
|
+
folder: str | None = typer.Option(
|
|
1742
|
+
None,
|
|
1743
|
+
"--folder",
|
|
1744
|
+
help=(
|
|
1745
|
+
'Run every recorded test under one folder, e.g. --folder "Settings" '
|
|
1746
|
+
"— nested folders included."
|
|
1747
|
+
),
|
|
1748
|
+
),
|
|
1737
1749
|
proxy: bool = typer.Option(
|
|
1738
1750
|
False,
|
|
1739
1751
|
"--proxy",
|
|
@@ -1822,11 +1834,20 @@ def test_run_command(
|
|
|
1822
1834
|
) -> None:
|
|
1823
1835
|
"""Replay a recorded test, capturing video, trace and coverage.
|
|
1824
1836
|
|
|
1837
|
+
Three forms, exactly one at a time: a test by name, ``--folder
|
|
1838
|
+
"<folder path>"`` for everything nested under one folder, or ``--all``
|
|
1839
|
+
for every recorded test in the repository. A folder is matched the way a
|
|
1840
|
+
name is — by slug — so it is the same path the web composes.
|
|
1841
|
+
|
|
1825
1842
|
Each run opens a session on the branch, so the video and the lines it
|
|
1826
1843
|
reached sit together in verifAIed, and the run's pass/fail becomes the
|
|
1827
1844
|
branch's answer for that test — which is what clears the
|
|
1828
1845
|
``unverified_recorded_test`` gap in ``check_done``.
|
|
1829
1846
|
|
|
1847
|
+
The coverage becomes **this test's** contribution to the branch's
|
|
1848
|
+
Browser coverage, replacing what the same test last reached and never
|
|
1849
|
+
touching the suite's or another test's.
|
|
1850
|
+
|
|
1830
1851
|
``--env`` picks which environment from ``.verifaied/config.toml``
|
|
1831
1852
|
every spec in this run is pointed at: its ``base_url`` becomes
|
|
1832
1853
|
Playwright's ``baseURL``, its ``reset`` runs before **each** spec so
|
|
@@ -1840,7 +1861,7 @@ def test_run_command(
|
|
|
1840
1861
|
0 every test replayed cleanly
|
|
1841
1862
|
* the worst exit code across the run
|
|
1842
1863
|
2 a run was clean but its final coverage upload failed
|
|
1843
|
-
1 usage / config error (no token, unknown test, no Playwright)
|
|
1864
|
+
1 usage / config error (no token, unknown test or folder, no Playwright)
|
|
1844
1865
|
"""
|
|
1845
1866
|
resolved_url, resolved_token, repo_id = _test_prelude(
|
|
1846
1867
|
repo=repo, api_url=api_url, token=token
|
|
@@ -1855,6 +1876,7 @@ def test_run_command(
|
|
|
1855
1876
|
commit_sha=commit_sha,
|
|
1856
1877
|
ref=ref,
|
|
1857
1878
|
run_all=run_all,
|
|
1879
|
+
folder=folder,
|
|
1858
1880
|
runner_dir=runner_dir,
|
|
1859
1881
|
port=port,
|
|
1860
1882
|
interval=interval,
|
|
@@ -785,10 +785,11 @@ def clear_manual_coverage(
|
|
|
785
785
|
"""DELETE the accumulated manual dimension for a (repo, branch).
|
|
786
786
|
|
|
787
787
|
Manual `collect` sessions accumulate server-side; this resets them so
|
|
788
|
-
the next pass starts fresh.
|
|
789
|
-
|
|
790
|
-
(``{cleared_functions}``). Raises
|
|
791
|
-
means the branch has no local
|
|
788
|
+
the next pass starts fresh. Browser coverage is untouched — every
|
|
789
|
+
producer there replaces its own contribution, so it has no clear.
|
|
790
|
+
Returns the parsed response (``{cleared_functions}``). Raises
|
|
791
|
+
``ApiError`` on any non-2xx — a 404 means the branch has no local
|
|
792
|
+
analysis to clear.
|
|
792
793
|
"""
|
|
793
794
|
url = f"{api_url.rstrip('/')}/repositories/{repo_id}/manual-coverage"
|
|
794
795
|
try:
|
|
@@ -336,11 +336,13 @@ def flush_upload(
|
|
|
336
336
|
) -> UploadResult | None:
|
|
337
337
|
"""Upload the merged state as session coverage, if it changed.
|
|
338
338
|
|
|
339
|
-
``coverage_kind`` picks the dimension: ``"browser"``
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
``coverage_kind`` picks the dimension: ``"browser"`` when this collector
|
|
340
|
+
is wrapping a suite command, ``"manual"`` for a bare interactive
|
|
341
|
+
session. They differ server-side — a browser upload becomes this
|
|
342
|
+
branch's suite contribution, replacing what the last wrapped run
|
|
343
|
+
contributed and nothing else, while manual accumulates until cleared —
|
|
344
|
+
which is exactly the difference between "a suite just ran" and "someone
|
|
345
|
+
is clicking around".
|
|
344
346
|
|
|
345
347
|
Returns None when nothing new arrived since the last *successful*
|
|
346
348
|
upload. The merged JSON goes through a tempfile so the existing
|
|
@@ -396,9 +398,9 @@ def flush_upload(
|
|
|
396
398
|
def format_upload_summary(result: UploadResult, coverage_kind: str = "manual") -> str:
|
|
397
399
|
"""One line per upload: what *this* session's dimension now looks like.
|
|
398
400
|
|
|
399
|
-
A session must report its own numbers. Printing the
|
|
400
|
-
hand-driven pass would tell the operator their clicking
|
|
401
|
-
the two dimensions move independently by design.
|
|
401
|
+
A session must report its own numbers. Printing the branch's browser
|
|
402
|
+
counts after a hand-driven pass would tell the operator their clicking
|
|
403
|
+
had no effect — the two dimensions move independently by design.
|
|
402
404
|
"""
|
|
403
405
|
if coverage_kind == "browser":
|
|
404
406
|
exercised = result.browser_exercised_count
|
|
@@ -1103,10 +1105,10 @@ def run_record(
|
|
|
1103
1105
|
flow), exiting 0 — or 2 if the final upload failed. Uploads as the
|
|
1104
1106
|
**manual** dimension.
|
|
1105
1107
|
|
|
1106
|
-
With ``command``: run it with the collector alongside (the
|
|
1108
|
+
With ``command``: run it with the collector alongside (the suite flow,
|
|
1107
1109
|
``verifaied collect -- npx playwright test``), then flush and exit with
|
|
1108
1110
|
the command's own code, so the wrapper is transparent to CI. Uploads as
|
|
1109
|
-
|
|
1111
|
+
this branch's **suite** contribution to its browser coverage.
|
|
1110
1112
|
|
|
1111
1113
|
``port=None`` resolves per mode: the reporter's fixed default for a
|
|
1112
1114
|
bare session, an ephemeral port for a wrapped one.
|
|
@@ -1138,12 +1140,14 @@ def run_record(
|
|
|
1138
1140
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
1139
1141
|
thread.start()
|
|
1140
1142
|
|
|
1141
|
-
# Wrapping a command means a suite just ran, which is the
|
|
1142
|
-
#
|
|
1143
|
-
#
|
|
1144
|
-
#
|
|
1143
|
+
# Wrapping a command means a suite just ran, which is the branch's
|
|
1144
|
+
# suite contribution to its browser coverage (replacing what the last
|
|
1145
|
+
# wrapped run contributed, and nothing else's). A bare session is a
|
|
1146
|
+
# person clicking around, which is the manual dimension (accumulated
|
|
1147
|
+
# until cleared). The mode already tells us which, so the user never
|
|
1148
|
+
# has to.
|
|
1145
1149
|
coverage_kind = "browser" if command else "manual"
|
|
1146
|
-
dimension = "
|
|
1150
|
+
dimension = "suite" if command else "manual"
|
|
1147
1151
|
console.print(
|
|
1148
1152
|
f"[green]Recording[/green] {dimension} browser coverage on "
|
|
1149
1153
|
f"[bold]http://{HOST}:{bound_port}[/bold] → branch "
|
|
@@ -1154,8 +1158,8 @@ def run_record(
|
|
|
1154
1158
|
console.print(
|
|
1155
1159
|
f"[dim]Running: {' '.join(command)} "
|
|
1156
1160
|
f"(VITE_COVERAGE=true, VERIFAIED_RECORD_PORT={bound_port}). "
|
|
1157
|
-
"Results land
|
|
1158
|
-
"
|
|
1161
|
+
"Results land on the branch's Browser coverage page as the "
|
|
1162
|
+
"suite's contribution.[/dim]"
|
|
1159
1163
|
)
|
|
1160
1164
|
elif not proxy:
|
|
1161
1165
|
console.print(
|
|
@@ -189,7 +189,7 @@ RUN_PROGRESS_MIN_INTERVAL = 0.5
|
|
|
189
189
|
# the whole compatibility contract in one place: a CLI that predates runs
|
|
190
190
|
# sends no field at all, is stored as record-only, and never takes a run
|
|
191
191
|
# it would not know how to answer.
|
|
192
|
-
STUDIO_KINDS: tuple[str, ...] = ("record", "run", "edit", "amend")
|
|
192
|
+
STUDIO_KINDS: tuple[str, ...] = ("record", "run", "run_headless", "edit", "amend")
|
|
193
193
|
|
|
194
194
|
# ``major.minor`` prefixes of ``@playwright/test`` the recorder driver has
|
|
195
195
|
# been verified against. **Fail-closed**: the driver reaches an internal
|
|
@@ -2828,9 +2828,14 @@ def replay_spec(
|
|
|
2828
2828
|
|
|
2829
2829
|
This is where a recorded test becomes evidence: the spec runs with
|
|
2830
2830
|
video and trace on, under the coverage collector, against a session
|
|
2831
|
-
linked to the test. Video, trace and
|
|
2832
|
-
|
|
2833
|
-
|
|
2831
|
+
linked to the test. Video, trace and coverage all land on that session,
|
|
2832
|
+
and the session's exit code becomes the branch's pass/fail for the test
|
|
2833
|
+
when it closes.
|
|
2834
|
+
|
|
2835
|
+
The coverage becomes **this test's** contribution to the branch's
|
|
2836
|
+
browser coverage, replacing what the same test last reached and never
|
|
2837
|
+
touching the suite's or another test's — which is what makes "wrap your
|
|
2838
|
+
suite" and "run this test" two remedies that no longer undo each other.
|
|
2834
2839
|
|
|
2835
2840
|
With pre-steps, what actually runs is a **composed** spec — their
|
|
2836
2841
|
bodies then this one, in a throwaway file — and the coverage that
|
|
@@ -3223,8 +3228,8 @@ def _run_playwright(
|
|
|
3223
3228
|
|
|
3224
3229
|
The child runs on a thread rather than in the foreground so the
|
|
3225
3230
|
collector can upload on an interval — a long flow's coverage shouldn't
|
|
3226
|
-
all land in one lump at the end, and the Browser
|
|
3227
|
-
in as the replay proceeds is what makes a slow run legible.
|
|
3231
|
+
all land in one lump at the end, and the branch's Browser coverage
|
|
3232
|
+
filling in as the replay proceeds is what makes a slow run legible.
|
|
3228
3233
|
|
|
3229
3234
|
``var_env`` is the selected environment's variables, which is where a
|
|
3230
3235
|
normalized spec's ``process.env.VERIFAIED_VAR_*`` references are
|
|
@@ -6801,9 +6806,16 @@ def run_test_studio(
|
|
|
6801
6806
|
# against a service that hasn't been deployed yet.
|
|
6802
6807
|
cancelled = False
|
|
6803
6808
|
with keeper(busy_beat):
|
|
6804
|
-
|
|
6809
|
+
kind = request.get("kind")
|
|
6810
|
+
if kind in ("run", "run_headless"):
|
|
6805
6811
|
_run_requested(
|
|
6806
6812
|
request,
|
|
6813
|
+
# A single Run is somebody sitting in front of
|
|
6814
|
+
# this machine watching one flow; a whole folder
|
|
6815
|
+
# or a whole branch is not, and forty windows
|
|
6816
|
+
# opening in front of them is why nobody would
|
|
6817
|
+
# click it twice.
|
|
6818
|
+
headed=kind == "run",
|
|
6807
6819
|
api_url=api_url,
|
|
6808
6820
|
token=token,
|
|
6809
6821
|
repo_id=repo_id,
|
|
@@ -6820,7 +6832,7 @@ def run_test_studio(
|
|
|
6820
6832
|
runner=runner,
|
|
6821
6833
|
shell_runner=shell_runner,
|
|
6822
6834
|
)
|
|
6823
|
-
elif
|
|
6835
|
+
elif kind == "amend":
|
|
6824
6836
|
_amend_requested(
|
|
6825
6837
|
request,
|
|
6826
6838
|
api_url=api_url,
|
|
@@ -6840,7 +6852,7 @@ def run_test_studio(
|
|
|
6840
6852
|
runner=runner,
|
|
6841
6853
|
shell_runner=shell_runner,
|
|
6842
6854
|
)
|
|
6843
|
-
elif
|
|
6855
|
+
elif kind == "edit":
|
|
6844
6856
|
cancelled = _edit_requested(
|
|
6845
6857
|
request,
|
|
6846
6858
|
api_url=api_url,
|
|
@@ -7185,6 +7197,7 @@ def _amend_requested(
|
|
|
7185
7197
|
def _run_requested(
|
|
7186
7198
|
request: dict[str, Any],
|
|
7187
7199
|
*,
|
|
7200
|
+
headed: bool,
|
|
7188
7201
|
api_url: str,
|
|
7189
7202
|
token: str,
|
|
7190
7203
|
repo_id: UUID,
|
|
@@ -7201,7 +7214,7 @@ def _run_requested(
|
|
|
7201
7214
|
shell_runner: ShellRunner,
|
|
7202
7215
|
env_name: str | None = None,
|
|
7203
7216
|
) -> None:
|
|
7204
|
-
"""Replay the one test a heartbeat handed over
|
|
7217
|
+
"""Replay the one test a heartbeat handed over.
|
|
7205
7218
|
|
|
7206
7219
|
The same shape as :func:`_record_requested`, and for the same reasons:
|
|
7207
7220
|
``recording`` goes out first because replaying a chain of pre-steps can
|
|
@@ -7209,10 +7222,13 @@ def _run_requested(
|
|
|
7209
7222
|
it otherwise, and the rows are re-listed rather than trusted from the
|
|
7210
7223
|
request, because the spec path and the stored chain live on the row.
|
|
7211
7224
|
|
|
7212
|
-
|
|
7213
|
-
sitting in front of this machine,
|
|
7214
|
-
|
|
7215
|
-
|
|
7225
|
+
``headed`` is the whole difference between the two run kinds. A single
|
|
7226
|
+
Run is somebody clicking one test and sitting in front of this machine,
|
|
7227
|
+
so watching the browser do it is the point; a folder or a whole branch
|
|
7228
|
+
queued at once is nobody watching, and opening forty windows in front of
|
|
7229
|
+
them would be the reason they never clicked it again. The run's own
|
|
7230
|
+
verdict lands on the session either way — this reports only that the
|
|
7231
|
+
replay happened.
|
|
7216
7232
|
"""
|
|
7217
7233
|
request_id = UUID(str(request["id"]))
|
|
7218
7234
|
post_recording_request_status(
|
|
@@ -7239,7 +7255,7 @@ def _run_requested(
|
|
|
7239
7255
|
console.print(
|
|
7240
7256
|
f"[green]Running[/green] "
|
|
7241
7257
|
f"[bold]{ref_of(test['feature'], test['name'])}[/bold] "
|
|
7242
|
-
"from the queue."
|
|
7258
|
+
"from the queue." + ("" if headed else " [dim](headless)[/dim]")
|
|
7243
7259
|
)
|
|
7244
7260
|
replay_spec(
|
|
7245
7261
|
api_url=api_url,
|
|
@@ -7258,7 +7274,7 @@ def _run_requested(
|
|
|
7258
7274
|
console=console,
|
|
7259
7275
|
err_console=err_console,
|
|
7260
7276
|
all_rows=rows,
|
|
7261
|
-
headed=
|
|
7277
|
+
headed=headed,
|
|
7262
7278
|
env_name=env_name,
|
|
7263
7279
|
runner=runner,
|
|
7264
7280
|
shell_runner=shell_runner,
|
|
@@ -7291,6 +7307,34 @@ def _fail_request(
|
|
|
7291
7307
|
err_console.print(f"[yellow]could not report the failure[/yellow]: {e}")
|
|
7292
7308
|
|
|
7293
7309
|
|
|
7310
|
+
def in_folder(row: dict[str, Any], folder_slug: str) -> bool:
|
|
7311
|
+
"""Whether one listed test lives under a folder, at any depth.
|
|
7312
|
+
|
|
7313
|
+
Matched on the **slug**, the way :func:`find_test` matches a name, so
|
|
7314
|
+
``--folder "API tokens"`` and ``--folder api-tokens`` are the same
|
|
7315
|
+
folder — and so the flag agrees with the path the web composes.
|
|
7316
|
+
|
|
7317
|
+
Nested tests count: a folder is a place, and "run Settings" meaning "but
|
|
7318
|
+
not the tests in Settings/Tokens" is a distinction nobody drew when they
|
|
7319
|
+
filed them there. The ``/`` in the prefix test is what keeps
|
|
7320
|
+
``settings-api`` out of ``settings``.
|
|
7321
|
+
"""
|
|
7322
|
+
slug = str(row.get("feature_slug") or "")
|
|
7323
|
+
return slug == folder_slug or slug.startswith(f"{folder_slug}/")
|
|
7324
|
+
|
|
7325
|
+
|
|
7326
|
+
def no_folder_match(folder: str, rows: list[dict[str, Any]]) -> str:
|
|
7327
|
+
"""What to say when ``--folder`` named nothing that can be run.
|
|
7328
|
+
|
|
7329
|
+
The folders that do exist are the fix, so they are the message: a typo
|
|
7330
|
+
is answered by the list the user meant to pick from rather than by
|
|
7331
|
+
"nothing matched".
|
|
7332
|
+
"""
|
|
7333
|
+
folders = sorted({str(row.get("feature") or "") for row in rows} - {""})
|
|
7334
|
+
known = ", ".join(folders) if folders else "none yet"
|
|
7335
|
+
return f'no recorded tests under "{folder}" — the folders here are: {known}'
|
|
7336
|
+
|
|
7337
|
+
|
|
7294
7338
|
def run_test_run(
|
|
7295
7339
|
*,
|
|
7296
7340
|
api_url: str,
|
|
@@ -7301,6 +7345,7 @@ def run_test_run(
|
|
|
7301
7345
|
commit_sha: str | None,
|
|
7302
7346
|
ref: str | None,
|
|
7303
7347
|
run_all: bool,
|
|
7348
|
+
folder: str | None = None,
|
|
7304
7349
|
runner_dir: Path | None,
|
|
7305
7350
|
port: int,
|
|
7306
7351
|
interval: float,
|
|
@@ -7313,7 +7358,13 @@ def run_test_run(
|
|
|
7313
7358
|
runner: ProcessRunner = run_process,
|
|
7314
7359
|
shell_runner: ShellRunner = run_shell_process,
|
|
7315
7360
|
) -> int:
|
|
7316
|
-
"""Replay one recorded test,
|
|
7361
|
+
"""Replay one recorded test, every ready one, or a folder of them.
|
|
7362
|
+
|
|
7363
|
+
Exactly one of the three says what to run: a name, ``--all``, or
|
|
7364
|
+
``--folder "<path>"``. A folder is matched by **slug**, the way a name
|
|
7365
|
+
is, and it takes everything nested under it — a folder is a place, and
|
|
7366
|
+
"run Settings" meaning "not the tests in Settings/Tokens" would be a
|
|
7367
|
+
distinction nobody drew when they filed them there.
|
|
7317
7368
|
|
|
7318
7369
|
Returns the **worst** exit code across the run: one broken flow in ten
|
|
7319
7370
|
is a failure, and reporting the last one's code would let it hide
|
|
@@ -7329,24 +7380,40 @@ def run_test_run(
|
|
|
7329
7380
|
one answer for the whole invocation, because "--all against stage"
|
|
7330
7381
|
is one question.
|
|
7331
7382
|
"""
|
|
7332
|
-
|
|
7383
|
+
chosen = sum((ref is not None, run_all, folder is not None))
|
|
7384
|
+
if chosen != 1:
|
|
7333
7385
|
raise RecordedTestError(
|
|
7334
|
-
|
|
7335
|
-
|
|
7386
|
+
"say what to run, and only one of the three: name a test — "
|
|
7387
|
+
'`verifaied test run "<feature path>/<name>"` — or pass '
|
|
7388
|
+
'--folder "<folder path>" for everything under one folder, or '
|
|
7389
|
+
"--all for every recorded test"
|
|
7390
|
+
)
|
|
7391
|
+
# Before the listing, because it is a question about the flag rather
|
|
7392
|
+
# than about the repo: ``slugify_feature_path("")`` is ``""``, which
|
|
7393
|
+
# matches the rootless tests and nothing else — a silent answer to a
|
|
7394
|
+
# question the user did not ask.
|
|
7395
|
+
if folder is not None and not folder.strip():
|
|
7396
|
+
raise RecordedTestError(
|
|
7397
|
+
"`--folder` needs a folder path; use --all to run every recorded test"
|
|
7336
7398
|
)
|
|
7337
7399
|
resolved_branch = branch if branch is not None else detect_branch()
|
|
7338
7400
|
resolved_runner_dir = find_runner_dir(root, runner_dir)
|
|
7339
7401
|
rows = list_recorded_tests(
|
|
7340
7402
|
api_url=api_url, token=token, repo_id=repo_id, branch=resolved_branch
|
|
7341
7403
|
)
|
|
7342
|
-
if run_all:
|
|
7404
|
+
if run_all or folder is not None:
|
|
7343
7405
|
ready = [r for r in rows if r.get("status") == "ready"]
|
|
7406
|
+
if folder is not None:
|
|
7407
|
+
folder_slug = slugify_feature_path(folder)
|
|
7408
|
+
scoped = [r for r in ready if in_folder(r, folder_slug)]
|
|
7409
|
+
else:
|
|
7410
|
+
scoped = ready
|
|
7344
7411
|
# Suite rows are born ready, so they would otherwise be swept up
|
|
7345
7412
|
# by --all and replayed under a config that isn't theirs — see
|
|
7346
7413
|
# ``suite_origin_error`` for why that is destructive rather than
|
|
7347
7414
|
# merely useless.
|
|
7348
|
-
targets = [r for r in
|
|
7349
|
-
skipped = len(
|
|
7415
|
+
targets = [r for r in scoped if not is_suite_row(r)]
|
|
7416
|
+
skipped = len(scoped) - len(targets)
|
|
7350
7417
|
if skipped:
|
|
7351
7418
|
console.print(
|
|
7352
7419
|
f"[dim]Skipping {skipped} test(s) tracked from your Playwright "
|
|
@@ -7354,6 +7421,12 @@ def run_test_run(
|
|
|
7354
7421
|
"`verifaied collect`.[/dim]"
|
|
7355
7422
|
)
|
|
7356
7423
|
if not targets:
|
|
7424
|
+
if folder is not None:
|
|
7425
|
+
# A named folder with nothing in it is a typo far more often
|
|
7426
|
+
# than it is an empty folder, so it is a refusal rather than
|
|
7427
|
+
# the clean zero ``--all`` answers with — and it names the
|
|
7428
|
+
# folders that do exist, which is the fix.
|
|
7429
|
+
raise RecordedTestError(no_folder_match(folder, rows))
|
|
7357
7430
|
console.print(
|
|
7358
7431
|
"[dim]No recorded tests to run yet — record one with "
|
|
7359
7432
|
'`verifaied test record "<Feature path>/<Name>" --url <url>`. '
|
|
@@ -1160,12 +1160,13 @@ def test_wrapped_session_says_where_the_results_land(
|
|
|
1160
1160
|
err_console=MagicMock(),
|
|
1161
1161
|
)
|
|
1162
1162
|
printed = " ".join(str(c) for c in console.print.call_args_list)
|
|
1163
|
-
assert "
|
|
1164
|
-
assert "Browser
|
|
1163
|
+
assert "suite browser coverage" in printed
|
|
1164
|
+
assert "Browser coverage page as the suite's contribution" in printed
|
|
1165
1165
|
|
|
1166
1166
|
|
|
1167
1167
|
def test_summary_reports_the_session_s_own_dimension():
|
|
1168
|
-
"""A hand-driven pass must not be told the
|
|
1168
|
+
"""A hand-driven pass must not be told the branch's browser numbers — the
|
|
1169
|
+
two move
|
|
1169
1170
|
independently, and reporting the wrong one reads as "your clicking did
|
|
1170
1171
|
nothing"."""
|
|
1171
1172
|
result = _result(
|
|
@@ -2523,11 +2523,131 @@ def test_run_refuses_a_draft_and_names_the_record_command(tmp_path: Path):
|
|
|
2523
2523
|
_run(tmp_path, rows=[_test_row(status="draft")], codes=[0])
|
|
2524
2524
|
|
|
2525
2525
|
|
|
2526
|
-
def
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2526
|
+
def test_run_needs_exactly_one_of_a_ref_all_or_folder(tmp_path: Path):
|
|
2527
|
+
"""Three ways to say what to run, and saying two of them is as much a
|
|
2528
|
+
usage error as saying none."""
|
|
2529
|
+
for overrides in (
|
|
2530
|
+
{"ref": None, "run_all": False},
|
|
2531
|
+
{"ref": "Settings/API tokens", "run_all": True},
|
|
2532
|
+
{"ref": "Settings/API tokens", "folder": "Settings"},
|
|
2533
|
+
{"ref": None, "run_all": True, "folder": "Settings"},
|
|
2534
|
+
):
|
|
2535
|
+
with pytest.raises(RecordedTestError, match="--folder") as excinfo:
|
|
2536
|
+
_run(tmp_path, rows=[], codes=[], **overrides)
|
|
2537
|
+
assert "--all" in str(excinfo.value)
|
|
2538
|
+
|
|
2539
|
+
|
|
2540
|
+
def test_run_folder_replays_everything_nested_under_it(tmp_path: Path):
|
|
2541
|
+
"""A folder is a place: "run Settings" including Settings/Tokens is
|
|
2542
|
+
what anybody filing a test there meant."""
|
|
2543
|
+
top = _test_row(
|
|
2544
|
+
feature="Settings", name="Profile", feature_slug="settings", name_slug="profile"
|
|
2545
|
+
)
|
|
2546
|
+
nested = _test_row(
|
|
2547
|
+
feature="Settings/Tokens",
|
|
2548
|
+
name="Revoke",
|
|
2549
|
+
feature_slug="settings/tokens",
|
|
2550
|
+
name_slug="revoke",
|
|
2551
|
+
)
|
|
2552
|
+
elsewhere = _test_row(
|
|
2553
|
+
feature="Settings-api",
|
|
2554
|
+
name="Ping",
|
|
2555
|
+
feature_slug="settings-api",
|
|
2556
|
+
name_slug="ping",
|
|
2557
|
+
)
|
|
2558
|
+
rootless = _test_row(feature="", name="Log in", feature_slug="", name_slug="log-in")
|
|
2559
|
+
|
|
2560
|
+
code, replayed = _run(
|
|
2561
|
+
tmp_path,
|
|
2562
|
+
rows=[top, nested, elsewhere, rootless],
|
|
2563
|
+
codes=[0, 0],
|
|
2564
|
+
ref=None,
|
|
2565
|
+
folder="Settings",
|
|
2566
|
+
)
|
|
2567
|
+
|
|
2568
|
+
assert code == 0
|
|
2569
|
+
assert [c.kwargs["test"] for c in replayed.call_args_list] == [top, nested]
|
|
2570
|
+
|
|
2571
|
+
|
|
2572
|
+
def test_run_folder_matches_on_the_slug(tmp_path: Path):
|
|
2573
|
+
"""The same rule `find_test` uses for a name, so the flag agrees with
|
|
2574
|
+
the path the web composes."""
|
|
2575
|
+
row = _test_row(
|
|
2576
|
+
feature="API tokens",
|
|
2577
|
+
name="Revoke",
|
|
2578
|
+
feature_slug="api-tokens",
|
|
2579
|
+
name_slug="revoke",
|
|
2580
|
+
)
|
|
2581
|
+
|
|
2582
|
+
code, replayed = _run(
|
|
2583
|
+
tmp_path, rows=[row], codes=[0], ref=None, folder="api-tokens"
|
|
2584
|
+
)
|
|
2585
|
+
|
|
2586
|
+
assert code == 0
|
|
2587
|
+
assert replayed.call_args.kwargs["test"] is row
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
def test_run_folder_skips_drafts_and_suite_rows(tmp_path: Path):
|
|
2591
|
+
console = _console()
|
|
2592
|
+
ready = _test_row(
|
|
2593
|
+
feature="Settings", name="Profile", feature_slug="settings", name_slug="profile"
|
|
2594
|
+
)
|
|
2595
|
+
draft = _test_row(
|
|
2596
|
+
feature="Settings",
|
|
2597
|
+
name="Later",
|
|
2598
|
+
feature_slug="settings",
|
|
2599
|
+
name_slug="later",
|
|
2600
|
+
status="draft",
|
|
2601
|
+
)
|
|
2602
|
+
suite = _test_row(
|
|
2603
|
+
feature="Settings",
|
|
2604
|
+
name="Their own",
|
|
2605
|
+
feature_slug="settings",
|
|
2606
|
+
name_slug="their-own",
|
|
2607
|
+
origin="suite",
|
|
2608
|
+
)
|
|
2609
|
+
|
|
2610
|
+
code, replayed = _run(
|
|
2611
|
+
tmp_path,
|
|
2612
|
+
rows=[ready, draft, suite],
|
|
2613
|
+
codes=[0],
|
|
2614
|
+
ref=None,
|
|
2615
|
+
folder="Settings",
|
|
2616
|
+
console=console,
|
|
2617
|
+
)
|
|
2618
|
+
|
|
2619
|
+
assert code == 0
|
|
2620
|
+
assert [c.kwargs["test"] for c in replayed.call_args_list] == [ready]
|
|
2621
|
+
printed = " ".join(str(c.args[0]) for c in console.print.call_args_list)
|
|
2622
|
+
assert "Skipping 1 test(s) tracked from your Playwright suite" in printed
|
|
2623
|
+
|
|
2624
|
+
|
|
2625
|
+
def test_run_folder_with_no_match_refuses_and_names_the_folders(tmp_path: Path):
|
|
2626
|
+
"""A typo is not a clean zero, unlike --all: the folders that do exist
|
|
2627
|
+
are the fix, so they are the message."""
|
|
2628
|
+
rows = [
|
|
2629
|
+
_test_row(
|
|
2630
|
+
feature="Settings/Tokens",
|
|
2631
|
+
name="Revoke",
|
|
2632
|
+
feature_slug="settings/tokens",
|
|
2633
|
+
name_slug="revoke",
|
|
2634
|
+
),
|
|
2635
|
+
_test_row(feature="", name="Log in", feature_slug="", name_slug="log-in"),
|
|
2636
|
+
]
|
|
2637
|
+
|
|
2638
|
+
with pytest.raises(RecordedTestError) as excinfo:
|
|
2639
|
+
_run(tmp_path, rows=rows, codes=[], ref=None, folder="Setings")
|
|
2640
|
+
|
|
2641
|
+
assert str(excinfo.value) == (
|
|
2642
|
+
'no recorded tests under "Setings" — the folders here are: Settings/Tokens'
|
|
2643
|
+
)
|
|
2644
|
+
|
|
2645
|
+
|
|
2646
|
+
def test_run_folder_refuses_a_blank_path_before_it_slugs_it(tmp_path: Path):
|
|
2647
|
+
"""`slugify_feature_path("")` is `""`, which matches the rootless
|
|
2648
|
+
tests and nothing else — an answer to a question nobody asked."""
|
|
2649
|
+
with pytest.raises(RecordedTestError, match="needs a folder path"):
|
|
2650
|
+
_run(tmp_path, rows=[_test_row()], codes=[], ref=None, folder=" ")
|
|
2531
2651
|
|
|
2532
2652
|
|
|
2533
2653
|
def test_run_all_skips_drafts_and_reports_the_worst_code(tmp_path: Path):
|
|
@@ -2778,6 +2898,28 @@ def test_test_run_command_forwards_all(monkeypatch):
|
|
|
2778
2898
|
assert run.call_args.kwargs["branch"] == "feature-a"
|
|
2779
2899
|
|
|
2780
2900
|
|
|
2901
|
+
def test_test_run_command_forwards_the_folder(monkeypatch):
|
|
2902
|
+
monkeypatch.setenv(ENV_API_TOKEN, "vr_live_tok")
|
|
2903
|
+
with patch("verifaied.cli.run_test_run", return_value=0) as run:
|
|
2904
|
+
result = runner.invoke(
|
|
2905
|
+
app,
|
|
2906
|
+
[
|
|
2907
|
+
"test",
|
|
2908
|
+
"run",
|
|
2909
|
+
"--folder",
|
|
2910
|
+
"Settings/Tokens",
|
|
2911
|
+
"--repo",
|
|
2912
|
+
str(uuid4()),
|
|
2913
|
+
"--api-url",
|
|
2914
|
+
"http://api.test",
|
|
2915
|
+
],
|
|
2916
|
+
)
|
|
2917
|
+
assert result.exit_code == 0
|
|
2918
|
+
assert run.call_args.kwargs["folder"] == "Settings/Tokens"
|
|
2919
|
+
assert run.call_args.kwargs["run_all"] is False
|
|
2920
|
+
assert run.call_args.kwargs["ref"] is None
|
|
2921
|
+
|
|
2922
|
+
|
|
2781
2923
|
def test_test_run_command_propagates_the_worst_code(monkeypatch):
|
|
2782
2924
|
monkeypatch.setenv(ENV_API_TOKEN, "vr_live_tok")
|
|
2783
2925
|
with patch("verifaied.cli.run_test_run", return_value=3):
|
|
@@ -8580,7 +8722,7 @@ def test_studio_declares_every_kind_on_every_beat(tmp_path: Path):
|
|
|
8580
8722
|
"""The service claims only what a studio says it can do, so this is
|
|
8581
8723
|
the whole compatibility contract in one field."""
|
|
8582
8724
|
studio = _studio(tmp_path, acks=[_ack()])
|
|
8583
|
-
assert STUDIO_KINDS == ("record", "run", "edit", "amend")
|
|
8725
|
+
assert STUDIO_KINDS == ("record", "run", "run_headless", "edit", "amend")
|
|
8584
8726
|
working = [beat for beat in studio.beats if not beat.get("closing")]
|
|
8585
8727
|
assert working
|
|
8586
8728
|
assert all(beat["kinds"] == list(STUDIO_KINDS) for beat in working)
|
|
@@ -8608,6 +8750,25 @@ def test_studio_replays_a_run_request_headed_and_records_nothing(
|
|
|
8608
8750
|
assert [s["status"] for s in studio.statuses] == ["recording", "done"]
|
|
8609
8751
|
|
|
8610
8752
|
|
|
8753
|
+
def test_studio_replays_a_headless_run_request_with_no_window(tmp_path: Path):
|
|
8754
|
+
"""A batch is nobody watching, and forty windows opening in front of
|
|
8755
|
+
somebody is the reason they would never click it twice."""
|
|
8756
|
+
row = _test_row()
|
|
8757
|
+
studio = _studio(
|
|
8758
|
+
tmp_path,
|
|
8759
|
+
acks=[_ack(_studio_request(row["id"], kind="run_headless"))],
|
|
8760
|
+
rows=[row],
|
|
8761
|
+
)
|
|
8762
|
+
|
|
8763
|
+
assert studio.records == []
|
|
8764
|
+
assert len(studio.replays) == 1
|
|
8765
|
+
call = studio.replays[0]
|
|
8766
|
+
assert call["headed"] is False
|
|
8767
|
+
assert call["driver"] == "suite"
|
|
8768
|
+
assert call["test"] is row
|
|
8769
|
+
assert [s["status"] for s in studio.statuses] == ["recording", "done"]
|
|
8770
|
+
|
|
8771
|
+
|
|
8611
8772
|
def test_studio_reports_done_for_a_run_that_failed_its_assertions(
|
|
8612
8773
|
tmp_path: Path,
|
|
8613
8774
|
):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|