from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
DEFAULT_TIMEOUT_S = 300.0
def discover(metadata: dict) -> list[tuple[str, list[str]]]:
examples = []
for pkg in metadata.get("packages", []):
if Path(pkg["manifest_path"]).resolve().parent != ROOT:
continue
for target in pkg.get("targets", []):
if "example" in target.get("kind", []):
examples.append((target["name"], list(target.get("required-features", []))))
return sorted(examples)
def feature_union(examples) -> list[str]:
feats = set()
for _, req in examples:
feats.update(req)
feats.discard("std") return sorted(feats)
def run_one(binary: Path, timeout_s: float) -> tuple[str, float, str]:
start = time.monotonic()
try:
proc = subprocess.run(
[str(binary)],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
timeout=timeout_s,
check=False,
)
except subprocess.TimeoutExpired as exc:
out = (exc.stdout or b"").decode("utf-8", "replace")
return "timeout", time.monotonic() - start, out[-2000:]
elapsed = time.monotonic() - start
out = proc.stdout.decode("utf-8", "replace")
status = "ok" if proc.returncode == 0 else f"exit {proc.returncode}"
return status, elapsed, out[-2000:]
def run_all(binaries: dict[str, Path], timeout_s: float) -> tuple[list, list[str]]:
rows = []
problems = []
if not binaries:
problems.append("no example to run (0 executed)")
return rows, problems
executed = 0
for name, binary in sorted(binaries.items()):
if not binary.is_file():
problems.append(f"{name}: binary {binary} missing after the build")
continue
status, elapsed, tail = run_one(binary, timeout_s)
executed += 1
rows.append((name, status, elapsed))
print(f"{status:>8} {elapsed:7.1f} s {name}", flush=True)
if status != "ok":
problems.append(f"{name}: {status} after {elapsed:.1f} s\n{tail}")
if executed == 0:
problems.append("0 examples executed")
elif executed != len(binaries):
problems.append(f"executed {executed} of {len(binaries)} examples")
return rows, problems
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
ap.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT_S)
ap.add_argument("--list", action="store_true", help="print the examples and exit")
args = ap.parse_args()
meta = json.loads(
subprocess.run(
["cargo", "metadata", "--no-deps", "--format-version", "1"],
cwd=ROOT,
check=True,
stdout=subprocess.PIPE,
).stdout
)
examples = discover(meta)
features = feature_union(examples)
files = sorted((ROOT / "examples").glob("*.rs"))
if args.list:
for name, req in examples:
print(name, ",".join(req))
print(f"{len(examples)} examples, features: {','.join(features) or '(default)'}")
return 0
if not examples:
print("run_examples: no example target discovered", file=sys.stderr)
return 1
if len(examples) < len(files):
print(
f"run_examples: {len(examples)} example targets but {len(files)} examples/*.rs files",
file=sys.stderr,
)
return 1
build = ["cargo", "build", "--release", "--examples"]
if features:
build += ["--features", ",".join(features)]
print("+", " ".join(build), flush=True)
subprocess.run(build, cwd=ROOT, check=True)
target_dir = Path(meta.get("target_directory", ROOT / "target"))
exe = ".exe" if os.name == "nt" else ""
binaries = {
name: target_dir / "release" / "examples" / f"{name}{exe}" for name, _ in examples
}
rows, problems = run_all(binaries, args.timeout)
total = sum(r[2] for r in rows)
print(f"\n{len(rows)} examples executed in {total:.1f} s (timeout {args.timeout:.0f} s each)")
if problems:
print("\nrun_examples: FAILED", file=sys.stderr)
for p in problems:
print("-", p, file=sys.stderr)
return 1
print("run_examples: OK")
return 0
if __name__ == "__main__":
sys.exit(main())