fix(tests): parallel runner reports a known flag's missing value as usage, not per file

The bare-flag check asked pytest's parser which tokens it does not know,
but wrapped parse_known_args in the same blanket except that guards
parser construction. A known flag with a missing value (`--tb` alone)
raises pytest.UsageError there, which the except turned into "nothing
unknown", so discovery ran and every per-file pytest died with
"argument --tb: expected one argument".

Keep the fallback around building the parser only; let parse_known_args
run outside it and surface UsageError (and the unknown-token list) as
this runner's own usage error before discovery. One invariant test.
This commit is contained in:
teknium1
2026-09-18 03:32:47 -07:00
committed by Teknium
parent d7bedcee1e
commit 49397cf2b4
2 changed files with 34 additions and 12 deletions

View File

@@ -56,7 +56,7 @@ import threading
import time
from concurrent.futures import ThreadPoolExecutor, Future
from pathlib import Path
from typing import Dict, List, Tuple
from typing import Dict, List, Optional, Tuple
# Default test discovery roots.
@@ -824,8 +824,8 @@ def _make_stdio_glyph_safe() -> None:
pass
def _unknown_pytest_flags(tokens: List[str]) -> List[str]:
"""Return the bare passthrough tokens pytest itself would reject.
def _pytest_flag_error(tokens: List[str]) -> Optional[str]:
"""Return pytest's own complaint about the bare passthrough tokens, if any.
A mistyped flag (``--jbs``) that is not one of OUR options used to be
forwarded to every per-file pytest, so the run discovered the whole suite
@@ -833,19 +833,27 @@ def _unknown_pytest_flags(tokens: List[str]) -> List[str]:
learn about a typo. Ask pytest's own argparse parser (with the installed
plugins loaded, so ``-n``/``--timeout`` count) which tokens it does not
know; argparse handles the attached-value (``-rA``), combined-flag
(``-xvs``) and ``-k expr`` forms for us. If the parser cannot be built
the check is skipped and tokens are forwarded as before.
(``-xvs``) and ``-k expr`` forms for us. A known flag with a bad or
missing value (``--tb`` alone) makes that parser raise ``UsageError``;
it is reported the same way instead of once per discovered file. Only
if the parser cannot be built is the check skipped and tokens forwarded
as before.
"""
try:
from _pytest.config import get_config
from _pytest.config import UsageError, get_config
config = get_config()
config.pluginmanager.load_setuptools_entrypoints("pytest11")
parser = config._parser.optparser
_, unknown = parser.parse_known_args(tokens)
except Exception:
return []
return [tok for tok in unknown if tok.startswith("-")]
return None
try:
_, unknown = parser.parse_known_args(tokens)
except UsageError as exc:
# "usage: ...\n<prog>: error: argument --tb: expected one argument"
return str(exc).rsplit("error: ", 1)[-1].strip()
unknown = [tok for tok in unknown if tok.startswith("-")]
return f"unrecognized arguments: {' '.join(unknown)}" if unknown else None
def main() -> int:
@@ -1008,9 +1016,9 @@ def main() -> int:
# here with usage instead of once per discovered file. Anything after a
# literal ``--`` is the caller's explicit choice and is forwarded as-is.
if bare_passthrough:
unknown = _unknown_pytest_flags(bare_passthrough)
if unknown:
parser.error(f"unrecognized arguments: {' '.join(unknown)}")
flag_error = _pytest_flag_error(bare_passthrough)
if flag_error:
parser.error(flag_error)
# ── Node-id selectors → file + ``-k`` filter ────────────────────────────
# This runner is FILE-granular: it spawns one ``pytest <file>`` per test

View File

@@ -312,6 +312,20 @@ def test_unknown_bare_flag_errors_with_usage_instead_of_sweeping(tmp_path: Path)
assert "2✓" in proc.stdout or "2 passed" in proc.stdout, proc.stdout
def test_known_flag_missing_value_errors_with_usage_instead_of_sweeping(
tmp_path: Path,
) -> None:
"""``--tb`` with no value is a pytest UsageError, not a per-file sweep.
The flag itself is known, so an unknown-token check alone lets it through;
pytest's own parser must be allowed to reject it up front.
"""
probe_dir = _make_probe_dir(tmp_path)
proc = _run_runner(probe_dir, "--tb")
assert proc.returncode == 2, proc.stdout
assert "usage:" in proc.stdout and "--tb: expected one argument" in proc.stdout
assert "Discovered" not in proc.stdout
def test_bare_value_flag_keeps_its_value(tmp_path: Path) -> None: