Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/master'
Browse files Browse the repository at this point in the history
  • Loading branch information
iamzili committed Jul 12, 2021
2 parents 8920f86 + eea908c commit 111a8c4
Show file tree
Hide file tree
Showing 16 changed files with 451 additions and 92 deletions.
1 change: 1 addition & 0 deletions changes/591.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
On macOS, iOS, and Android, ``briefcase run`` now displays the application logs once the application has started.
1 change: 1 addition & 0 deletions changes/598.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dropped the Jinja2 version pin as Python 3.5 is no longer supported.
3 changes: 1 addition & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ install_requires =
requests >= 2.22.0
GitPython >= 3.0.8
dmgbuild >= 1.3.3; sys_platform == "darwin"
# Jinja2 3.0 drops support for Python 3.5
Jinja2 < 3.0
Jinja2

[options.packages.find]
where = src
Expand Down
40 changes: 40 additions & 0 deletions src/briefcase/integrations/android_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -951,3 +951,43 @@ def start_app(self, package, activity):
package=package, activity=activity, device=self.device,
)
)

def clear_log(self):
"""
Clear the log for the device.
Returns `None` on success; raises an exception on failure.
"""
try:
# Invoke `adb logcat -c`
self.run("logcat", "-c")
except subprocess.CalledProcessError:
raise BriefcaseCommandError(
"Unable to clear log on {device}".format(
device=self.device,
)
)

def logcat(self):
"""
Start tailing the adb log for the device.
"""
try:
# Using subprocess.run() with no I/O redirection so the user sees
# the full output and can send input.
self.command.subprocess.run(
[
os.fsdecode(self.android_sdk.adb_path),
"-s",
self.device,
"logcat",
"-s",
"MainActivity:*",
"stdio:*",
"Python:*",
],
env=self.android_sdk.env,
check=True,
)
except subprocess.CalledProcessError:
raise BriefcaseCommandError("Error starting ADB logcat.")
11 changes: 11 additions & 0 deletions src/briefcase/platforms/android/gradle.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,22 @@ def run_app(self, app: BaseConfig, device_or_avd=None, **kwargs):
))
adb.install_apk(self.binary_path(app))

print()
print("[{app.app_name}] Clearing device log...".format(
app=app,
))
adb.clear_log()

# To start the app, we launch `org.beeware.android.MainActivity`.
print()
print("[{app.app_name}] Launching app...".format(app=app))
adb.start_app(package, "org.beeware.android.MainActivity")

print()
print("[{app.app_name}] Following device log output (type CTRL-C to stop log)...".format(app=app))
print("=" * 75)
adb.logcat()


class GradlePackageCommand(GradleMixin, PackageCommand):
description = "Create an Android App Bundle and APK in release mode."
Expand Down
20 changes: 20 additions & 0 deletions src/briefcase/platforms/iOS/xcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,26 @@ def run_app(self, app: BaseConfig, udid=None, **kwargs):
)
)

# Start streaming logs for the app.
try:
print()
print("[{app.app_name}] Following simulator log output (type CTRL-C to stop log)...".format(app=app))
print("=" * 75)
self.subprocess.run(
[
"xcrun", "simctl", "spawn", udid,
"log", "stream",
"--style", "compact",
"--predicate", 'senderImagePath ENDSWITH "/{app.formal_name}"'.format(app=app)
],
check=True,
)
except subprocess.CalledProcessError:
print()
raise BriefcaseCommandError(
"Unable to start log stream for app {app.app_name}.".format(app=app)
)

# Preserve the device selection as state.
return {
'udid': udid
Expand Down
65 changes: 64 additions & 1 deletion src/briefcase/platforms/macOS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,71 @@ class macOSMixin:
platform = 'macOS'


class macOSPackageMixin:
class macOSRunMixin:
def run_app(self, app: BaseConfig, **kwargs):
"""
Start the application.
:param app: The config object for the app
:param base_path: The path to the project directory.
"""
print()
print('[{app.app_name}] Starting app...'.format(
app=app
))
try:
self.subprocess.run(
[
'open',
'-n', # Force a new app to be launched
os.fsdecode(self.binary_path(app)),
],
check=True,
)
except subprocess.CalledProcessError:
print()
raise BriefcaseCommandError(
"Unable to start app {app.app_name}.".format(app=app)
)

# Start streaming logs for the app.
try:
print()
print("[{app.app_name}] Following system log output (type CTRL-C to stop log)...".format(app=app))
print("=" * 75)
# Streaming the system log is... a mess. The system log contains a
# *lot* of noise from other processes; even if you filter by
# process, there's a lot of macOS-generated noise. It's very
# difficult to extract just the "user generated" stdout/err log
# messages.
#
# The following sets up a log stream filter that looks for:
# 1. a log sender that matches that app binary; or,
# 2. a log sender of libffi, and a process that matches the app binary.
# Case (1) works for pre-Python 3.9 static linked binaries.
# Case (2) works for Python 3.9+ dynamic linked binaries.
self.subprocess.run(
[
"log",
"stream",
"--style", "compact",
"--predicate",
'senderImagePath=="{sender}"'
' OR (processImagePath=="{sender}"'
' AND senderImagePath=="/usr/lib/libffi.dylib")'.format(
sender=os.fsdecode(self.binary_path(app) / "Contents" / "MacOS" / app.formal_name)
)
],
check=True,
)
except subprocess.CalledProcessError:
print()
raise BriefcaseCommandError(
"Unable to start log stream for app {app.app_name}.".format(app=app)
)


class macOSPackageMixin:
@property
def packaging_formats(self):
return ['app', 'dmg']
Expand Down
32 changes: 2 additions & 30 deletions src/briefcase/platforms/macOS/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import os
import subprocess
import tempfile
from pathlib import Path

Expand All @@ -12,8 +11,7 @@
UpdateCommand
)
from briefcase.config import BaseConfig
from briefcase.exceptions import BriefcaseCommandError
from briefcase.platforms.macOS import macOSMixin, macOSPackageMixin
from briefcase.platforms.macOS import macOSMixin, macOSRunMixin, macOSPackageMixin


class macOSAppMixin(macOSMixin):
Expand Down Expand Up @@ -66,35 +64,9 @@ class macOSAppBuildCommand(macOSAppMixin, BuildCommand):
description = "Build a macOS app."


class macOSAppRunCommand(macOSAppMixin, RunCommand):
class macOSAppRunCommand(macOSRunMixin, macOSAppMixin, RunCommand):
description = "Run a macOS app."

def run_app(self, app: BaseConfig, **kwargs):
"""
Start the application.
:param app: The config object for the app
:param base_path: The path to the project directory.
"""
print()
print('[{app.app_name}] Starting app...'.format(
app=app
))
try:
print()
self.subprocess.run(
[
'open',
os.fsdecode(self.binary_path(app)),
],
check=True,
)
except subprocess.CalledProcessError:
print()
raise BriefcaseCommandError(
"Unable to start app {app.app_name}.".format(app=app)
)


class macOSAppPackageCommand(macOSPackageMixin, macOSAppMixin, PackageCommand):
description = "Package a macOS app for distribution."
Expand Down
25 changes: 2 additions & 23 deletions src/briefcase/platforms/macOS/xcode.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import subprocess

from briefcase.commands import (
Expand All @@ -11,8 +10,8 @@
)
from briefcase.config import BaseConfig
from briefcase.exceptions import BriefcaseCommandError
from briefcase.platforms.macOS import macOSMixin, macOSRunMixin, macOSPackageMixin
from briefcase.integrations.xcode import verify_xcode_install
from briefcase.platforms.macOS import macOSMixin, macOSPackageMixin


class macOSXcodeMixin(macOSMixin):
Expand Down Expand Up @@ -109,29 +108,9 @@ def build_app(self, app: BaseConfig, **kwargs):
)


class macOSXcodeRunCommand(macOSXcodeMixin, RunCommand):
class macOSXcodeRunCommand(macOSRunMixin, macOSXcodeMixin, RunCommand):
description = "Run a macOS app."

def run_app(self, app: BaseConfig, **kwargs):
"""
Start the application.
:param app: The config object for the app
:param base_path: The path to the project directory.
"""
print()
print('[{app.app_name}] Starting app...'.format(
app=app
))
try:
print()
self.subprocess.run(['open', os.fsdecode(self.binary_path(app))], check=True)
except subprocess.CalledProcessError:
print()
raise BriefcaseCommandError(
"Unable to start app {app.app_name}.".format(app=app)
)


class macOSXcodePackageCommand(macOSPackageMixin, macOSXcodeMixin, PackageCommand):
description = "Package a macOS app for distribution."
Expand Down
47 changes: 47 additions & 0 deletions tests/integrations/android_sdk/ADB/test_clear_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import subprocess
from unittest.mock import MagicMock

import pytest

from briefcase.exceptions import BriefcaseCommandError, InvalidDeviceError
from briefcase.integrations.android_sdk import ADB


def test_clear_log(mock_sdk, capsys):
"Invoking `clear_log()` calls `run()` with the appropriate parameters."
# Mock out the run command on an adb instance
adb = ADB(mock_sdk, "exampleDevice")
adb.run = MagicMock(return_value="example normal adb output")

# Invoke clear_log
adb.clear_log()

# Validate call parameters.
adb.run.assert_called_once_with("logcat", "-c")

# Validate that the normal output of the command was not printed (since there
# was no error).
assert "normal adb output" not in capsys.readouterr()


def test_adb_failure(mock_sdk):
"If adb logcat fails, the error is caught."
# Mock out the run command on an adb instance
adb = ADB(mock_sdk, "exampleDevice")
adb.run = MagicMock(side_effect=subprocess.CalledProcessError(
returncode=1, cmd='adb logcat'
))

with pytest.raises(BriefcaseCommandError):
adb.clear_log()


def test_invalid_device(mock_sdk):
"If the device doesn't exist, the error is caught."
# Use real `adb` output from launching an activity that does not exist.
# Mock out the run command on an adb instance
adb = ADB(mock_sdk, "exampleDevice")
adb.run = MagicMock(side_effect=InvalidDeviceError('device', 'exampleDevice'))

with pytest.raises(InvalidDeviceError):
adb.clear_log()
45 changes: 45 additions & 0 deletions tests/integrations/android_sdk/ADB/test_logcat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import os
import subprocess
from unittest.mock import MagicMock

import pytest

from briefcase.exceptions import BriefcaseCommandError
from briefcase.integrations.android_sdk import ADB


def test_logcat(mock_sdk):
"Invoking `logcat()` calls `run()` with the appropriate parameters."
# Mock out the run command on an adb instance
adb = ADB(mock_sdk, "exampleDevice")

# Invoke logcat
adb.logcat()

# Validate call parameters.
mock_sdk.command.subprocess.run.assert_called_once_with(
[
os.fsdecode(mock_sdk.adb_path),
"-s",
"exampleDevice",
"logcat",
"-s",
"MainActivity:*",
"stdio:*",
"Python:*",
],
env=mock_sdk.env,
check=True,
)


def test_adb_failure(mock_sdk):
"If adb logcat fails, the error is caught."
# Mock out the run command on an adb instance
adb = ADB(mock_sdk, "exampleDevice")
mock_sdk.command.subprocess.run = MagicMock(side_effect=subprocess.CalledProcessError(
returncode=1, cmd='adb logcat'
))

with pytest.raises(BriefcaseCommandError):
adb.logcat()
5 changes: 5 additions & 0 deletions tests/platforms/android/gradle/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,18 @@ def test_run_existing_device(run_command, first_app_config):
first_app_config=first_app_config
),
)

run_command.mock_adb.clear_log.assert_called_once()

run_command.mock_adb.start_app.assert_called_once_with(
"{first_app_config.package_name}.{first_app_config.module_name}".format(
first_app_config=first_app_config
),
"org.beeware.android.MainActivity",
)

run_command.mock_adb.logcat.assert_called_once()


def test_run_created_device(run_command, first_app_config):
"If the user chooses to run on a newly created device, an error is raised (for now)"
Expand Down
Loading

0 comments on commit 111a8c4

Please sign in to comment.