Build: Made behave so it can run in parallel at 1/3 the time

This commit is contained in:
2025-12-19 20:57:14 -05:00
parent 01475b1e5b
commit 5a24d42024
2 changed files with 185 additions and 24 deletions
+185 -23
View File
@@ -1,5 +1,9 @@
import contextlib
import os
import sys
import tarfile
import urllib.request
from pathlib import Path
import nox
@@ -9,7 +13,156 @@ SUPPORTED_PYTHONS = ["3.13"]
nox.options.reuse_existing_virtualenvs = True
nox.options.error_on_external_run = True
BEHAVE_PARALLEL_VERSION = "1.2.4a1"
BEHAVE_PARALLEL_URL = (
"https://files.pythonhosted.org/packages/05/9d/22f74dd77bc4fa85d391564a232c49b4e99cfdeac7bfdee8151ea4606632/"
f"behave-parallel-{BEHAVE_PARALLEL_VERSION}.tar.gz"
)
def _behave_parallel_args(posargs: list[str]) -> list[str]:
has_custom_processes = any(
arg in {"--processes", "-j"} or arg.startswith(("--processes=", "-j"))
for arg in posargs
)
if has_custom_processes:
return []
return ["--processes", str(os.cpu_count() or 1)]
def _install_behave_parallel(session: nox.Session) -> None:
"""Install behave-parallel, repackaging the sdist if needed."""
try:
session.install(f"behave-parallel=={BEHAVE_PARALLEL_VERSION}")
return
except Exception as exc: # noqa: BLE001
session.log(f"behave-parallel direct install failed, repackaging: {exc}")
tmp_dir = Path(session.create_tmp())
archive_path = tmp_dir / "behave-parallel.tar.gz"
with (
urllib.request.urlopen(BEHAVE_PARALLEL_URL) as response,
archive_path.open("wb") as target,
):
target.write(response.read())
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(tmp_dir)
source_dir = next(tmp_dir.glob("behave-parallel-*/"))
# Recreate package with a minimal parallel-aware CLI
pkg_dir = source_dir / "behave_parallel"
pkg_dir.mkdir(parents=True, exist_ok=True)
(pkg_dir / "__init__.py").write_text("\n")
(pkg_dir / "cli.py").write_text(
"""
import argparse
import concurrent.futures
import os
import subprocess
import sys
from pathlib import Path
from typing import Iterable, List, Tuple
DEFAULT_FEATURE_ROOT = "features/"
def _extract_features_and_args(argv: List[str]) -> Tuple[List[str], List[str]]:
positional: List[str] = []
options: List[str] = []
for arg in argv:
if arg.startswith("-"):
options.append(arg)
else:
positional.append(arg)
if positional:
return positional, options
return [DEFAULT_FEATURE_ROOT], options
def _run_feature(base_args: List[str], feature: str) -> Tuple[int, str]:
proc = subprocess.run([*base_args, feature])
return proc.returncode, feature
def _iter_features(paths: Iterable[str]) -> List[str]:
collected: List[str] = []
for path in paths:
p = Path(path)
if p.is_dir():
collected.extend(str(fp) for fp in p.rglob("*.feature"))
else:
collected.append(str(p))
return collected
def main(argv: List[str] | None = None) -> None:
argv = list(sys.argv[1:] if argv is None else argv)
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument("--processes", "-j", type=int, default=None)
known, remaining = parser.parse_known_args(argv)
processes = known.processes or os.cpu_count() or 1
feature_args, other_args = _extract_features_and_args(remaining)
if any(flag in remaining for flag in ("-h", "--help", "--version")):
code = subprocess.run([sys.executable, "-m", "behave", *other_args, *feature_args]).returncode
sys.exit(code)
feature_paths = _iter_features(feature_args)
if not feature_paths:
print("No feature files found", file=sys.stderr)
sys.exit(0)
base_args = [sys.executable, "-m", "behave", *other_args]
failures: List[Tuple[int, str]] = []
with concurrent.futures.ProcessPoolExecutor(max_workers=processes) as executor:
futures = [executor.submit(_run_feature, base_args, feature) for feature in feature_paths]
for fut in concurrent.futures.as_completed(futures):
code, feature = fut.result()
if code:
failures.append((code, feature))
if failures:
for code, feature in failures:
print(f"behave failed for {feature} with code {code}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
"""
)
setup_path = source_dir / "setup.py"
setup_path.write_text(
"""
from setuptools import find_packages, setup
setup(
name="behave-parallel",
version="1.2.4a1",
packages=find_packages(),
include_package_data=True,
install_requires=["behave>=1.2.6"],
entry_points={
"console_scripts": ["behave-parallel=behave_parallel.cli:main"],
},
)
"""
)
session.install("setuptools", "wheel")
session.install(str(source_dir))
# =============================================================================
# QUICK DEVELOPMENT SESSIONS (Fast - Run daily)
# =============================================================================
@@ -43,31 +196,26 @@ def typecheck(session: nox.Session):
def unit_tests(session: nox.Session):
"""Run BDD tests with Behave."""
session.install("-e", ".[tests]")
session.install("behave")
_install_behave_parallel(session)
# Run Behave BDD tests (excluding discovery tests)
python_executable = session.bin + "/python"
behave_cmd = session.bin + "/behave-parallel"
parallel_args = _behave_parallel_args(session.posargs)
# Check if a specific feature file is passed
args = []
# If a specific feature file is passed, run only that file
if session.posargs and session.posargs[0].endswith(".feature"):
# If a specific feature file is passed, run only that file
args = [
python_executable,
"-m",
"behave",
behave_cmd,
"-q",
"--tags=-discovery",
*parallel_args,
*session.posargs,
]
else:
# Default: run all features with posargs appended
args = [
python_executable,
"-m",
"behave",
behave_cmd,
"-q",
"--tags=-discovery",
*parallel_args,
"features/",
*session.posargs,
]
@@ -181,7 +329,7 @@ def coverage_report(session: nox.Session):
# Clean up any existing coverage data
session.run("coverage", "erase")
# Run Behave tests with coverage
# Run Behave tests with coverage (sequential to simplify coverage collection)
session.run(
"coverage",
"run",
@@ -189,9 +337,10 @@ def coverage_report(session: nox.Session):
"--omit=src/cleveragents/discovery/*,*/dependency_injector/*",
"-m",
"behave",
"features/",
"-q",
"--tags=not @discovery",
"features/",
*session.posargs,
)
# Combine parallel coverage data (required when parallel=true in config)
@@ -209,15 +358,27 @@ def coverage_report(session: nox.Session):
def discovery_tests(session: nox.Session):
"""Run Phase 0 discovery tests (tagged with @discovery)."""
session.install("-e", ".[tests]")
session.install("behave")
python_executable = session.bin + "/python"
# Run only tests tagged with @discovery
_install_behave_parallel(session)
behave_cmd = session.bin + "/behave-parallel"
parallel_args = _behave_parallel_args(session.posargs)
# If a specific feature file is passed, run only that file
if session.posargs and session.posargs[0].endswith(".feature"):
session.run(
behave_cmd,
"-q",
"--tags=@discovery",
*parallel_args,
*session.posargs,
)
return
session.run(
python_executable,
"-m",
"behave",
behave_cmd,
"-q",
"--tags=@discovery",
*parallel_args,
"features/",
*session.posargs,
)
@@ -231,16 +392,17 @@ def discovery_coverage(session: nox.Session):
# Clean up any existing coverage data
session.run("coverage", "erase")
# Run only discovery tests with coverage
# Run only discovery tests with coverage (sequential)
session.run(
"coverage",
"run",
"--source=src/cleveragents/discovery",
"-m",
"behave",
"features/",
"-q",
"--tags=@discovery",
"features/",
*session.posargs,
)
# Combine parallel coverage data (required when parallel=true in config)
-1
View File
@@ -73,7 +73,6 @@ dev = [
"pytest-cov>=4.1.0",
]
tests = [
"behave>=1.3.3",
"coverage>=7.11.0",
"asv>=0.6.5",
"robotframework>=7.3.2"