|
| 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