diff --git a/changes/1992.bugfix.rst b/changes/1992.bugfix.rst new file mode 100644 index 000000000..ea2129d63 --- /dev/null +++ b/changes/1992.bugfix.rst @@ -0,0 +1 @@ +The ``--test`` flag now works for console apps for macOS. diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py index f31b3271f..536327b91 100644 --- a/src/briefcase/platforms/macOS/__init__.py +++ b/src/briefcase/platforms/macOS/__init__.py @@ -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, ) @@ -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, @@ -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( @@ -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 diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py index d82c0dee2..fcd619aba 100644 --- a/tests/platforms/macOS/app/test_run.py +++ b/tests/platforms/macOS/app/test_run.py @@ -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 @@ -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 @@ -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