1515from asyncio import wait_for
1616from contextlib import asynccontextmanager
1717from datetime import datetime , timezone
18+ from enum import IntEnum , auto
1819from glob import glob
1920from os .path import abspath , basename , relpath
2021from pathlib import Path
6162hidden_output = []
6263
6364
65+ # Based on android/log.h in the NDK.
66+ class LogPriority (IntEnum ):
67+ UNKNOWN = 0
68+ DEFAULT = auto ()
69+ VERBOSE = auto ()
70+ DEBUG = auto ()
71+ INFO = auto ()
72+ WARN = auto ()
73+ ERROR = auto ()
74+ FATAL = auto ()
75+ SILENT = auto ()
76+
77+
6478def log_verbose (context , line , stream = sys .stdout ):
6579 if context .verbose :
6680 stream .write (line )
@@ -505,47 +519,47 @@ async def logcat_task(context, initial_devices):
505519 pid = await wait_for (find_pid (serial ), startup_timeout )
506520
507521 # `--pid` requires API level 24 or higher.
508- args = [adb , "-s" , serial , "logcat" , "--pid" , pid , "--format" , "tag" ]
522+ #
523+ # `--binary` mode is used in order to detect which messages end with a
524+ # newline, which most of the other modes don't indicate (except `--format
525+ # long`). For example, every time pytest runs a test, it prints a "." and
526+ # flushes the stream. Each "." becomes a separate log message, but we should
527+ # show them all on the same line.
528+ args = [adb , "-s" , serial , "logcat" , "--pid" , pid , "--binary" ]
509529 logcat_started = False
510530 async with async_process (
511- * args , stdout = subprocess .PIPE , stderr = subprocess . STDOUT ,
531+ * args , stdout = subprocess .PIPE , stderr = None
512532 ) as process :
513- while line := (await process .stdout .readline ()).decode (* DECODE_ARGS ):
514- if match := re .fullmatch (r"([A-Z])/(.*)" , line , re .DOTALL ):
533+ while True :
534+ try :
535+ priority , tag , message = await read_logcat (process .stdout )
515536 logcat_started = True
516- level , message = match .groups ()
517- else :
518- # If the regex doesn't match, this is either a logcat startup
519- # error, or the second or subsequent line of a multi-line
520- # message. Python won't produce multi-line messages, but other
521- # components might.
522- level , message = None , line
537+ except asyncio .IncompleteReadError :
538+ break
523539
524540 # Exclude high-volume messages which are rarely useful.
525541 if context .verbose < 2 and "from python test_syslog" in message :
526542 continue
527543
528544 # Put high-level messages on stderr so they're highlighted in the
529545 # buildbot logs. This will include Python's own stderr.
530- stream = (
531- sys .stderr
532- if level in ["W" , "E" , "F" ] # WARNING, ERROR, FATAL (aka ASSERT)
533- else sys .stdout
534- )
535-
536- # To simplify automated processing of the output, e.g. a buildbot
537- # posting a failure notice on a GitHub PR, we strip the level and
538- # tag indicators from Python's stdout and stderr.
539- for prefix in ["python.stdout: " , "python.stderr: " ]:
540- if message .startswith (prefix ):
541- global python_started
542- python_started = True
543- stream .write (message .removeprefix (prefix ))
544- break
546+ stream = sys .stderr if priority >= LogPriority .WARN else sys .stdout
547+
548+ # The app's stdout and stderr should be passed through transparently
549+ # to our own corresponding streams.
550+ if tag in ["python.stdout" , "python.stderr" ]:
551+ global python_started
552+ python_started = True
553+ stream .write (message )
554+ stream .flush ()
545555 else :
546556 # Non-Python messages add a lot of noise, but they may
547- # sometimes help explain a failure.
548- log_verbose (context , line , stream )
557+ # sometimes help explain a failure. Format them in the same way
558+ # as `logcat --format tag`.
559+ formatted = f"{ priority .name [0 ]} /{ tag } : { message } "
560+ if not formatted .endswith ("\n " ):
561+ formatted += "\n "
562+ log_verbose (context , formatted , stream )
549563
550564 # If the device disconnects while logcat is running, which always
551565 # happens in --managed mode, some versions of adb return non-zero.
@@ -556,6 +570,44 @@ async def logcat_task(context, initial_devices):
556570 raise CalledProcessError (status , args )
557571
558572
573+ # Read one binary log message from the given StreamReader. The message format is
574+ # described at https://android.stackexchange.com/a/74660. All supported versions
575+ # of Android use format version 2 or later.
576+ async def read_logcat (stream ):
577+ async def read_bytes (size ):
578+ return await stream .readexactly (size )
579+
580+ async def read_int (size ):
581+ return int .from_bytes (await read_bytes (size ), "little" )
582+
583+ payload_len = await read_int (2 )
584+ if payload_len < 2 :
585+ # 1 byte for priority, 1 byte for null terminator of tag.
586+ raise ValueError (f"payload length { payload_len } is too short" )
587+
588+ header_len = await read_int (2 )
589+ if header_len < 4 :
590+ raise ValueError (f"header length { header_len } is too short" )
591+ await read_bytes (header_len - 4 ) # Ignore other header fields.
592+
593+ priority_int = await read_int (1 )
594+ try :
595+ priority = LogPriority (priority_int )
596+ except ValueError :
597+ priority = LogPriority .UNKNOWN
598+
599+ payload_fields = (await read_bytes (payload_len - 1 )).split (b"\0 " )
600+ if len (payload_fields ) < 2 :
601+ raise ValueError (
602+ f"payload { payload !r} does not contain at least 2 "
603+ f"null-separated fields"
604+ )
605+ tag , message , * _ = [
606+ field .decode (* DECODE_ARGS ) for field in payload_fields
607+ ]
608+ return priority , tag , message
609+
610+
559611def stop_app (serial ):
560612 run ([adb , "-s" , serial , "shell" , "am" , "force-stop" , APP_ID ], log = False )
561613
0 commit comments