diff --git a/CHANGELOG.md b/CHANGELOG.md index c9a7662..b29d6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,8 @@ All notable changes to this project should be documented in this file. ### Fixed +- Fixed error messages in `minimize.py` printing to stdout instead of stderr, by @devdanzin. +- Fixed timezone-naive `datetime.now()` calls in `triage.py` to use `datetime.now(timezone.utc)` for consistency with the rest of the codebase, by @devdanzin. - Fixed stat_key inconsistency: orchestrator checked `"timeout_count"` but execution.py returns `"timeouts_found"`, causing `HealthMonitor.record_timeout()` to never trigger, by @devdanzin. ### Enhanced diff --git a/lafleur/minimize.py b/lafleur/minimize.py index 3e5a846..61f1c4f 100644 --- a/lafleur/minimize.py +++ b/lafleur/minimize.py @@ -142,20 +142,26 @@ def _load_and_validate_metadata(crash_dir: Path) -> tuple[dict, str, list[Path]] """ metadata_path = crash_dir / "metadata.json" if not metadata_path.exists(): - print("[!] Error: metadata.json not found. Cannot minimize.") + print("[!] Error: metadata.json not found. Cannot minimize.", file=sys.stderr) sys.exit(1) try: metadata = json.loads(metadata_path.read_text()) except json.JSONDecodeError: - print("[!] Error: Invalid metadata.json.") + print("[!] Error: Invalid metadata.json.", file=sys.stderr) sys.exit(1) # Validate that we have a non-zero crash return code target_returncode = metadata.get("returncode") if target_returncode is None or target_returncode == 0: - print("[!] Error: metadata.json has no crash return code (returncode is missing or 0).") - print(" Cannot determine crash reproduction without a non-zero exit code.") + print( + "[!] Error: metadata.json has no crash return code (returncode is missing or 0).", + file=sys.stderr, + ) + print( + " Cannot determine crash reproduction without a non-zero exit code.", + file=sys.stderr, + ) sys.exit(1) grep_pattern = extract_grep_pattern(metadata) @@ -164,7 +170,7 @@ def _load_and_validate_metadata(crash_dir: Path) -> tuple[dict, str, list[Path]] script_files = sorted([f for f in crash_dir.glob("*.py") if f.name != "combined_repro.py"]) if not script_files: - print("[!] No script files found in bundle.") + print("[!] No script files found in bundle.", file=sys.stderr) sys.exit(1) return metadata, grep_pattern, script_files @@ -396,8 +402,14 @@ def minimize_session(crash_dir: Path, target_python: str, force_overwrite: bool) # Initial Reproduction Check print("[*] Verifying initial crash reproduction...") if not check_reproduction(script_files, metadata, grep_pattern, target_python): - print(f"[!] Error: Crash does NOT reproduce with the provided python ({target_python}).") - print(" Check that you are using the correct JIT-enabled build.") + print( + f"[!] Error: Crash does NOT reproduce with the provided python ({target_python}).", + file=sys.stderr, + ) + print( + " Check that you are using the correct JIT-enabled build.", + file=sys.stderr, + ) sys.exit(1) # Stage 1: Script Minimization @@ -465,7 +477,7 @@ def main(): args = parser.parse_args() if not args.crash_dir.exists(): - print(f"Error: Directory {args.crash_dir} does not exist.") + print(f"Error: Directory {args.crash_dir} does not exist.", file=sys.stderr) sys.exit(1) minimize_session(args.crash_dir, args.target_python, args.force_overwrite) diff --git a/lafleur/triage.py b/lafleur/triage.py index bd17e16..4bae173 100644 --- a/lafleur/triage.py +++ b/lafleur/triage.py @@ -11,7 +11,7 @@ import json import subprocess import sys -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from typing import Any @@ -23,7 +23,7 @@ def load_json_file(path: Path) -> dict[str, Any] | None: try: with open(path, encoding="utf-8") as f: return json.load(f) - except (FileNotFoundError, json.JSONDecodeError, OSError): + except FileNotFoundError, json.JSONDecodeError, OSError: return None @@ -46,7 +46,7 @@ def get_revision_date(revision: str) -> int | None: ) if result.returncode == 0: return int(result.stdout.strip()) - except (subprocess.TimeoutExpired, ValueError, OSError): + except subprocess.TimeoutExpired, ValueError, OSError: pass return None @@ -60,7 +60,7 @@ def parse_iso_timestamp(timestamp_str: str) -> int | None: timestamp_str = timestamp_str[:-1] + "+00:00" dt = datetime.fromisoformat(timestamp_str) return int(dt.timestamp()) - except (ValueError, TypeError): + except ValueError, TypeError: return None @@ -278,7 +278,7 @@ def import_campaign(args: argparse.Namespace) -> None: if len(rev_part) >= 8: cpython_revision = rev_part revision_date = get_revision_date(cpython_revision) - except (IndexError, ValueError): + except IndexError, ValueError: pass # Fall back to start_time for revision_date proxy @@ -401,9 +401,11 @@ def record_issue_wizard(args: argparse.Namespace) -> None: url = f"https://github.com/python/cpython/issues/{issue_number}" reported_by = input("Reported by (your GitHub username): ").strip() - reported_date = input(f"Reported date (default: {datetime.now().date().isoformat()}): ").strip() + reported_date = input( + f"Reported date (default: {datetime.now(timezone.utc).date().isoformat()}): " + ).strip() if not reported_date: - reported_date = datetime.now().date().isoformat() + reported_date = datetime.now(timezone.utc).date().isoformat() description = input("Brief description: ").strip() @@ -464,7 +466,7 @@ def record_issue_wizard(args: argparse.Namespace) -> None: fingerprint=fingerprint, run_id="manual", instance_name="manual", - timestamp=datetime.now().isoformat(), + timestamp=datetime.now(timezone.utc).isoformat(), ) registry.link_crash_to_issue(fingerprint, issue_number) print(f"[+] Created crash record and linked to issue #{issue_number}") diff --git a/tests/test_minimize.py b/tests/test_minimize.py index 5759ac7..c1b336f 100644 --- a/tests/test_minimize.py +++ b/tests/test_minimize.py @@ -430,13 +430,13 @@ def test_missing_returncode_exits(self): from io import StringIO - captured_stdout = StringIO() - with patch("sys.stdout", captured_stdout): + captured_stderr = StringIO() + with patch("sys.stderr", captured_stderr): with self.assertRaises(SystemExit) as ctx: minimize_session(self.crash_dir, "python3", force_overwrite=True) self.assertEqual(ctx.exception.code, 1) - self.assertIn("returncode", captured_stdout.getvalue().lower()) + self.assertIn("returncode", captured_stderr.getvalue().lower()) def test_zero_returncode_exits(self): """Test that returncode 0 in metadata causes exit.""" @@ -447,13 +447,13 @@ def test_zero_returncode_exits(self): from io import StringIO - captured_stdout = StringIO() - with patch("sys.stdout", captured_stdout): + captured_stderr = StringIO() + with patch("sys.stderr", captured_stderr): with self.assertRaises(SystemExit) as ctx: minimize_session(self.crash_dir, "python3", force_overwrite=True) self.assertEqual(ctx.exception.code, 1) - self.assertIn("returncode", captured_stdout.getvalue().lower()) + self.assertIn("returncode", captured_stderr.getvalue().lower()) @patch("lafleur.minimize.run_session") @patch("lafleur.minimize.shutil.which")