Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions scripts/epic_body_staleness.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,15 +459,35 @@ def _cli(argv: list[str] | None = None) -> int:
print(f"epic_body_staleness: {exc}", file=sys.stderr)
return 1

json.dump(
text = json.dumps(
build_payload(epics, merged_prs),
sys.stdout,
ensure_ascii=False,
indent=2 if args.pretty else None,
)
print()
_write_stdout_utf8(text + "\n")
return 0


def _write_stdout_utf8(text: str) -> None:
"""Ecrit le rapport en UTF-8 sur stdout, quel que soit l'encodage de la console.

``json.dump(..., sys.stdout, ensure_ascii=False)`` passe par l'encodage de la
console : sous Windows en cp1252, un seul titre d'EPIC portant un caractere
hors cp1252 (U+21C4, U+2192, emoji) leve ``UnicodeEncodeError`` et le rapport
entier est perdu, y compris les entrees encodables. Mesure du 2026-11-04 :
premier run tronque a 1980 octets sur 59 findings, donc 2 findings visibles
sur 59. On ecrit les octets directement pour que le resultat ne dépende plus
de la console de la lane qui mesure.
"""
data = text.encode("utf-8")
buffer = getattr(sys.stdout, "buffer", None)
if buffer is not None:
buffer.write(data)
buffer.flush()
else: # stdout remplace par un objet sans buffer (capture de test)
sys.stdout.write(text)
sys.stdout.flush()


if __name__ == "__main__":
raise SystemExit(_cli())
38 changes: 38 additions & 0 deletions scripts/tests/test_epic_body_staleness.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from __future__ import annotations

import importlib.util
import io
import json
import sys
from pathlib import Path
Expand Down Expand Up @@ -419,6 +420,43 @@ def test_cli_prints_json_and_never_writes(monkeypatch, capsys):
assert payload["corpus"]["open_epics_examined"] == 1


class _Cp1252Console:
"""Console Windows cp1252 : `.write` echoue sur un caractere hors cp1252."""

def __init__(self) -> None:
self.buffer = io.BytesIO()

def write(self, text: str) -> int:
text.encode("cp1252") # leve UnicodeEncodeError comme la vraie console
return len(text)

def flush(self) -> None: # noqa: D401 - interface TextIO minimale
return None


def test_cli_survives_a_title_outside_the_console_codepage(monkeypatch):
"""Regression #13906 : un titre hors cp1252 faisait perdre le rapport entier.

Mesure du 2026-11-04 : `json.dump(..., sys.stdout)` levait UnicodeEncodeError
au premier titre portant U+21C4 et le fichier redirige restait tronque a
1980 octets, soit 2 findings visibles sur 59. Le rapport doit desormais
sortir en UTF-8 sans dependre de PYTHONIOENCODING.
"""
epic = Epic(42, "[EPIC] Compression ⇄ quantisation", "## Objectif\nConstruire la flotte.")
pr = _pr(101, "See #42")
monkeypatch.setattr(_MODULE, "list_open_epics", lambda repo: [epic])
monkeypatch.setattr(_MODULE, "list_merged_prs", lambda repo, limit: [pr])
console = _Cp1252Console()
monkeypatch.setattr(sys, "stdout", console)

assert _MODULE._cli(["--repo", "example/repo"]) == 0

raw = console.buffer.getvalue().decode("utf-8")
payload = json.loads(raw)
assert payload["corpus"]["open_epics_examined"] == 1
assert "⇄" in raw


def test_cli_reports_read_failure(monkeypatch, capsys):
def fail(_repo):
raise RuntimeError("GitHub unavailable")
Expand Down
Loading