Repository: ARMmbed/mbed-tools

Issue:
BSD: `mbed-tools detect` causes Python stack trace from mbed_tools/devices 
### Description

Desired behavior:
-  `mbed-tools detect` lists out USB-connected boards, or a friendly message if it can't

Actual behavior:
- `mbed-tools detect` causes a Python stack trace to be printed

```
[mbedtools] patater@example.com:~ % mbed-tools detect
Traceback (most recent call last):
  File "/home/patater/venvs/mbedtools/bin/mbed-tools", line 8, in <module>
    sys.exit(cli())
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/click/core.py", line 829, in __call__
    return self.main(*args, **kwargs)
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/click/core.py", line 782, in main
    rv = self.invoke(ctx)
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/mbed_tools/cli/main.py", line 38, in invoke
    super().invoke(context)
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/click/core.py", line 1259, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/click/core.py", line 1066, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/click/core.py", line 610, in invoke
    return callback(*args, **kwargs)
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/mbed_tools/cli/list_connected_devices.py", line 29, in list_connected_devices
    connected_devices = get_connected_devices()
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/mbed_tools/devices/devices.py", line 32, in get_connected_devices
    for candidate_device in detect_candidate_devices():
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/mbed_tools/devices/_internal/detect_candidate_devices.py", line 16, in detect_candidate_devices
    return detector.find_candidates()
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/mbed_tools/devices/_internal/darwin/device_detector.py", line 40, in find_candidates
    usb_devices_data = system_profiler.get_end_usb_devices_data()
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/mbed_tools/devices/_internal/darwin/system_profiler.py", line 42, in get_end_usb_devices_data
    data = get_all_usb_devices_data()
  File "/home/patater/venvs/mbedtools/lib/python3.7/site-packages/mbed_tools/devices/_internal/darwin/system_profiler.py", line 34, in get_all_usb_devices_data
    output = subprocess.check_output(["system_profiler", "-xml", "SPUSBDataType"], stderr=subprocess.DEVNULL)
  File "/usr/local/lib/python3.7/subprocess.py", line 411, in check_output
    **kwargs).stdout
  File "/usr/local/lib/python3.7/subprocess.py", line 488, in run
    with Popen(*popenargs, **kwargs) as process:
  File "/usr/local/lib/python3.7/subprocess.py", line 800, in __init__
    restore_signals, start_new_session)
  File "/usr/local/lib/python3.7/subprocess.py", line 1551, in _execute_child
    raise child_exception_type(errno_num, err_msg, err_filename)
FileNotFoundError: [Errno 2] No such file or directory: 'system_profiler': 'system_profiler'
```

It looks like something called `darwin/device_detector.py` is being used, but my host OS is not Darwin or macOS. It's just your friendly, neighborhood FreeBSD. I'd guess the OS detection is not very sophisticated.

```
[mbedtools] patater@example.com:~ % mbed-tools --version
5.4.0
```

### Issue request type

- [ ] Enhancement
- [X] Bug


Comments:

Patch:
diff --git a/news/20210218112043.bugfix b/news/20210218112043.bugfix
new file mode 100644
index 0000000..f595eac
--- /dev/null
+++ b/news/20210218112043.bugfix
@@ -0,0 +1,1 @@
+Raise a nicer error from mbed-tools detect when running on an unrecognised OS.
diff --git a/src/mbed_tools/devices/_internal/detect_candidate_devices.py b/src/mbed_tools/devices/_internal/detect_candidate_devices.py
index 7078c4f..418cbc2 100644
--- a/src/mbed_tools/devices/_internal/detect_candidate_devices.py
+++ b/src/mbed_tools/devices/_internal/detect_candidate_devices.py
@@ -8,6 +8,7 @@ from typing import Iterable
 
 from mbed_tools.devices._internal.candidate_device import CandidateDevice
 from mbed_tools.devices._internal.base_detector import DeviceDetector
+from mbed_tools.devices.exceptions import UnknownOSError
 
 
 def detect_candidate_devices() -> Iterable[CandidateDevice]:
@@ -26,7 +27,12 @@ def _get_detector_for_current_os() -> DeviceDetector:
         from mbed_tools.devices._internal.linux.device_detector import LinuxDeviceDetector
 
         return LinuxDeviceDetector()
-    else:
+    if platform.system() == "Darwin":
         from mbed_tools.devices._internal.darwin.device_detector import DarwinDeviceDetector
 
         return DarwinDeviceDetector()
+
+    raise UnknownOSError(
+        f"We have detected the OS you are running is '{platform.system()}'. "
+        "Unfortunately we haven't implemented device detection support for this OS yet. Sorry!"
+    )
diff --git a/src/mbed_tools/devices/exceptions.py b/src/mbed_tools/devices/exceptions.py
index 4763b88..570941d 100644
--- a/src/mbed_tools/devices/exceptions.py
+++ b/src/mbed_tools/devices/exceptions.py
@@ -16,3 +16,7 @@ class DeviceLookupFailed(MbedDevicesError):
 
 class NoDevicesFound(MbedDevicesError):
     """No Mbed Enabled devices were found."""
+
+
+class UnknownOSError(MbedDevicesError):
+    """The current OS is not supported."""


Test patch:
diff --git a/tests/devices/_internal/test_detect_candidate_devices.py b/tests/devices/_internal/test_detect_candidate_devices.py
index 75c5032..74137b3 100644
--- a/tests/devices/_internal/test_detect_candidate_devices.py
+++ b/tests/devices/_internal/test_detect_candidate_devices.py
@@ -2,39 +2,49 @@
 # Copyright (c) 2020-2021 Arm Limited and Contributors. All rights reserved.
 # SPDX-License-Identifier: Apache-2.0
 #
-from unittest import TestCase, mock
+import pytest
+from unittest import mock
 
 from tests.devices.markers import windows_only, darwin_only, linux_only
 from mbed_tools.devices._internal.base_detector import DeviceDetector
+from mbed_tools.devices.exceptions import UnknownOSError
 from mbed_tools.devices._internal.detect_candidate_devices import (
     detect_candidate_devices,
     _get_detector_for_current_os,
 )
 
 
-class TestDetectCandidateDevices(TestCase):
+class TestDetectCandidateDevices:
     @mock.patch("mbed_tools.devices._internal.detect_candidate_devices._get_detector_for_current_os")
     def test_returns_candidates_using_os_specific_detector(self, _get_detector_for_current_os):
         detector = mock.Mock(spec_set=DeviceDetector)
         _get_detector_for_current_os.return_value = detector
-        self.assertEqual(detect_candidate_devices(), detector.find_candidates.return_value)
+        assert detect_candidate_devices() == detector.find_candidates.return_value
 
 
-class TestGetDetectorForCurrentOS(TestCase):
+class TestGetDetectorForCurrentOS:
     @windows_only
     def test_windows_uses_correct_module(self):
         from mbed_tools.devices._internal.windows.device_detector import WindowsDeviceDetector
 
-        self.assertIsInstance(_get_detector_for_current_os(), WindowsDeviceDetector)
+        assert isinstance(_get_detector_for_current_os(), WindowsDeviceDetector)
 
     @darwin_only
     def test_darwin_uses_correct_module(self):
         from mbed_tools.devices._internal.darwin.device_detector import DarwinDeviceDetector
 
-        self.assertIsInstance(_get_detector_for_current_os(), DarwinDeviceDetector)
+        assert isinstance(_get_detector_for_current_os(), DarwinDeviceDetector)
 
     @linux_only
     def test_linux_uses_correct_module(self):
         from mbed_tools.devices._internal.linux.device_detector import LinuxDeviceDetector
 
-        self.assertIsInstance(_get_detector_for_current_os(), LinuxDeviceDetector)
+        assert isinstance(_get_detector_for_current_os(), LinuxDeviceDetector)
+
+    @mock.patch("platform.system")
+    def test_raises_when_os_is_unknown(self, platform_system):
+        os_name = "SomethingNobodyUses"
+        platform_system.return_value = os_name
+
+        with pytest.raises(UnknownOSError):
+            _get_detector_for_current_os()