Skip to content

Commit 8cc0340

Browse files
committed
Add a partial-line-marker on Apple system logger and optimize the iOS testbed runner
1 parent 6544bf4 commit 8cc0340

3 files changed

Lines changed: 63 additions & 7 deletions

File tree

Lib/_apple_support.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ def write(self, s):
3737

3838

3939
class LogStream(io.RawIOBase):
40+
# Marker appended to any log message that does not end with a newline,
41+
# so a cooperating log reader can re-join it with the following message
42+
# instead of rendering it as a spurious standalone line. Placing the
43+
# marker after the data also prevents the system log from stripping any
44+
# trailing whitespace. Uses ASCII Unit Separator (0x1f).
45+
PARTIAL_LINE_MARKER = b"\x1f"
46+
4047
def __init__(self, log_write, level):
4148
self.log_write = log_write
4249
self.level = level
@@ -59,8 +66,14 @@ def write(self, b):
5966
# Writing an empty string to the stream should have no effect.
6067
if b:
6168
# Encode null bytes using "modified UTF-8" to avoid truncating the
62-
# message. This should not affect the return value, as the caller
63-
# may be expecting it to match the length of the input.
64-
self.log_write(self.level, b.replace(b"\x00", b"\xc0\x80"))
69+
# message.
70+
data = b.replace(b"\x00", b"\xc0\x80")
71+
72+
# Append a marker to partial lines (see PARTIAL_LINE_MARKER).
73+
if not b.endswith(b"\n"):
74+
data += self.PARTIAL_LINE_MARKER
75+
self.log_write(self.level, data)
6576

77+
# Modifications of the changed data should not affect the return value, as
78+
# the caller may be expecting it to match the length of the input.
6679
return len(b)

Lib/test/test_apple.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import unittest
2-
from _apple_support import SystemLog
2+
from _apple_support import LogStream, SystemLog
33
from test.support import is_apple
44
from unittest.mock import Mock, call
55

66
if not is_apple:
77
raise unittest.SkipTest("Apple-specific")
88

9+
MARKER = LogStream.PARTIAL_LINE_MARKER
910

1011
# Test redirection of stdout and stderr to the Apple system log.
1112
class TestAppleSystemLogOutput(unittest.TestCase):
@@ -52,6 +53,21 @@ def test_simple_str(self):
5253

5354
self.assert_writes([b"hello world\n"])
5455

56+
def test_partial_line_marker(self):
57+
self.log.write("complete\n")
58+
self.assert_writes([b"complete\n"])
59+
60+
self.log.write("partial")
61+
self.log.flush()
62+
self.assert_writes([b"partial" + MARKER])
63+
64+
self.log.write("trailing whitespace ")
65+
self.log.flush()
66+
self.assert_writes([b"trailing whitespace " + MARKER])
67+
68+
self.log.write("done\n")
69+
self.assert_writes([b"done\n"])
70+
5571
def test_buffered_str(self):
5672
self.log.write("h")
5773
self.log.write("ello")
@@ -60,7 +76,7 @@ def test_buffered_str(self):
6076
self.log.write("goodbye.")
6177
self.log.flush()
6278

63-
self.assert_writes([b"hello world\n", b"goodbye."])
79+
self.assert_writes([b"hello world\n", b"goodbye." + MARKER])
6480

6581
def test_manual_flush(self):
6682
self.log.write("Hello")
@@ -74,7 +90,7 @@ def test_manual_flush(self):
7490
self.assert_writes([b"Goodbye world\n"])
7591

7692
self.log.flush()
77-
self.assert_writes([b"Hello again"])
93+
self.assert_writes([b"Hello again" + MARKER])
7894

7995
def test_non_ascii(self):
8096
# Spanish
@@ -142,7 +158,7 @@ def test_byteslike_in_buffer(self):
142158
self.log.buffer.write(b"goodbye")
143159
self.log.flush()
144160

145-
self.assert_writes([b"hello", b"goodbye"])
161+
self.assert_writes([b"hello" + MARKER, b"goodbye" + MARKER])
146162

147163
def test_non_byteslike_in_buffer(self):
148164
for obj in ["hello", None, 42]:

Platforms/Apple/testbed/__main__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,23 @@
2424
r"\s+iOSTestbed\[\d+:\w+\] " # Process/thread ID
2525
)
2626

27+
# The system log escapes non-printable bytes using caret notation (e.g. ESC
28+
# becomes "\^["). This regex matches those sequences so they can be restored.
29+
LOG_CTRL_CHAR_REGEX = re.compile(r"\\\^(.)")
30+
31+
# Matches the partial-line marker (ASCII Unit Separator, 0x1f) appended by
32+
# Lib/_apple_support.py to log messages that did not end with a newline,
33+
# followed by the log-appended newline. Removing both causes the next write
34+
# to continue on the same line rather than starting a new one.
35+
LOG_PARTIAL_LINE_REGEX = re.compile(r"\x1f\n$")
36+
37+
38+
def _decode_log_ctrl_char(match):
39+
char = match.group(1)
40+
# Caret notation: "^?" is DEL (0x7f); otherwise XOR the character with
41+
# 0x40 (e.g. "^[" -> ESC 0x1b).
42+
return "\x7f" if char == "?" else chr(ord(char) ^ 0x40)
43+
2744

2845
# Select a simulator device to use.
2946
def select_simulator_device(platform):
@@ -99,6 +116,16 @@ def xcode_test(location: Path, platform: str, simulator: str, verbose: bool):
99116
while line := (process.stdout.readline()).decode(*DECODE_ARGS):
100117
# Strip the timestamp/process prefix from each log line
101118
line = LOG_PREFIX_REGEX.sub("", line)
119+
120+
# Restore control characters (e.g. ANSI color escapes) escaped by the
121+
# system log.
122+
line = LOG_CTRL_CHAR_REGEX.sub(_decode_log_ctrl_char, line)
123+
124+
# A marker immediately before the message's trailing newline means the
125+
# writer did not emit a newline: it is a partial line that should be
126+
# joined with the following message rather than shown on its own line.
127+
line = LOG_PARTIAL_LINE_REGEX.sub("", line)
128+
102129
sys.stdout.write(line)
103130
sys.stdout.flush()
104131

0 commit comments

Comments
 (0)