Skip to content
Merged
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
6 changes: 4 additions & 2 deletions oracletrace/compare.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from .tracer import TracerData, FunctionData
from .tracer import TracerData, FunctionData, _safe_char
from rich import print
from typing import Any, Dict, List, Set, Optional
from dataclasses import dataclass

_ARROW = _safe_char("→", "->")

@dataclass
class RegressionData:
name: str
Expand Down Expand Up @@ -59,7 +61,7 @@ def compare_traces(

print(
f"{name}\n"
f" total_time: {old_time:.4f}s {new_time:.4f}s "
f" total_time: {old_time:.4f}s {_ARROW} {new_time:.4f}s "
f"[{color}]({percent:+.2f}%)[/]\n"
) if not show_only_regressions or percent > threshold else ...

Expand Down
17 changes: 16 additions & 1 deletion oracletrace/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@
ProfileEvent: TypeAlias = Literal["call", "return", "c_call", "c_return", "c_exception"]
ProfileFunction: TypeAlias = Callable[[FrameType, ProfileEvent, Any], object]


def _safe_char(preferred: str, fallback: str) -> str:
# Use the unicode char only when stdout can actually encode it —
# piped output on Windows is often cp1252, where rich raises
# UnicodeEncodeError and the whole run crashes after tracing finished
enc = getattr(sys.stdout, "encoding", None) or "utf-8"
try:
preferred.encode(enc)
return preferred
except (UnicodeEncodeError, LookupError):
return fallback


_CYCLE = _safe_char("↻", "@")

@dataclass
class TracerMetadata:
root_path: str
Expand Down Expand Up @@ -271,7 +286,7 @@ def add_nodes(parent_node: Tree, parent_key: str, current_path: set[str]) -> Non
_total_time = self._func_time[child_key]
# Detect recursion to prevent infinite loops in the tree
if child_key in current_path:
parent_node.add(f"[red] {child_key}[/] ({count}x)")
parent_node.add(f"[red]{_CYCLE} {child_key}[/] ({count}x)")
continue

node_text = f"{child_key} [dim]({count}x, {_total_time:.4f}s)[/]"
Expand Down
66 changes: 66 additions & 0 deletions tests/test_encoding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import os
import subprocess
import sys
import tempfile
from pathlib import Path

from oracletrace.tracer import _safe_char

REPO_ROOT = Path(__file__).resolve().parents[1]

RECURSIVE_SCRIPT = """\
def rec(n):
if n:
rec(n - 1)

def main():
rec(3)

main()
"""


class _FakeStdout:
def __init__(self, encoding):
self.encoding = encoding


def test_safe_char_keeps_unicode_when_encodable(monkeypatch):
monkeypatch.setattr(sys, "stdout", _FakeStdout("utf-8"))
assert _safe_char("↻", "@") == "↻"


def test_safe_char_falls_back_on_cp1252(monkeypatch):
monkeypatch.setattr(sys, "stdout", _FakeStdout("cp1252"))
assert _safe_char("↻", "@") == "@"
assert _safe_char("→", "->") == "->"


def test_safe_char_survives_missing_encoding(monkeypatch):
monkeypatch.setattr(sys, "stdout", _FakeStdout(None))
assert _safe_char("→", "->") == "→" # falls back to utf-8


def test_piped_cp1252_output_does_not_crash():
# Piped output on Windows often defaults to cp1252, which cannot encode
# the recursion marker in the call tree; the run then dies with
# UnicodeEncodeError AFTER tracing succeeded. Forced via PYTHONIOENCODING
# so the regression is caught on any platform.
env = {
**os.environ,
"PYTHONPATH": str(REPO_ROOT),
"PYTHONIOENCODING": "cp1252",
}
with tempfile.TemporaryDirectory(dir=REPO_ROOT) as tmp:
script = Path(tmp) / "script.py"
script.write_text(RECURSIVE_SCRIPT, encoding="utf-8")
result = subprocess.run(
[sys.executable, "-m", "oracletrace", "script.py"],
capture_output=True,
text=True,
cwd=tmp,
env=env,
)
assert result.returncode == 0, result.stderr
assert "UnicodeEncodeError" not in result.stderr
assert "rec" in result.stdout