Skip to content

Commit

Permalink
Merge pull request #1676 from rmartin16/color-adb
Browse files Browse the repository at this point in the history
ADB log tailing color output
  • Loading branch information
freakboy3742 committed Mar 7, 2024
2 parents eae5368 + 2e34d6f commit b66ee70
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 9 deletions.
1 change: 1 addition & 0 deletions changes/1676.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
When an app runs on an Android device or emulator, the logging output is now colored.
17 changes: 16 additions & 1 deletion src/briefcase/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
SENSITIVE_SETTING_RE = re.compile(r"API|TOKEN|KEY|SECRET|PASS|SIGNATURE", flags=re.I)

# 7-bit C1 ANSI escape sequences
ANSI_ESCAPE_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
ANSI_ESC_SEQ_RE_DEF = r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])"
ANSI_ESCAPE_RE = re.compile(ANSI_ESC_SEQ_RE_DEF)


class InputDisabled(Exception):
Expand Down Expand Up @@ -519,6 +520,20 @@ def is_interactive(self):
# `sys.__stdout__` is used because Rich captures and redirects `sys.stdout`
return sys.__stdout__.isatty()

@property
def is_color_enabled(self):
"""Is the underlying Rich console using color?
Rich can be explicitly configured to not use color at Console initialization or
the NO_COLOR environment variable; alternatively, the derived color system for
the terminal is influenced by attributes of the platform as well as FORCE_COLOR.
"""
# no_color has precedence since color_system can be set even if color is disabled
if self.print.console.no_color:
return False
else:
return self.print.console.color_system is not None

def progress_bar(self):
"""Returns a progress bar as a context manager."""
return Progress(
Expand Down
6 changes: 4 additions & 2 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -1610,7 +1610,8 @@ def logcat(self, pid: str) -> subprocess.Popen:
pid,
]
# Filter out some noisy and useless tags.
+ [f"{tag}:S" for tag in ["EGL_emulation"]],
+ [f"{tag}:S" for tag in ["EGL_emulation"]]
+ (["--format=color"] if self.tools.input.is_color_enabled else []),
env=self.tools.android_sdk.env,
encoding="UTF-8",
stdout=subprocess.PIPE,
Expand Down Expand Up @@ -1643,7 +1644,8 @@ def logcat_tail(self, since: datetime):
"stdio:*",
"python.stdout:*",
"AndroidRuntime:*",
],
]
+ (["--format=color"] if self.tools.input.is_color_enabled else []),
env=self.tools.android_sdk.env,
check=True,
encoding="UTF-8",
Expand Down
7 changes: 6 additions & 1 deletion src/briefcase/platforms/android/gradle.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
UpdateCommand,
)
from briefcase.config import AppConfig, parsed_version
from briefcase.console import ANSI_ESC_SEQ_RE_DEF
from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.android_sdk import AndroidSDK
from briefcase.integrations.subprocess import SubprocessArgT
Expand All @@ -35,7 +36,11 @@ def safe_formal_name(name):
return re.sub(r"\s+", " ", re.sub(r'[!/\\:<>"\?\*\|]', "", name)).strip()


ANDROID_LOG_PREFIX_REGEX = re.compile(r"[A-Z]/(?P<tag>.*?): (?P<content>.*)")
# Matches zero or more ANSI control chars wrapping the message for when
# the Android emulator is printing in color.
ANDROID_LOG_PREFIX_REGEX = re.compile(
rf"(?:{ANSI_ESC_SEQ_RE_DEF})*[A-Z]/(?P<tag>.*?): (?P<content>.*?(?=\x1B|$))(?:{ANSI_ESC_SEQ_RE_DEF})*"
)


def android_log_clean_filter(line):
Expand Down
33 changes: 33 additions & 0 deletions tests/console/Console/test_is_color_enabled.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from unittest.mock import PropertyMock

import pytest
from rich.console import ColorSystem

from briefcase.console import Console, Printer


@pytest.mark.parametrize(
"no_color, color_system, is_enabled",
[
(False, ColorSystem.TRUECOLOR, True),
(False, None, False),
(True, ColorSystem.TRUECOLOR, False),
(True, None, False),
],
)
def test_is_color_enabled(no_color, color_system, is_enabled, monkeypatch):
"""Color is enabled/disabled based on no_color and color_system."""
printer = Printer()
monkeypatch.setattr(
type(printer.console),
"color_system",
PropertyMock(return_value=color_system),
)
printer.console.no_color = no_color
console = Console(printer=printer)

# confirm these values to make sure they didn't change somehow...
assert printer.console.no_color is no_color
assert printer.console.color_system is color_system

assert console.is_color_enabled is is_enabled
15 changes: 13 additions & 2 deletions tests/integrations/android_sdk/ADB/test_logcat.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import subprocess
from unittest import mock

import pytest

def test_logcat(mock_tools, adb):

@pytest.mark.parametrize("is_color_enabled", [True, False])
def test_logcat(mock_tools, adb, is_color_enabled, monkeypatch):
"""Invoking `logcat()` calls `Popen()` with the appropriate parameters."""
# Mock whether color is enabled for the console
monkeypatch.setattr(
type(mock_tools.input),
"is_color_enabled",
mock.PropertyMock(return_value=is_color_enabled),
)

# Mock the result of calling Popen so we can compare against this return value
popen = mock.MagicMock()
mock_tools.subprocess.Popen.return_value = popen
Expand All @@ -22,7 +32,8 @@ def test_logcat(mock_tools, adb):
"--pid",
"1234",
"EGL_emulation:S",
],
]
+ (["--format=color"] if is_color_enabled else []),
env=mock_tools.android_sdk.env,
encoding="UTF-8",
stdout=subprocess.PIPE,
Expand Down
15 changes: 12 additions & 3 deletions tests/integrations/android_sdk/ADB/test_logcat_tail.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import subprocess
from datetime import datetime
from unittest.mock import MagicMock
from unittest.mock import MagicMock, PropertyMock

import pytest

from briefcase.exceptions import BriefcaseCommandError


def test_logcat_tail(mock_tools, adb):
@pytest.mark.parametrize("is_color_enabled", [True, False])
def test_logcat_tail(mock_tools, adb, is_color_enabled, monkeypatch):
"""Invoking `logcat_tail()` calls `run()` with the appropriate parameters."""
# Mock whether color is enabled for the console
monkeypatch.setattr(
type(mock_tools.input),
"is_color_enabled",
PropertyMock(return_value=is_color_enabled),
)

# Invoke logcat_tail with a specific timestamp
adb.logcat_tail(since=datetime(2022, 11, 10, 9, 8, 7))

Expand All @@ -27,7 +35,8 @@ def test_logcat_tail(mock_tools, adb):
"stdio:*",
"python.stdout:*",
"AndroidRuntime:*",
],
]
+ (["--format=color"] if is_color_enabled else []),
env=mock_tools.android_sdk.env,
check=True,
encoding="UTF-8",
Expand Down
39 changes: 39 additions & 0 deletions tests/platforms/android/gradle/test_android_log_clean_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@
False,
),
),
(
"\x1b[32mD/libEGL : loaded /vendor/lib64/egl/libEGL_emulation.so\x1b[0m",
(
"loaded /vendor/lib64/egl/libEGL_emulation.so",
False,
),
),
(
"D/stdio : Could not find platform independent libraries <prefix>",
("Could not find platform independent libraries <prefix>", False),
Expand All @@ -27,28 +34,60 @@
"D/MainActivity: onStart() start",
("onStart() start", False),
),
(
"\x1b[32mD/MainActivity: onStart() start\x1b[0m",
("onStart() start", False),
),
# Python App messages
(
"I/python.stdout: Python app launched & stored in Android Activity class",
("Python app launched & stored in Android Activity class", True),
),
(
"\x1b[32mI/python.stdout: Python app launched & stored in Android Activity class\x1b[0m",
("Python app launched & stored in Android Activity class", True),
),
(
"I/python.stdout: ",
("", True),
),
(
"\x1b[32mI/python.stdout: \x1b[0m",
("", True),
),
(
"\x1b[32m\x1b[98mI/python.stdout: \x1b[32m\x1b[0m",
("", True),
),
(
"\x1b[32m\x1b[98mI/python.stdout: this is colored output\x1b[32m\x1b[0m",
("this is colored output", True),
),
(
"I/python.stderr: test_case (tests.foobar.test_other.TestOtherMethods)",
("test_case (tests.foobar.test_other.TestOtherMethods)", True),
),
(
"\x1b[32mI/python.stderr: test_case (tests.foobar.test_other.TestOtherMethods)\x1b[0m",
("test_case (tests.foobar.test_other.TestOtherMethods)", True),
),
(
"I/python.stderr: ",
("", True),
),
(
"\x1b[32mI/python.stderr: \x1b[0m",
("", True),
),
# Unknown content
(
"This doesn't match the regex",
("This doesn't match the regex", False),
),
(
"\x1b[33mThis doesn't match the regex\x1b[33m",
("\x1b[33mThis doesn't match the regex\x1b[33m", False),
),
],
)
def test_filter(original, filtered):
Expand Down

0 comments on commit b66ee70

Please sign in to comment.