Run a subprocess step with consistent timing/logging/failure formatting.
(
cmd: list[str],
*,
step: str,
cwd: pathlib.Path,
env: dict[str, str],
timeout_s: int | float | None = None,
stream_output: bool = False,
log_prefix: str = "examples",
failure_prefix: str = "Subprocess step failed",
output_tail_chars: int = 8000,
)
| 662 | |
| 663 | |
| 664 | def run_subprocess_step( |
| 665 | cmd: list[str], |
| 666 | *, |
| 667 | step: str, |
| 668 | cwd: pathlib.Path, |
| 669 | env: dict[str, str], |
| 670 | timeout_s: int | float | None = None, |
| 671 | stream_output: bool = False, |
| 672 | log_prefix: str = "examples", |
| 673 | failure_prefix: str = "Subprocess step failed", |
| 674 | output_tail_chars: int = 8000, |
| 675 | ) -> tuple[subprocess.CompletedProcess, float]: |
| 676 | """Run a subprocess step with consistent timing/logging/failure formatting.""" |
| 677 | print(f"[{log_prefix}] step={step} command={' '.join(cmd)}", flush=True) |
| 678 | start = time.perf_counter() |
| 679 | run_kwargs = { |
| 680 | "cwd": cwd, |
| 681 | "env": env, |
| 682 | "check": False, |
| 683 | } |
| 684 | if timeout_s is not None: |
| 685 | run_kwargs["timeout"] = timeout_s |
| 686 | if not stream_output: |
| 687 | run_kwargs["capture_output"] = True |
| 688 | run_kwargs["text"] = True |
| 689 | result = subprocess.run(cmd, **run_kwargs) |
| 690 | elapsed_s = time.perf_counter() - start |
| 691 | print(f"[{log_prefix}] step={step} elapsed_s={elapsed_s:.2f}", flush=True) |
| 692 | |
| 693 | if result.returncode != 0: |
| 694 | if stream_output: |
| 695 | output_info = "See streamed test logs above for subprocess output." |
| 696 | else: |
| 697 | output = (result.stdout or "") + (result.stderr or "") |
| 698 | output_info = f"output_tail=\n{output[-output_tail_chars:]}" |
| 699 | raise AssertionError( |
| 700 | f"{failure_prefix}: {step}\n" |
| 701 | f"elapsed_s={elapsed_s:.2f}\n" |
| 702 | f"returncode={result.returncode}\n" |
| 703 | f"command={' '.join(cmd)}\n" |
| 704 | f"{output_info}" |
| 705 | ) |
| 706 | return result, elapsed_s |
| 707 | |
| 708 | |
| 709 | @contextlib.contextmanager |