Files
2026-07-13 13:05:14 +08:00

399 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""Run all examples."""
from __future__ import annotations
import argparse
import os
import socket
import subprocess
import sys
import time
from glob import glob
from pathlib import Path
from typing import TYPE_CHECKING, Any, Self
if TYPE_CHECKING:
from types import TracebackType
EXTRA_ARGS = {
"examples/python/clock": ["--steps=200"], # Make it faster
"examples/python/detect_and_track_objects": ["--max-frame=10"], # Make it faster
"examples/python/face_tracking": ["--max-frame=30"], # Make sure it finishes
"examples/python/human_pose_tracking": ["--max-frame=10"], # Make it faster
"examples/python/live_camera_edge_detection": ["--num-frames=30"], # Make sure it finishes
}
HAS_NO_RERUN_ARGS = {
"examples/python/blueprint",
"examples/python/dna",
"examples/python/minimal",
"examples/python/multiprocessing",
"examples/python/shared_recording",
"examples/python/stdio",
}
MIN_PYTHON_REQUIREMENTS: dict[str, tuple[int, int]] = {
"examples/python/controlnet": (3, 10),
# pyopf requires Python 3.10
"examples/python/open_photogrammetry_format": (3, 10),
}
MAX_PYTHON_REQUIREMENTS: dict[str, tuple[int, int]] = {
"examples/python/face_tracking": (3, 11), # TODO(ab): remove when mediapipe is 3.12 compatible
"examples/python/human_pose_tracking": (3, 11), # TODO(ab): remove when mediapipe is 3.12 compatible
"examples/python/llm_embedding_ner": (3, 11), # TODO(ab): remove when torch is umap-learn/numba is 3.12 compatible
}
SKIP_LIST = [
# depth_sensor requires a specific piece of hardware to be attached
"examples/python/live_depth_sensor",
# ros requires complex system dependencies to be installed
"examples/python/ros_node",
# this needs special treatment to run
"examples/python/external_importer",
]
MAC_SKIP_LIST: list[str] = []
def start_process(args: list[str], *, wait: bool) -> Any:
readable_cmd = " ".join(f'"{a}"' if " " in a else a for a in args)
print(f"> {readable_cmd}")
process = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if wait:
returncode = process.wait()
if returncode != 0:
print(output_from_process(process))
print()
print(f"process exited with error code {returncode}")
sys.exit(returncode)
return process
def run_py_example(path: str, viewer_port: int | None = None, *, wait: bool = True, save: str | None = None) -> Any:
args = [os.path.join(path, "main.py")]
if path in EXTRA_ARGS:
args += EXTRA_ARGS[path]
if save is not None:
args += [f"--save={save}"]
if viewer_port is not None:
args += ["--connect", f"--url=rerun+http://127.0.0.1:{viewer_port}/proxy"]
return start_process(
args,
wait=wait,
)
# stdout and stderr
def output_from_process(process: subprocess.Popen[bytes]) -> str:
return process.communicate()[0].decode("utf-8").rstrip()
def get_free_port() -> int:
with socket.socket() as s:
s.bind(("", 0))
return int(s.getsockname()[1])
def collect_examples(fast: bool) -> list[str]:
if fast:
# cherry-picked
return [
"examples/python/car",
"examples/python/clock",
"examples/python/dicom_mri",
"examples/python/plots",
"examples/python/raw_mesh",
"examples/python/rgbd",
"examples/python/structure_from_motion",
]
else:
examples = []
for main_path in glob("examples/python/**/main.py"):
example = os.path.dirname(main_path)
if example in SKIP_LIST:
continue
major, minor, *_ = sys.version_info
if example in MIN_PYTHON_REQUIREMENTS:
req_major, req_minor = MIN_PYTHON_REQUIREMENTS[example]
if major < req_major or (major == req_major and minor < req_minor):
continue
if example in MAX_PYTHON_REQUIREMENTS:
req_major, req_minor = MAX_PYTHON_REQUIREMENTS[example]
if major > req_major or (major == req_major and minor > req_minor):
continue
if example in MAC_SKIP_LIST and sys.platform == "darwin":
continue
examples.append(example)
return examples
def print_example_output(path: str, example: Any) -> None:
output = example.communicate()[0].decode("utf-8").rstrip()
print(f"\nExample {path}:\n{output}\n")
class Viewer:
should_close: bool
web: bool
sdk_port: int # where the logging SDK sends the log stream (where the server receives)
web_viewer_port: int # the HTTP port where we serve the web viewer
grpc_server_port: int # the gRPC port where we serve the log stream
process: Any | None
def __init__(self, close: bool = False, web: bool = False) -> None:
self.should_close = close
self.web = web
self.sdk_port = get_free_port()
self.web_viewer_port = get_free_port()
self.grpc_server_port = get_free_port()
self.process = None
def close(self) -> None:
if self.process is not None and self.should_close:
self.process.kill()
self.process = None
def start(self) -> Viewer:
print(f"\nStarting viewer on {'web ' if self.web else ''}port {self.sdk_port}")
CARGO_TARGET_DIR = Path(os.environ.get("CARGO_TARGET_DIR", "./target"))
args = [f"{CARGO_TARGET_DIR}/debug/rerun", f"--port={self.sdk_port}"]
if self.web:
args += [
"--web-viewer",
f"--web-viewer-port={self.web_viewer_port}",
f"--port={self.grpc_server_port}",
]
self.process = subprocess.Popen(args)
time.sleep(1)
return self
def __enter__(self) -> Self:
self.start()
return self
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
traceback: TracebackType | None,
) -> None:
self.close()
def run_sdk_build() -> None:
print("Building Python SDK…")
returncode = subprocess.Popen(
[
"maturin",
"develop",
"--manifest-path",
"rerun_py/Cargo.toml",
'--extras="tests"',
],
).wait()
assert returncode == 0, f"process exited with error code {returncode}"
def run_viewer_build(web: bool) -> None:
print("Building Rerun Viewer…")
returncode = subprocess.Popen([
"cargo",
"build",
"-p",
"rerun-cli",
"--no-default-features",
"--features=web_viewer" if web else "--features=native_viewer",
]).wait()
assert returncode == 0, f"process exited with error code {returncode}"
def run_install_requirements(examples: list[str]) -> None:
"""Install dependencies for the provided list of examples if they have a requirements.txt file."""
args = []
for example in examples:
req = Path(example) / "requirements.txt"
if req.exists():
args.extend(["-r", str(req)])
print("Installing examples requirements…")
returncode = subprocess.Popen(["pip", "install", *args]).wait()
assert returncode == 0, f"process exited with error code {returncode}"
def run_web(examples: list[str], parallel: bool) -> None:
if parallel:
entries: list[tuple[str, Any, Any]] = []
# start all examples in parallel
for path in examples:
if path in HAS_NO_RERUN_ARGS:
continue
# each example gets its own viewer
viewer = Viewer(web=True).start()
example = run_py_example(path, viewer_port=viewer.sdk_port, wait=False)
entries.append((path, viewer, example))
# wait for examples to finish logging
for entry in entries:
_, _, example = entry
example.wait()
# give servers/viewers a moment to finish loading data
time.sleep(5)
# shut down servers/viewers
for entry in entries:
path, viewer, example = entry
print_example_output(path, example)
viewer.close()
else:
with Viewer(close=True, web=True) as viewer:
for path in examples:
if path in HAS_NO_RERUN_ARGS:
continue
process = run_py_example(path, viewer_port=viewer.sdk_port)
print(f"{output_from_process(process)}\n")
def run_save(examples: list[str]) -> None:
for path in examples:
if path not in HAS_NO_RERUN_ARGS:
process = run_py_example(path, save=os.path.join(path, "out.rrd"))
print(f"{output_from_process(process)}\n")
def run_saved_example(path: str, *, wait: bool) -> Any:
return start_process(
[
"cargo",
"rerun",
os.path.join(path, "out.rrd"),
],
wait=wait,
)
def run_load(examples: list[str], parallel: bool, close: bool) -> None:
examples = [path for path in examples if path not in HAS_NO_RERUN_ARGS]
if parallel:
entries: list[tuple[str, Any]] = []
for path in examples:
example = run_saved_example(path, wait=False)
entries.append((path, example))
for entry in entries:
path, example = entry
print_example_output(path, example)
if close:
example.kill()
else:
# run all examples sequentially
for path in examples:
# each one must be closed for the next one to start running
process = run_saved_example(path, wait=True)
print(f"{output_from_process(process)}\n")
def run_native(examples: list[str], parallel: bool, close: bool) -> None:
if parallel:
# start all examples in parallel:
cleanup: list[tuple[Any, Any]] = []
for path in examples:
if path in HAS_NO_RERUN_ARGS:
continue
# each example gets its own viewer
viewer = Viewer().start()
example = run_py_example(path, viewer.sdk_port, wait=False)
cleanup.append((viewer, example))
# wait for all processes to finish, and close the viewers if requested
for pair in cleanup:
viewer, example = pair
print_example_output(path, example)
if close:
viewer.close()
else:
# run all examples sequentially in a single viewer
with Viewer(close) as viewer:
for path in examples:
if path in HAS_NO_RERUN_ARGS:
continue
process = run_py_example(path, viewer_port=viewer.sdk_port, wait=True)
print(f"{output_from_process(process)}\n")
def main() -> None:
parser = argparse.ArgumentParser(description="Runs all examples.")
parser.add_argument("--skip-build", action="store_true", help="Skip building the Python SDK.")
parser.add_argument(
"--install-requirements",
action="store_true",
help="Install Python requirements for each example.",
)
parser.add_argument("--web", action="store_true", help="Run examples in a web viewer.")
parser.add_argument(
"--save",
action="store_true",
help="Run examples and save them to disk as rrd.",
)
parser.add_argument(
"--load",
action="store_true",
help="Run examples using rrd files previously saved via `--save`.",
)
parser.add_argument("--fast", action="store_true", help="Run only examples which complete quickly.")
parser.add_argument("--parallel", action="store_true", help="Run all examples in parallel.")
parser.add_argument("--close", action="store_true", help="Close the viewer after running all examples.")
args = parser.parse_args()
examples = collect_examples(args.fast)
if not args.skip_build:
if not args.load:
run_sdk_build()
if not args.save:
run_viewer_build(args.web)
if args.install_requirements:
run_install_requirements(examples)
if args.web:
run_web(examples, parallel=args.parallel)
return
if args.save:
run_save(examples)
if not args.load:
return
if args.load:
run_load(examples, parallel=args.parallel, close=args.close)
return
run_native(examples, parallel=args.parallel, close=args.close)
if __name__ == "__main__":
main()