Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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