mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-15 17:25:26 -04:00
400 lines
12 KiB
Python
400 lines
12 KiB
Python
"""Direct tests for the focused test-selection runner (tests/run_focus.py).
|
|
|
|
Command construction is tested separately from process execution: the pure
|
|
builder functions are asserted directly, and ``run`` is exercised with an
|
|
injected fake executor so no pytest subprocess is ever spawned.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tests.run_focus import (
|
|
FocusSelection,
|
|
build_marker_expression,
|
|
build_pytest_command,
|
|
discover_sub_areas,
|
|
normalize_sub_area,
|
|
run,
|
|
)
|
|
|
|
PY = "PY" # placeholder interpreter for deterministic command assertions
|
|
|
|
|
|
def _cmd(**kwargs) -> list[str]:
|
|
"""Build a pytest command for a FocusSelection made from kwargs."""
|
|
return build_pytest_command(FocusSelection(**kwargs), python=PY)
|
|
|
|
|
|
# --- marker expression building -------------------------------------------
|
|
|
|
|
|
def test_area_only_marker_expression():
|
|
assert build_marker_expression("security", None) == "area_security"
|
|
|
|
|
|
def test_sub_area_only_marker_expression():
|
|
assert build_marker_expression(None, "cookbook") == "sub_cookbook"
|
|
|
|
|
|
def test_area_and_sub_area_marker_expression():
|
|
assert build_marker_expression("services", "cookbook") == "area_services and sub_cookbook"
|
|
|
|
|
|
def test_no_selection_marker_expression_is_none():
|
|
assert build_marker_expression(None, None) is None
|
|
|
|
|
|
def test_fast_only_marker_expression():
|
|
assert build_marker_expression(None, None, fast=True) == "not slow"
|
|
|
|
|
|
def test_fast_composes_with_area():
|
|
assert build_marker_expression("services", None, fast=True) == "area_services and not slow"
|
|
|
|
|
|
def test_fast_composes_with_area_and_sub_area():
|
|
assert (
|
|
build_marker_expression("services", "cookbook", fast=True)
|
|
== "area_services and sub_cookbook and not slow"
|
|
)
|
|
|
|
|
|
# --- command construction --------------------------------------------------
|
|
|
|
|
|
def test_area_only_command():
|
|
assert _cmd(area="security") == [PY, "-m", "pytest", "-m", "area_security"]
|
|
|
|
|
|
def test_sub_area_only_command():
|
|
assert _cmd(sub_area="cookbook") == [PY, "-m", "pytest", "-m", "sub_cookbook"]
|
|
|
|
|
|
def test_area_and_sub_area_command():
|
|
assert _cmd(area="services", sub_area="cookbook") == [
|
|
PY, "-m", "pytest", "-m", "area_services and sub_cookbook",
|
|
]
|
|
|
|
|
|
def test_keyword_only_command():
|
|
assert _cmd(keyword="taxonomy") == [PY, "-m", "pytest", "-k", "taxonomy"]
|
|
|
|
|
|
def test_area_and_keyword_command():
|
|
assert _cmd(area="services", keyword="cookbook") == [
|
|
PY, "-m", "pytest", "-m", "area_services", "-k", "cookbook",
|
|
]
|
|
|
|
|
|
def test_passthrough_pytest_args_appended_last():
|
|
command = _cmd(area="services", pytest_args=("--maxfail=1", "-q"))
|
|
assert command == [PY, "-m", "pytest", "-m", "area_services", "--maxfail=1", "-q"]
|
|
|
|
|
|
def test_last_failed_appends_safe_flags():
|
|
assert _cmd(last_failed=True) == [
|
|
PY,
|
|
"-m",
|
|
"pytest",
|
|
"--last-failed",
|
|
"--last-failed-no-failures=none",
|
|
]
|
|
|
|
|
|
def test_default_python_is_current_interpreter():
|
|
command = build_pytest_command(FocusSelection(area="cli"))
|
|
assert command[0] == sys.executable
|
|
|
|
|
|
# --- fast lane and duration visibility -------------------------------------
|
|
|
|
|
|
def test_fast_only_command():
|
|
assert _cmd(fast=True) == [PY, "-m", "pytest", "-m", "not slow"]
|
|
|
|
|
|
def test_fast_with_area_command():
|
|
assert _cmd(area="services", fast=True) == [
|
|
PY, "-m", "pytest", "-m", "area_services and not slow",
|
|
]
|
|
|
|
|
|
def test_fast_with_area_and_sub_area_command():
|
|
assert _cmd(area="services", sub_area="cookbook", fast=True) == [
|
|
PY, "-m", "pytest", "-m", "area_services and sub_cookbook and not slow",
|
|
]
|
|
|
|
|
|
def test_durations_appends_flag():
|
|
assert _cmd(fast=True, durations=25) == [
|
|
PY, "-m", "pytest", "-m", "not slow", "--durations=25",
|
|
]
|
|
|
|
|
|
def test_durations_min_appends_flag():
|
|
assert _cmd(fast=True, durations=25, durations_min=0.05) == [
|
|
PY, "-m", "pytest", "-m", "not slow", "--durations=25", "--durations-min=0.05",
|
|
]
|
|
|
|
|
|
def test_durations_is_not_a_focus_selector():
|
|
assert FocusSelection(durations=25).has_focus is False
|
|
assert FocusSelection(fast=True).has_focus is True
|
|
|
|
|
|
def test_durations_kept_before_passthrough_args():
|
|
command = _cmd(fast=True, durations=25, pytest_args=("-q",))
|
|
assert command == [PY, "-m", "pytest", "-m", "not slow", "--durations=25", "-q"]
|
|
|
|
|
|
# --- sub-area normalization ------------------------------------------------
|
|
|
|
|
|
def test_normalize_sub_area_lowercases_and_collapses():
|
|
assert normalize_sub_area("Cook Book") == "cook_book"
|
|
|
|
|
|
def test_normalize_sub_area_strips_separators():
|
|
assert normalize_sub_area("--owner.scope--") == "owner_scope"
|
|
|
|
|
|
def test_normalize_sub_area_removes_marker_prefix():
|
|
assert normalize_sub_area("sub_cookbook") == "cookbook"
|
|
|
|
|
|
def test_normalize_sub_area_rejects_empty_after_normalization():
|
|
with pytest.raises(argparse.ArgumentTypeError):
|
|
normalize_sub_area("!!!")
|
|
|
|
|
|
def test_discover_sub_areas_from_test_filename(tmp_path):
|
|
(tmp_path / "test_cookbook_helpers.py").write_text("", encoding="utf-8")
|
|
|
|
assert discover_sub_areas(tmp_path) == frozenset({"cookbook"})
|
|
|
|
|
|
# --- run(): dry-run, execution, validation ---------------------------------
|
|
|
|
|
|
class _FakeExecutor:
|
|
"""Records the command it was asked to run and returns a fixed code."""
|
|
|
|
def __init__(self, returncode: int = 0):
|
|
self.returncode = returncode
|
|
self.calls: list[list[str]] = []
|
|
|
|
def __call__(self, command: list[str]) -> int:
|
|
self.calls.append(command)
|
|
return self.returncode
|
|
|
|
|
|
def test_dry_run_prints_command_and_does_not_execute(capsys):
|
|
executor = _FakeExecutor()
|
|
code = run(
|
|
["--dry-run", "--area", "services", "--sub-area", "cookbook"],
|
|
executor=executor,
|
|
)
|
|
out = capsys.readouterr().out
|
|
assert code == 0
|
|
assert executor.calls == []
|
|
assert out == (
|
|
f"{sys.executable} -m pytest "
|
|
"-m 'area_services and sub_cookbook'\n"
|
|
)
|
|
|
|
|
|
def test_dry_run_last_failed_prints_safe_flags(capsys):
|
|
executor = _FakeExecutor()
|
|
code = run(["--dry-run", "--last-failed"], executor=executor)
|
|
out = capsys.readouterr().out
|
|
assert code == 0
|
|
assert executor.calls == []
|
|
assert out == (
|
|
f"{sys.executable} -m pytest "
|
|
"--last-failed --last-failed-no-failures=none\n"
|
|
)
|
|
|
|
|
|
def test_run_invokes_executor_with_built_command():
|
|
executor = _FakeExecutor(returncode=3)
|
|
code = run(["--keyword", "taxonomy", "--", "--maxfail=1"], executor=executor)
|
|
assert code == 3
|
|
assert executor.calls == [[sys.executable, "-m", "pytest", "-k", "taxonomy", "--maxfail=1"]]
|
|
|
|
|
|
def test_run_last_failed_only():
|
|
executor = _FakeExecutor()
|
|
run(["--last-failed"], executor=executor)
|
|
assert executor.calls == [[
|
|
sys.executable,
|
|
"-m",
|
|
"pytest",
|
|
"--last-failed",
|
|
"--last-failed-no-failures=none",
|
|
]]
|
|
|
|
|
|
@pytest.mark.parametrize("value", ["cookbook", "sub_cookbook"])
|
|
def test_run_accepts_both_sub_area_forms(value):
|
|
executor = _FakeExecutor()
|
|
run(["--sub-area", value], executor=executor)
|
|
assert executor.calls == [[
|
|
sys.executable,
|
|
"-m",
|
|
"pytest",
|
|
"-m",
|
|
"sub_cookbook",
|
|
]]
|
|
|
|
|
|
def test_invalid_area_exits_with_error():
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
run(["--area", "bogus"], executor=_FakeExecutor())
|
|
assert excinfo.value.code == 2
|
|
|
|
|
|
def test_invalid_sub_area_exits_with_error(capsys):
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
run(
|
|
["--sub-area", "definitely_not_a_real_sub_area"],
|
|
executor=_FakeExecutor(),
|
|
)
|
|
assert excinfo.value.code == 2
|
|
assert "unknown sub-area" in capsys.readouterr().err
|
|
|
|
|
|
def test_no_focus_selector_is_rejected():
|
|
executor = _FakeExecutor()
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
run(["--", "-q"], executor=executor)
|
|
assert excinfo.value.code == 2
|
|
assert executor.calls == []
|
|
|
|
|
|
def test_fast_run_invokes_executor_with_not_slow():
|
|
executor = _FakeExecutor()
|
|
run(["--fast"], executor=executor)
|
|
assert executor.calls == [[sys.executable, "-m", "pytest", "-m", "not slow"]]
|
|
|
|
|
|
def test_fast_with_durations_run_invokes_executor():
|
|
executor = _FakeExecutor()
|
|
run(["--area", "services", "--fast", "--durations", "25"], executor=executor)
|
|
assert executor.calls == [[
|
|
sys.executable,
|
|
"-m",
|
|
"pytest",
|
|
"-m",
|
|
"area_services and not slow",
|
|
"--durations=25",
|
|
]]
|
|
|
|
|
|
def test_fast_durations_dry_run_prints_command(capsys):
|
|
executor = _FakeExecutor()
|
|
code = run(["--dry-run", "--fast", "--durations", "25"], executor=executor)
|
|
out = capsys.readouterr().out
|
|
assert code == 0
|
|
assert executor.calls == []
|
|
assert out == f"{sys.executable} -m pytest -m 'not slow' --durations=25\n"
|
|
|
|
|
|
def test_durations_alone_is_rejected_before_executor():
|
|
executor = _FakeExecutor()
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
run(["--durations", "25"], executor=executor)
|
|
assert excinfo.value.code == 2
|
|
assert executor.calls == []
|
|
|
|
|
|
def test_durations_zero_is_allowed_means_show_all():
|
|
executor = _FakeExecutor()
|
|
run(["--fast", "--durations", "0"], executor=executor)
|
|
assert executor.calls == [[
|
|
sys.executable, "-m", "pytest", "-m", "not slow", "--durations=0",
|
|
]]
|
|
|
|
|
|
@pytest.mark.parametrize("flag,value", [("--durations", "-1"), ("--durations-min", "-0.5")])
|
|
def test_negative_duration_values_are_rejected(flag, value):
|
|
executor = _FakeExecutor()
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
run(["--fast", flag, value], executor=executor)
|
|
assert excinfo.value.code == 2
|
|
assert executor.calls == []
|
|
|
|
|
|
@pytest.mark.parametrize("argv", [
|
|
["--fast", "--durations-min", "0.05"],
|
|
["--area", "services", "--durations-min", "0.05"],
|
|
])
|
|
def test_durations_min_without_durations_is_rejected(argv):
|
|
executor = _FakeExecutor()
|
|
with pytest.raises(SystemExit) as excinfo:
|
|
run(argv, executor=executor)
|
|
assert excinfo.value.code == 2
|
|
assert executor.calls == []
|
|
|
|
|
|
def test_durations_min_with_durations_is_allowed():
|
|
executor = _FakeExecutor()
|
|
run(["--fast", "--durations", "25", "--durations-min", "0.05"], executor=executor)
|
|
assert executor.calls == [[
|
|
sys.executable,
|
|
"-m",
|
|
"pytest",
|
|
"-m",
|
|
"not slow",
|
|
"--durations=25",
|
|
"--durations-min=0.05",
|
|
]]
|
|
|
|
|
|
# --- fast lane deselects evidence-backed slow tests (real collection) -------
|
|
|
|
# Node names in tests/test_auth_config_lock_concurrency.py: the single unmarked
|
|
# fast test, and the five @pytest.mark.slow tests the fast lane must exclude.
|
|
_FAST_AUTH_CONCURRENCY_TEST = "test_parallel_creates_same_username_only_one_wins"
|
|
_SLOW_AUTH_CONCURRENCY_TESTS = (
|
|
"test_parallel_creates_no_lost_users",
|
|
"test_parallel_deletes_no_corruption",
|
|
"test_parallel_renames_no_lost_users",
|
|
"test_mixed_operations_no_corruption",
|
|
"test_file_always_valid_json_during_concurrent_ops",
|
|
)
|
|
|
|
|
|
def test_fast_lane_collects_only_unmarked_auth_concurrency_test():
|
|
"""`--fast` collection drops the marked slow tests but keeps the fast one.
|
|
|
|
Unlike the other tests here, this runs a real `--collect-only` so it proves
|
|
the `slow` markers actually deselect during collection, not just that the
|
|
command is built with `not slow`.
|
|
"""
|
|
repo_root = Path(__file__).resolve().parents[1]
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"tests/run_focus.py",
|
|
"--fast",
|
|
"--",
|
|
"--collect-only",
|
|
"-q",
|
|
"tests/test_auth_config_lock_concurrency.py",
|
|
],
|
|
cwd=repo_root,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
assert result.returncode == 0, result.stderr or result.stdout
|
|
collected = result.stdout
|
|
|
|
assert _FAST_AUTH_CONCURRENCY_TEST in collected
|
|
for slow_test in _SLOW_AUTH_CONCURRENCY_TESTS:
|
|
assert slow_test not in collected, f"slow test was not deselected: {slow_test}"
|