Build: Made behave so it can run in parallel at 1/3 the time
This commit is contained in:
+185
-23
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user