Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow test mode for console apps on macOS #1992

Merged
merged 1 commit into from
Sep 11, 2024
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/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