Skip to content

Commit

Permalink
Merge pull request #1992 from rmartin16/macos-console-app-testing
Browse files Browse the repository at this point in the history
Allow test mode for console apps on macOS
  • Loading branch information
freakboy3742 authored Sep 11, 2024
2 parents 1a5ec17 + 6f05d94 commit d9e4d2f
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 27 deletions.
1 change: 1 addition & 0 deletions changes/1992.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The ``--test`` flag now works for console apps for macOS.
58 changes: 39 additions & 19 deletions src/briefcase/platforms/macOS/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,11 @@ def run_app(
"""
# Console apps must operate in non-streaming mode so that console input can
# be handled correctly. However, if we're in test mode, we *must* stream so
# that we can see the test exit sentinel
if app.console_app and not test_mode:
# that we can see the test exit sentinel.
if app.console_app:
self.run_console_app(
app,
test_mode=test_mode,
passthrough=passthrough,
**kwargs,
)
Expand All @@ -268,32 +269,51 @@ def run_app(
def run_console_app(
self,
app: AppConfig,
test_mode: bool,
passthrough: list[str],
**kwargs,
):
"""Start the console application.
:param app: The config object for the app
:param test_mode: Boolean; Is the app running in test mode?
:param passthrough: The list of arguments to pass to the app
"""
try:
kwargs = self._prepare_app_kwargs(app=app, test_mode=False)

# Start the app directly
self.logger.info("=" * 75)
self.tools.subprocess.run(
[self.binary_path(app) / "Contents" / "MacOS" / f"{app.formal_name}"]
+ (passthrough if passthrough else []),
sub_kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode)
cmdline = [self.binary_path(app) / f"Contents/MacOS/{app.formal_name}"]
cmdline.extend(passthrough)

if test_mode:
# Stream the app's output for testing.
# When a console app runs normally, its stdout should be connected
# directly to the terminal to properly display the app. When its test
# suite is running, though, Briefcase should stream the output to
# capture the testing outcome.
app_popen = self.tools.subprocess.Popen(
cmdline,
cwd=self.tools.home_path,
check=True,
stream_output=False,
**kwargs,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
**sub_kwargs,
)
self._stream_app_logs(app, popen=app_popen, test_mode=test_mode)

except subprocess.CalledProcessError:
# The command line app *could* returns an error code, which is entirely legal.
# Ignore any subprocess error here.
pass
else:
try:
# Start the app directly
self.logger.info("=" * 75)
self.tools.subprocess.run(
cmdline,
cwd=self.tools.home_path,
check=True,
stream_output=False,
**sub_kwargs,
)
except subprocess.CalledProcessError:
# The command line app *could* returns an error code, which is entirely legal.
# Ignore any subprocess error here.
pass

def run_gui_app(
self,
Expand Down Expand Up @@ -347,7 +367,7 @@ def run_gui_app(
app_pid = None
try:
# Set up the log stream
kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode)
sub_kwargs = self._prepare_app_kwargs(app=app, test_mode=test_mode)

# Start the app in a way that lets us stream the logs
self.tools.subprocess.run(
Expand All @@ -356,7 +376,7 @@ def run_gui_app(
+ ((["--args"] + passthrough) if passthrough else []),
cwd=self.tools.home_path,
check=True,
**kwargs,
**sub_kwargs,
)

# Find the App process ID so log streaming can exit when the app exits
Expand Down
77 changes: 69 additions & 8 deletions tests/platforms/macOS/app/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def run_command(tmp_path):

# To satisfy coverage, the stop function must be invoked
# at least once when streaming app logs.
def mock_stream_app_logs(app, stop_func, **kwargs):
def mock_stream_app_logs(app, stop_func=lambda: 1 + 1, **kwargs):
stop_func()

command._stream_app_logs.side_effect = mock_stream_app_logs
Expand Down Expand Up @@ -242,19 +242,14 @@ def test_run_gui_app_find_pid_failed(
run_command.tools.os.kill.assert_not_called()


@pytest.mark.parametrize("is_console_app", [True, False])
def test_run_app_test_mode(
def test_run_gui_app_test_mode(
run_command,
first_app_config,
is_console_app,
sleep_zero,
tmp_path,
monkeypatch,
):
"""A macOS app can be started in test mode."""
# Test mode is the same regardless of whether it's test mode or not.
first_app_config.console_app = is_console_app

"""A macOS GUI app can be started in test mode."""
# Mock a popen object that represents the log stream
log_stream_process = mock.MagicMock(spec_set=subprocess.Popen)
run_command.tools.subprocess.Popen.return_value = log_stream_process
Expand Down Expand Up @@ -358,6 +353,72 @@ def test_run_console_app_with_passthrough(
run_command._stream_app_logs.assert_not_called()


def test_run_console_app_test_mode(run_command, first_app_config, sleep_zero, tmp_path):
"""A macOS console app can be started in test mode."""
first_app_config.console_app = True

# Mock a popen object that represents the app
app_process = mock.MagicMock(spec_set=subprocess.Popen)
run_command.tools.subprocess.Popen.return_value = app_process

run_command.run_app(first_app_config, test_mode=True, passthrough=[])

# Calls were made to start the app and to start a log stream.
bin_path = run_command.binary_path(first_app_config)
run_command.tools.subprocess.Popen.assert_called_with(
[bin_path / "Contents/MacOS/First App"],
cwd=tmp_path / "home",
env={"BRIEFCASE_MAIN_MODULE": "tests.first_app"},
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
)

# The log stream was not started
run_command._stream_app_logs.assert_called_with(
first_app_config,
popen=app_process,
test_mode=True,
)


def test_run_console_app_test_mode_with_passthrough(
run_command,
first_app_config,
sleep_zero,
tmp_path,
):
"""A macOS console app can be started in test mode with parameters and debug
mode."""
run_command.logger.verbosity = LogLevel.DEBUG

first_app_config.console_app = True

# Mock a popen object that represents the app
app_process = mock.MagicMock(spec_set=subprocess.Popen)
run_command.tools.subprocess.Popen.return_value = app_process

run_command.run_app(first_app_config, test_mode=True, passthrough=["foo", "--bar"])

# Calls were made to start the app and to start a log stream.
bin_path = run_command.binary_path(first_app_config)
run_command.tools.subprocess.Popen.assert_called_with(
[bin_path / "Contents/MacOS/First App", "foo", "--bar"],
cwd=tmp_path / "home",
env={"BRIEFCASE_DEBUG": "1", "BRIEFCASE_MAIN_MODULE": "tests.first_app"},
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=1,
)

# The log stream was not started
run_command._stream_app_logs.assert_called_with(
first_app_config,
popen=app_process,
test_mode=True,
)


def test_run_console_app_failed(run_command, first_app_config, sleep_zero, tmp_path):
"""If there's a problem started a console app, an exception is raised."""
# Set the app to be a console app
Expand Down

0 comments on commit d9e4d2f

Please sign in to comment.