"""``run-integration-tests`` console entry point. Runs a repo's integration suite with the org-standard pytest invocation — parallelism, an HTML report and Datadog tracing — so no repo has to re-declare those flags in a Makefile or Dockerfile. Marker-based changed-lambda filtering is applied by the companion pytest plugin (``test_fixtures.plugin``). Examples:: run-integration-tests # whole suite, parallel + report run-integration-tests --lambda finalize-job # one lambda (sets LAMBDA_FUNCTION_NAMES) run-integration-tests --serial -k test_x # serial (debugger-friendly), single case run-integration-tests --lambda finalize-job --rie --serial # local RIE: LAMBDA_ENDPOINT_URL is discovered automatically via # `docker compose --project-name rie-finalize-job port finalize-job 8080` # (see --rie below) run-integration-tests --lambda finalize-job \\ --endpoint-url http://localhost:9002 # RIE with a known/manual endpoint Any unrecognised arguments are passed straight through to pytest. """ import argparse from collections.abc import Callable, Sequence from importlib.util import find_spec import os import subprocess import sys DEFAULT_REPORT = 'report/report.html' class RIEDiscoveryError(RuntimeError): """The local RIE endpoint could not be discovered via docker compose.""" def discover_rie_endpoint( lambda_name: str, *, project_name: str | None = None, run: Callable[..., 'subprocess.CompletedProcess[str]'] = subprocess.run, ) -> str: """Return ``http://localhost:`` for *lambda_name*'s running RIE container. Shells out to ``docker compose --project-name port 8080`` — the same mechanism used to reach a manually-started RIE — so the caller never has to know or pass the port. *project_name* defaults to ``rie-``, the standard convention for isolating each lambda's RIE into its own compose project (so several can run in parallel); pass an explicit value only if a repo uses a different scheme. Requires the RIE container for *lambda_name* to already be up (e.g. via ``docker compose --project-name rie- --profile up -d --build --wait ``). """ project_name = project_name or f'rie-{lambda_name}' result = run( [ 'docker', 'compose', '--project-name', project_name, 'port', lambda_name, '8080', ], capture_output=True, text=True, ) output = result.stdout.strip() if result.returncode != 0 or not output: raise RIEDiscoveryError( f"Could not discover a running RIE for '{lambda_name}' in compose " f"project '{project_name}' via 'docker compose --project-name " f"{project_name} port {lambda_name} 8080'. Is its container running " f'(docker compose --project-name {project_name} --profile ' f'{lambda_name} up -d --build --wait {lambda_name})? ' f'stderr: {result.stderr.strip()}' ) port = output.rsplit(':', 1)[-1] return f'http://localhost:{port}' def _ddtrace_available() -> bool: return find_spec('ddtrace') is not None def build_pytest_args( args: argparse.Namespace, extra: Sequence[str], *, ddtrace: bool ) -> list[str]: """Assemble the pytest argument list from parsed options (pure/testable).""" # test_fixtures is both a console-script entry point (this module) and a # pytest11 plugin. Loading the entry point imports the test_fixtures package # before pytest's plugin autoloader gets a chance to mark it for assertion # rewriting, so pytest always warns "Module already imported so cannot be # rewritten; test_fixtures" — harmless (only affects introspection of # failures inside test_fixtures's own code, never the caller's test files), # but noisy on every run, so silence it here once for every repo. pytest_args = ['-v', '-W', 'ignore::pytest.PytestAssertRewriteWarning'] if not args.serial: # loadfile keeps a file's tests on one worker (serial within a lambda, # parallel across lambdas) — safe for shared per-lambda seed data. pytest_args += ['-n', 'auto', '--dist', 'loadfile'] if not args.no_report: pytest_args += [f'--html={args.report}', '--self-contained-html'] if ddtrace and not args.no_ddtrace: pytest_args += ['--ddtrace'] pytest_args += list(extra) return pytest_args def _parse_args(argv: Sequence[str]) -> tuple[argparse.Namespace, list[str]]: parser = argparse.ArgumentParser( prog='run-integration-tests', description='Run integration tests with the standard parallel + reporting options.', ) parser.add_argument( '-l', '--lambda', dest='lambda_name', help='Restrict to one lambda (sets LAMBDA_FUNCTION_NAMES). ' 'Comma-separate for several.', ) endpoint_group = parser.add_mutually_exclusive_group() endpoint_group.add_argument( '--endpoint-url', help='Point the Lambda client at a known local RIE URL ' '(sets LAMBDA_ENDPOINT_URL directly).', ) endpoint_group.add_argument( '--rie', action='store_true', help="Point the Lambda client at --lambda's already-running local RIE " 'container. The port is discovered automatically via ' "'docker compose --project-name rie- port 8080' — no " 'need to know or pass it.', ) parser.add_argument( '--rie-project', help='docker compose project name the RIE is running under (with --rie). ' "Defaults to 'rie-', the standard convention — only pass this if " 'the repo uses a different naming scheme.', ) parser.add_argument( '--serial', action='store_true', help='Disable parallelism (use for the debugger / single test).', ) parser.add_argument( '--report', default=DEFAULT_REPORT, help=f'HTML report path (default: {DEFAULT_REPORT}).', ) parser.add_argument( '--no-report', action='store_true', help='Skip the HTML report.' ) parser.add_argument( '--no-ddtrace', action='store_true', help='Disable Datadog tracing even when ddtrace is installed.', ) return parser.parse_known_args(list(argv)) def main(argv: Sequence[str] | None = None) -> int: args, extra = _parse_args(sys.argv[1:] if argv is None else argv) if args.lambda_name: os.environ['LAMBDA_FUNCTION_NAMES'] = args.lambda_name if args.endpoint_url: os.environ['LAMBDA_ENDPOINT_URL'] = args.endpoint_url if args.rie: if not args.lambda_name or ',' in args.lambda_name: raise SystemExit('--rie requires --lambda with exactly one lambda name.') os.environ['LAMBDA_ENDPOINT_URL'] = discover_rie_endpoint( args.lambda_name, project_name=args.rie_project ) if not args.no_report: report_dir = os.path.dirname(args.report) if report_dir: os.makedirs(report_dir, exist_ok=True) pytest_args = build_pytest_args(args, extra, ddtrace=_ddtrace_available()) import pytest return pytest.main(pytest_args) if __name__ == '__main__': raise SystemExit(main())