Skip to content

Commit 82b37c2

Browse files
committed
WS2
1 parent 891626f commit 82b37c2

File tree

3 files changed

+82
-7
lines changed

3 files changed

+82
-7
lines changed

codetracer-python-recorder/codetracer_python_recorder/cli.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from pathlib import Path
1212
from typing import Iterable, Sequence
1313

14-
from . import flush, start, stop
14+
from . import flush, policy_snapshot, start, stop
1515
from .auto_start import ENV_TRACE_FILTER
1616
from .formats import DEFAULT_FORMAT, SUPPORTED_FORMATS, normalize_format
1717

@@ -129,6 +129,15 @@ def _parse_args(argv: Sequence[str]) -> RecorderCLIConfig:
129129
"Use '--no-module-name-from-globals' to fall back to the legacy resolver."
130130
),
131131
)
132+
parser.add_argument(
133+
"--propagate-script-exit",
134+
action=argparse.BooleanOptionalAction,
135+
default=None,
136+
help=(
137+
"Mirror the traced script's exit status when the recorder succeeds (default: disabled). "
138+
"Use '--no-propagate-script-exit' to force a zero exit status."
139+
),
140+
)
132141

133142
known, remainder = parser.parse_known_args(argv)
134143
pending: list[str] = list(remainder)
@@ -192,6 +201,8 @@ def _parse_args(argv: Sequence[str]) -> RecorderCLIConfig:
192201
parser.error(f"unsupported io-capture mode '{other}'")
193202
if known.module_name_from_globals is not None:
194203
policy["module_name_from_globals"] = known.module_name_from_globals
204+
if known.propagate_script_exit is not None:
205+
policy["propagate_script_exit"] = known.propagate_script_exit
195206

196207
return RecorderCLIConfig(
197208
trace_dir=trace_dir,
@@ -286,7 +297,11 @@ def main(argv: Iterable[str] | None = None) -> int:
286297
sys.argv = old_argv
287298
return 1
288299

300+
snapshot = policy_snapshot()
301+
propagate_script_exit = bool(snapshot.get("propagate_script_exit"))
302+
289303
exit_code: int | None = None
304+
recorder_failed = False
290305
try:
291306
try:
292307
runpy.run_path(str(script_path), run_name="__main__")
@@ -297,13 +312,35 @@ def main(argv: Iterable[str] | None = None) -> int:
297312
finally:
298313
try:
299314
flush()
315+
except Exception as exc:
316+
recorder_failed = True
317+
sys.stderr.write(f"Failed to flush Codetracer session: {exc}\n")
300318
finally:
301-
stop(exit_code=exit_code)
302-
sys.argv = old_argv
319+
try:
320+
stop(exit_code=exit_code)
321+
except Exception as exc:
322+
recorder_failed = True
323+
sys.stderr.write(f"Failed to stop Codetracer session: {exc}\n")
324+
finally:
325+
sys.argv = old_argv
303326

304327
_serialise_metadata(trace_dir, script=script_path)
305328

306-
return exit_code if exit_code is not None else 0
329+
script_exit_code = exit_code if exit_code is not None else 0
330+
331+
if recorder_failed:
332+
return 1
333+
334+
if propagate_script_exit:
335+
return script_exit_code
336+
337+
if script_exit_code != 0:
338+
sys.stderr.write(
339+
f"Script exited with status {script_exit_code}; returning 0. "
340+
"Use '--propagate-script-exit' to mirror the script exit code.\n"
341+
)
342+
343+
return 0
307344

308345

309346
__all__ = ("main", "RecorderCLIConfig")

codetracer-python-recorder/tests/python/test_exit_payloads.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def test_cli_records_exit_code_in_toplevel_return(tmp_path: Path) -> None:
4444
check=False,
4545
)
4646

47-
assert result.returncode == 3, result.stderr
47+
assert result.returncode == 0, result.stderr
48+
assert "status 3; returning 0" in result.stderr
4849
exit_value = _last_return_value(trace_dir)
4950
assert exit_value["kind"] == "Int"
5051
assert exit_value["i"] == 3
@@ -54,6 +55,43 @@ def test_cli_records_exit_code_in_toplevel_return(tmp_path: Path) -> None:
5455
assert status == {"code": 3, "label": None}
5556

5657

58+
def test_cli_can_propagate_script_exit(tmp_path: Path) -> None:
59+
script = tmp_path / "exit_script.py"
60+
script.write_text(
61+
"import sys\n"
62+
"sys.exit(5)\n",
63+
encoding="utf-8",
64+
)
65+
66+
trace_dir = tmp_path / "trace"
67+
result = subprocess.run(
68+
[
69+
sys.executable,
70+
"-m",
71+
"codetracer_python_recorder",
72+
"--trace-dir",
73+
str(trace_dir),
74+
"--format",
75+
"json",
76+
"--propagate-script-exit",
77+
str(script),
78+
],
79+
capture_output=True,
80+
text=True,
81+
check=False,
82+
)
83+
84+
assert result.returncode == 5, result.stderr
85+
assert "status 5; returning 0" not in result.stderr
86+
exit_value = _last_return_value(trace_dir)
87+
assert exit_value["kind"] == "Int"
88+
assert exit_value["i"] == 5
89+
90+
metadata = json.loads((trace_dir / "trace_metadata.json").read_text(encoding="utf-8"))
91+
status = metadata.get("process_exit_status")
92+
assert status == {"code": 5, "label": None}
93+
94+
5795
def test_default_exit_payload_uses_placeholder(tmp_path: Path) -> None:
5896
trace_dir = tmp_path / "trace"
5997
trace_dir.mkdir()

design-docs/recorder-exit-code-policy-implementation-plan.status.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
## Workstream Progress
88
-**WS1 – Policy & Configuration Plumbing:** Added `propagate_script_exit` across `RecorderPolicy`, `PolicyUpdate`, PyO3 bindings, env parsing, and Python helpers; introduced `CODETRACER_PROPAGATE_SCRIPT_EXIT`; updated Rust + Python unit coverage; rebuilt the dev wheel (`maturin develop --features integration-test`) and verified via `just test`.
9-
- **WS2 – CLI Behaviour & Warning Surface:** _Not started._
9+
- **WS2 – CLI Behaviour & Warning Surface:** CLI now defaults to returning `0` on successful recordings, exposes `--propagate-script-exit`/`--no-propagate-script-exit`, emits a warning when suppressing non-zero script exits, and preserves non-zero statuses for recorder failures; added regression coverage (`test_exit_payloads.py`) and executed `just dev test`.
1010
-**WS3 – Documentation, Tooling, and Release Notes:** _Not started._
1111

1212
## Current Focus
13-
- Confirm no downstream consumers rely on the legacy policy snapshot schema; prepare CLI changes for WS2.
13+
- Plan WS3 documentation updates and release notes covering the new default exit behaviour and configuration surfaces.

0 commit comments

Comments
 (0)