invisible_playwright/src/invisible_playwright/cli.py
feder-cr 5dac302938 test: activate the full e2e (browser-driving) suite + add fetch --force
The 138 @pytest.mark.e2e tests were doubly inactive: deselected by addopts AND
skipped without a cached binary — and 3 of the 6 per-file firefox_binary
fixtures silently ignored INVPW_BINARY_PATH, so they'd test whatever was cached
even when you pointed the suite elsewhere (a false-confidence trap).

- Centralize firefox_binary into conftest.py (env INVPW_BINARY_PATH → cache →
  skip); delete the 6 duplicates. Unify test_webrtc_realness onto the same env.
- scripts/run_e2e.py: one command that runs ALL e2e against a chosen binary,
  with reruns so an under-load interaction flake (dblclick/hover pass 3/3 in
  isolation) self-heals while a real break fails every attempt. The webrtc e2e
  fake a TCP-only SOCKS locally, so the suite is offline. This is the MANDATORY
  pre-release browser gate (local — hosted runners are too interaction-flaky).
- Running the suite against firefox-9 surfaced a real gap: `invisible_playwright
  fetch --force` was unrecognized (the subparser took no args) though the e2e
  test + docstring expect it. Implement it: drop the cached version dir, refetch.
- Add pytest-rerunfailures + playwright to the dev extras.

Baseline against firefox-9: 136 passed, 1 skipped (linux_only on win host),
1 was the --force gap now fixed.
2026-06-09 15:40:02 +02:00

92 lines
3.2 KiB
Python

"""Command-line interface for invisible_playwright."""
from __future__ import annotations
import argparse
import shutil
import sys
from . import __version__
from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION
from .download import cache_root, ensure_binary
def _cmd_fetch(args: argparse.Namespace) -> int:
# --force: re-download even if already cached (drop the cached version dir,
# then let ensure_binary fetch it fresh). Useful to recover a corrupted cache
# or re-pull after a re-published release.
if getattr(args, "force", False):
from .download import cache_dir_for_version
d = cache_dir_for_version()
if d.exists():
shutil.rmtree(d, ignore_errors=True)
path = ensure_binary()
print(path)
return 0
def _cmd_path(_args: argparse.Namespace) -> int:
try:
path = ensure_binary()
except Exception as e:
print(f"error: {e}", file=sys.stderr)
return 1
print(path)
return 0
def _cmd_version(_args: argparse.Namespace) -> int:
print(f"invisible_playwright {__version__}")
print(f"BINARY_VERSION={BINARY_VERSION} (Firefox {FIREFOX_UPSTREAM_VERSION})")
return 0
def _cmd_clear_cache(_args: argparse.Namespace) -> int:
root = cache_root()
if root.exists():
shutil.rmtree(root)
print(f"removed: {root}")
else:
print(f"nothing to remove: {root}")
return 0
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(prog="invisible-playwright", description="invisible_playwright CLI")
# Top-level `--version` / `-V` flag so `python -m invisible_playwright --version`
# works (Python convention), in addition to the existing `version` subcommand.
p.add_argument(
"-V", "--version", action="version",
version=f"invisible_playwright {__version__} (BINARY_VERSION={BINARY_VERSION}, Firefox {FIREFOX_UPSTREAM_VERSION})",
)
sub = p.add_subparsers(dest="cmd")
fetch_p = sub.add_parser("fetch", help="download the patched Firefox binary")
fetch_p.add_argument("--force", action="store_true",
help="re-download even if already cached")
sub.add_parser("path", help="print the absolute path to the cached binary")
sub.add_parser("version", help="print wrapper and binary versions")
sub.add_parser("clear-cache", help="remove all cached binaries")
return p
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
if args.cmd is None:
# argparse-conventional: print usage + error message to stderr, exit 2.
# We can't keep `required=True` on the subparsers because that breaks
# the top-level `--version` flag (argparse demands a subcommand even
# when --version is the only token). parser.error() preserves the
# original "no subcommand" exit semantics tests expect.
parser.error("a subcommand is required (try --help, --version, or one of: fetch, path, version, clear-cache)")
dispatch = {
"fetch": _cmd_fetch,
"path": _cmd_path,
"version": _cmd_version,
"clear-cache": _cmd_clear_cache,
}
return dispatch[args.cmd](args)
if __name__ == "__main__":
sys.exit(main())