Repository: beeware/briefcase
Pull request #929 (2022-10-18 21:44:14)

Show Android emulator troubleshooting steps for CTRL-C
As discussed in #914, this is a less intrusive approach to provide more information when the user interrupts the process to start the Android emulator.

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] All new features have been tested
- [x] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct



Patch:
diff --git a/changes/929.misc.rst b/changes/929.misc.rst
new file mode 100644
index 00000000..64e013e0
--- /dev/null
+++ b/changes/929.misc.rst
@@ -0,0 +1,1 @@
+If the user sends CTRL-C while the Android emulator is starting, the troubleshooting steps are printed.
diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py
index 0972ef84..cbd33eb1 100644
--- a/src/briefcase/integrations/android_sdk.py
+++ b/src/briefcase/integrations/android_sdk.py
@@ -812,7 +812,7 @@ Use the -d/--device option to explicitly specify the device to use.
                 f"""
 In future, you can specify this device by running:
 
-    $ briefcase run android -d @{avd}
+    $ briefcase run android -d "@{avd}"
 """
             )
         elif device:
@@ -999,6 +999,27 @@ In future, you can specify this device by running:
             start_new_session=True,
         )
 
+        # wrap AVD name in quotes since '@' is a special char in PowerShell
+        emulator_command = " ".join(
+            f'"{arg}"' if arg.startswith("@") else arg
+            for arg in map(str, emulator_popen.args)
+        )
+
+        error_msg = (
+            "{prologue}"
+            + f"""
+
+Try starting the emulator manually by running:
+
+    $ {emulator_command}
+
+Resolve any problems you discover, then try running your app again. You may
+find this page helpful in diagnosing emulator problems.
+
+    https://developer.android.com/studio/run/emulator-acceleration#accel-vm
+"""
+        )
+
         # The boot process happens in 2 phases.
         # First, the emulator appears in the device list. However, it's
         # not ready until the boot process has finished. To determine
@@ -1013,18 +1034,9 @@ In future, you can specify this device by running:
                 while adb is None:
                     if emulator_popen.poll() is not None:
                         raise BriefcaseCommandError(
-                            f"""\
-Android emulator was unable to start!
-
-Try starting the emulator manually by running:
-
-    $ {' '.join(str(arg) for arg in emulator_popen.args)}
-
-Resolve any problems you discover, then try running your app again. You may
-find this page helpful in diagnosing emulator problems.
-
-    https://developer.android.com/studio/run/emulator-acceleration#accel-vm
-"""
+                            error_msg.format(
+                                prologue="Android emulator was unable to start!"
+                            )
                         )
 
                     for device, details in sorted(self.devices().items()):
@@ -1053,27 +1065,28 @@ find this page helpful in diagnosing emulator problems.
                     while not adb.has_booted():
                         if emulator_popen.poll() is not None:
                             raise BriefcaseCommandError(
-                                f"""\
-Android emulator was unable to boot!
-
-Try starting the emulator manually by running:
-
-    $ {' '.join(str(arg) for arg in emulator_popen.args)}
-
-Resolve any problems you discover, then try running your app again. You may
-find this page helpful in diagnosing emulator problems.
-
-    https://developer.android.com/studio/run/emulator-acceleration#accel-vm
-"""
+                                error_msg.format(
+                                    prologue="Android emulator was unable to boot!"
+                                )
                             )
 
                         # Try again in 2 seconds...
                         self.sleep(2)
-        except Exception:
+        except BaseException as e:
             # if the emulator exited, this should return its output immediately;
             # if it is still running, this will quickly time out and print nothing.
             with suppress(subprocess.TimeoutExpired):
                 self.tools.logger.info(emulator_popen.communicate(timeout=1)[0])
+
+            # Provide troubleshooting steps if user gives up on the emulator starting
+            if isinstance(e, KeyboardInterrupt):
+                self.tools.logger.warning()
+                self.tools.logger.warning(
+                    error_msg.format(
+                        prologue="Is the Android emulator not starting up properly?"
+                    )
+                )
+
             raise
 
         # Return the device ID and full name.


Test patch:
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_select_target_device.py b/tests/integrations/android_sdk/AndroidSDK/test_select_target_device.py
index e6e80112..3d26a77a 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_select_target_device.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_select_target_device.py
@@ -196,7 +196,7 @@ def test_select_running_emulator(mock_tools, android_sdk, capsys):
 
     # A re-run prompt has been provided
     out = capsys.readouterr().out
-    assert "briefcase run android -d @runningEmulator" in out
+    assert 'briefcase run android -d "@runningEmulator"' in out
 
 
 def test_select_idle_emulator(mock_tools, android_sdk, capsys):
@@ -214,7 +214,7 @@ def test_select_idle_emulator(mock_tools, android_sdk, capsys):
 
     # A re-run prompt has been provided
     out = capsys.readouterr().out
-    assert "briefcase run android -d @idleEmulator" in out
+    assert 'briefcase run android -d "@idleEmulator"' in out
 
 
 def test_select_create_emulator(mock_tools, android_sdk, capsys):
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
index bf508edf..6389fba2 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
@@ -94,7 +94,8 @@ def test_start_emulator(mock_tools, android_sdk):
     ]
 
     # poll() on the process continues to return None, indicating no problem.
-    emu_popen = MagicMock(spec_set=subprocess.Popen)
+    emu_popen = MagicMock(spec=subprocess.Popen)
+    emu_popen.args = [android_sdk.emulator_path, "@idleEmulator"]
     emu_popen.poll.return_value = None
     mock_tools.subprocess.Popen.return_value = emu_popen
 
@@ -167,14 +168,15 @@ def test_start_emulator_fast_start(mock_tools, android_sdk):
         # emu avd_name
         subprocess.CalledProcessError(
             returncode=1, cmd="emu avd name"
-        ),  # phyiscal device
+        ),  # physical device
         "idleEmulator\nOK",  # running emulator
         # shell getprop sys.boot_completed
         "1\n",
     ]
 
     # poll() on the process continues to return None, indicating no problem.
-    emu_popen = MagicMock(spec_set=subprocess.Popen)
+    emu_popen = MagicMock(spec=subprocess.Popen)
+    emu_popen.args = [android_sdk.emulator_path, "@idleEmulator"]
     emu_popen.poll.return_value = None
     mock_tools.subprocess.Popen.return_value = emu_popen
 
@@ -272,12 +274,12 @@ def test_emulator_fail_to_start(mock_tools, android_sdk):
     # poll() on the process returns None for the first two attempts, but then
     # returns 1 indicating failure.
     emu_popen = MagicMock(spec=subprocess.Popen)
-    emu_popen.poll.side_effect = [None, None, 1]
     emu_popen.args = [android_sdk.emulator_path, "@idleEmulator"]
+    emu_popen.poll.side_effect = [None, None, 1]
     mock_tools.subprocess.Popen.return_value = emu_popen
 
     # Start the emulator
-    with pytest.raises(BriefcaseCommandError):
+    with pytest.raises(BriefcaseCommandError) as exc_info:
         android_sdk.start_emulator("idleEmulator")
 
     # The process was started.
@@ -306,6 +308,9 @@ def test_emulator_fail_to_start(mock_tools, android_sdk):
     # Took a total of 2 naps before failing.
     assert android_sdk.sleep.call_count == 2
 
+    # Expected error message was printed
+    assert "Android emulator was unable to start!" in exc_info.value.msg
+
 
 def test_emulator_fail_to_boot(mock_tools, android_sdk):
     """If the emulator fails to boot, and error is displayed."""
@@ -365,6 +370,7 @@ def test_emulator_fail_to_boot(mock_tools, android_sdk):
 
     # poll() on the process returns failure during simulator boot
     emu_popen = MagicMock(spec=subprocess.Popen)
+    emu_popen.args = [android_sdk.emulator_path, "@idleEmulator"]
     emu_popen.poll.side_effect = [
         None,  # in start loop without emulator in device list
         None,  # in start loop without emulator in device list
@@ -377,7 +383,7 @@ def test_emulator_fail_to_boot(mock_tools, android_sdk):
     mock_tools.subprocess.Popen.return_value = emu_popen
 
     # Start the emulator
-    with pytest.raises(BriefcaseCommandError):
+    with pytest.raises(BriefcaseCommandError) as exc_info:
         android_sdk.start_emulator("idleEmulator")
 
     # The process was started.
@@ -412,3 +418,47 @@ def test_emulator_fail_to_boot(mock_tools, android_sdk):
 
     # Took a total of 4 naps.
     assert android_sdk.sleep.call_count == 4
+
+    # Expected error message was printed.
+    assert "Android emulator was unable to boot!" in exc_info.value.msg
+
+
+def test_emulator_ctrl_c(mock_tools, android_sdk, capsys):
+    """If emulator startup is interrupted by the user, an error is
+    displayed."""
+    # Short circuit device loop by returning no devices
+    android_sdk.devices = MagicMock(side_effect=[{}, {}])
+
+    # poll() on the process returns None for the first two attempts, but then
+    # raises KeyboardInterrupt to simulate the user sending CTRL-C
+    emu_popen = MagicMock(spec=subprocess.Popen)
+    emu_popen.args = [android_sdk.emulator_path, "@idleEmulator"]
+    emu_popen.poll.side_effect = [None, None, KeyboardInterrupt]
+    mock_tools.subprocess.Popen.return_value = emu_popen
+
+    # Start the emulator
+    with pytest.raises(KeyboardInterrupt):
+        android_sdk.start_emulator("idleEmulator")
+
+    # The process was started.
+    mock_tools.subprocess.Popen.assert_called_with(
+        [
+            os.fsdecode(android_sdk.emulator_path),
+            "@idleEmulator",
+            "-dns-server",
+            "8.8.8.8",
+        ],
+        env=android_sdk.env,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+        bufsize=1,
+        start_new_session=True,
+    )
+
+    # Took a total of 2 naps before KeyboardInterrupt.
+    assert android_sdk.sleep.call_count == 2
+
+    # Expected error message was printed.
+    assert (
+        "Is the Android emulator not starting up properly?" in capsys.readouterr().out
+    )