|
| 1 | +import io |
1 | 2 | import platform |
2 | 3 | import queue |
3 | 4 | import re |
4 | 5 | import subprocess |
5 | 6 | import sys |
6 | 7 | import unittest |
| 8 | +from _android_support import TextLogStream |
7 | 9 | from array import array |
8 | | -from contextlib import contextmanager |
| 10 | +from contextlib import ExitStack, contextmanager |
9 | 11 | from threading import Thread |
10 | 12 | from test.support import LOOPBACK_TIMEOUT |
11 | | -from time import time |
| 13 | +from time import sleep, time |
| 14 | +from unittest.mock import patch |
12 | 15 |
|
13 | 16 |
|
14 | 17 | if sys.platform != "android": |
@@ -81,18 +84,39 @@ def unbuffered(self, stream): |
81 | 84 | finally: |
82 | 85 | stream.reconfigure(write_through=False) |
83 | 86 |
|
| 87 | + # In --verbose3 mode, sys.stdout and sys.stderr are captured, so we can't |
| 88 | + # test them directly. Detect this mode and use some temporary streams with |
| 89 | + # the same properties. |
| 90 | + def stream_context(self, stream_name, level): |
| 91 | + # https://developer.android.com/ndk/reference/group/logging |
| 92 | + prio = {"I": 4, "W": 5}[level] |
| 93 | + |
| 94 | + stack = ExitStack() |
| 95 | + stack.enter_context(self.subTest(stream_name)) |
| 96 | + stream = getattr(sys, stream_name) |
| 97 | + if isinstance(stream, io.StringIO): |
| 98 | + stack.enter_context( |
| 99 | + patch( |
| 100 | + f"sys.{stream_name}", |
| 101 | + TextLogStream( |
| 102 | + prio, f"python.{stream_name}", errors="backslashreplace" |
| 103 | + ), |
| 104 | + ) |
| 105 | + ) |
| 106 | + return stack |
| 107 | + |
84 | 108 | def test_str(self): |
85 | 109 | for stream_name, level in [("stdout", "I"), ("stderr", "W")]: |
86 | | - with self.subTest(stream=stream_name): |
| 110 | + with self.stream_context(stream_name, level): |
87 | 111 | stream = getattr(sys, stream_name) |
88 | 112 | tag = f"python.{stream_name}" |
89 | 113 | self.assertEqual(f"<TextLogStream '{tag}'>", repr(stream)) |
90 | 114 |
|
91 | | - self.assertTrue(stream.writable()) |
92 | | - self.assertFalse(stream.readable()) |
| 115 | + self.assertIs(stream.writable(), True) |
| 116 | + self.assertIs(stream.readable(), False) |
93 | 117 | self.assertEqual("UTF-8", stream.encoding) |
94 | | - self.assertTrue(stream.line_buffering) |
95 | | - self.assertFalse(stream.write_through) |
| 118 | + self.assertIs(stream.line_buffering, True) |
| 119 | + self.assertIs(stream.write_through, False) |
96 | 120 |
|
97 | 121 | # stderr is backslashreplace by default; stdout is configured |
98 | 122 | # that way by libregrtest.main. |
@@ -147,6 +171,13 @@ def write(s, lines=None, *, write_len=None): |
147 | 171 | write("f\n\ng", ["exxf", ""]) |
148 | 172 | write("\n", ["g"]) |
149 | 173 |
|
| 174 | + # Since this is a line-based logging system, line buffering |
| 175 | + # cannot be turned off, i.e. a newline always causes a flush. |
| 176 | + stream.reconfigure(line_buffering=False) |
| 177 | + self.assertIs(stream.line_buffering, True) |
| 178 | + |
| 179 | + # However, buffering can be turned off completely if you want a |
| 180 | + # flush after every write. |
150 | 181 | with self.unbuffered(stream): |
151 | 182 | write("\nx", ["", "x"]) |
152 | 183 | write("\na\n", ["", "a"]) |
@@ -209,30 +240,30 @@ def __str__(self): |
209 | 240 | # (MAX_BYTES_PER_WRITE). |
210 | 241 | # |
211 | 242 | # ASCII (1 byte per character) |
212 | | - write(("foobar" * 700) + "\n", |
213 | | - [("foobar" * 666) + "foob", # 4000 bytes |
214 | | - "ar" + ("foobar" * 33)]) # 200 bytes |
| 243 | + write(("foobar" * 700) + "\n", # 4200 bytes in |
| 244 | + [("foobar" * 666) + "foob", # 4000 bytes out |
| 245 | + "ar" + ("foobar" * 33)]) # 200 bytes out |
215 | 246 |
|
216 | 247 | # "Full-width" digits 0-9 (3 bytes per character) |
217 | 248 | s = "\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19" |
218 | | - write((s * 150) + "\n", |
219 | | - [s * 100, # 3000 bytes |
220 | | - s * 50]) # 1500 bytes |
| 249 | + write((s * 150) + "\n", # 4500 bytes in |
| 250 | + [s * 100, # 3000 bytes out |
| 251 | + s * 50]) # 1500 bytes out |
221 | 252 |
|
222 | 253 | s = "0123456789" |
223 | | - write(s * 200, []) |
224 | | - write(s * 150, []) |
225 | | - write(s * 51, [s * 350]) # 3500 bytes |
226 | | - write("\n", [s * 51]) # 510 bytes |
| 254 | + write(s * 200, []) # 2000 bytes in |
| 255 | + write(s * 150, []) # 1500 bytes in |
| 256 | + write(s * 51, [s * 350]) # 510 bytes in, 3500 bytes out |
| 257 | + write("\n", [s * 51]) # 0 bytes in, 510 bytes out |
227 | 258 |
|
228 | 259 | def test_bytes(self): |
229 | 260 | for stream_name, level in [("stdout", "I"), ("stderr", "W")]: |
230 | | - with self.subTest(stream=stream_name): |
| 261 | + with self.stream_context(stream_name, level): |
231 | 262 | stream = getattr(sys, stream_name).buffer |
232 | 263 | tag = f"python.{stream_name}" |
233 | 264 | self.assertEqual(f"<BinaryLogStream '{tag}'>", repr(stream)) |
234 | | - self.assertTrue(stream.writable()) |
235 | | - self.assertFalse(stream.readable()) |
| 265 | + self.assertIs(stream.writable(), True) |
| 266 | + self.assertIs(stream.readable(), False) |
236 | 267 |
|
237 | 268 | def write(b, lines=None, *, write_len=None): |
238 | 269 | if write_len is None: |
@@ -330,3 +361,64 @@ def write(b, lines=None, *, write_len=None): |
330 | 361 | fr"{type(obj).__name__}" |
331 | 362 | ): |
332 | 363 | stream.write(obj) |
| 364 | + |
| 365 | + def test_rate_limit(self): |
| 366 | + # https://cs.android.com/android/platform/superproject/+/android-14.0.0_r1:system/logging/liblog/include/log/log_read.h;l=39 |
| 367 | + PER_MESSAGE_OVERHEAD = 28 |
| 368 | + |
| 369 | + # https://developer.android.com/ndk/reference/group/logging |
| 370 | + ANDROID_LOG_DEBUG = 3 |
| 371 | + |
| 372 | + # To avoid flooding the test script output, use a different tag rather |
| 373 | + # than stdout or stderr. |
| 374 | + tag = "python.rate_limit" |
| 375 | + stream = TextLogStream(ANDROID_LOG_DEBUG, tag) |
| 376 | + |
| 377 | + # Make a test message which consumes 1 KB of the logcat buffer. |
| 378 | + message = "Line {:03d} " |
| 379 | + message += "." * ( |
| 380 | + 1024 - PER_MESSAGE_OVERHEAD - len(tag) - len(message.format(0)) |
| 381 | + ) + "\n" |
| 382 | + |
| 383 | + # See _android_support.py. The default values of these parameters work |
| 384 | + # well across a wide range of devices, but we'll use smaller values to |
| 385 | + # ensure a quick and reliable test that doesn't flood the log too much. |
| 386 | + MAX_KB_PER_SECOND = 100 |
| 387 | + BUCKET_KB = 10 |
| 388 | + with ( |
| 389 | + patch("_android_support.MAX_BYTES_PER_SECOND", MAX_KB_PER_SECOND * 1024), |
| 390 | + patch("_android_support.BUCKET_SIZE", BUCKET_KB * 1024), |
| 391 | + ): |
| 392 | + # Make sure the token bucket is full. |
| 393 | + sleep(BUCKET_KB / MAX_KB_PER_SECOND) |
| 394 | + line_num = 0 |
| 395 | + |
| 396 | + # Write BUCKET_KB messages, and return the rate at which they were |
| 397 | + # accepted in KB per second. |
| 398 | + def write_bucketful(): |
| 399 | + nonlocal line_num |
| 400 | + start = time() |
| 401 | + max_line_num = line_num + BUCKET_KB |
| 402 | + while line_num < max_line_num: |
| 403 | + stream.write(message.format(line_num)) |
| 404 | + line_num += 1 |
| 405 | + return BUCKET_KB / (time() - start) |
| 406 | + |
| 407 | + # The first bucketful should be written with minimal delay. The |
| 408 | + # factor of 2 here is not arbitrary: it verifies that the system can |
| 409 | + # write fast enough to empty the bucket within two bucketfuls, which |
| 410 | + # the next part of the test depends on. |
| 411 | + self.assertGreater(write_bucketful(), MAX_KB_PER_SECOND * 2) |
| 412 | + |
| 413 | + # Write another bucketful to empty the token bucket completely. |
| 414 | + write_bucketful() |
| 415 | + |
| 416 | + # The next bucketful should be written at the rate limit. |
| 417 | + self.assertAlmostEqual( |
| 418 | + write_bucketful(), MAX_KB_PER_SECOND, |
| 419 | + delta=MAX_KB_PER_SECOND * 0.1 |
| 420 | + ) |
| 421 | + |
| 422 | + # Once the token bucket refills, we should go back to full speed. |
| 423 | + sleep(BUCKET_KB / MAX_KB_PER_SECOND) |
| 424 | + self.assertGreater(write_bucketful(), MAX_KB_PER_SECOND * 2) |
0 commit comments