Ejemplo n.º 1
0
def _validate_extension_package(package: types.ModuleType,
                                package_name: str) -> None:
  """Checks that the extension package is valid.

  Args:
    package: Extension package to check.
    package_name: Name of the extension package.

  Raises:
    PackageRegistrationError: Package has already been registered, or a required
      attribute is missing or invalid.
  """
  if package_name in extensions.package_info:
    raise errors.PackageRegistrationError(
        f"Package {package_name!r} has already been registered.",
        package_name=package_name)

  package_version = getattr(package, "__version__", None)
  if not package_version or not isinstance(package_version, str):
    raise errors.PackageRegistrationError(
        f"Expected __version__ to be a string, found {package_version}.",
        package_name=package_name)

  missing_functions = [
      func for func in ("download_key", "export_extensions")
      if not hasattr(package, func) or not callable(getattr(package, func))
  ]
  if missing_functions:
    raise errors.PackageRegistrationError(
        f"Package must define functions {missing_functions}.",
        package_name=package_name)
Ejemplo n.º 2
0
def _validate_device_classes(ext_auxiliary_devices: Iterable[Type[Any]],
                             ext_primary_devices: Iterable[Type[Any]],
                             ext_virtual_devices: Iterable[Type[Any]],
                             package_name: str) -> None:
  """Validates the extension device classes.

  Args:
    ext_auxiliary_devices: Auxiliary device classes to validate.
    ext_primary_devices: Primary device classes to validate.
    ext_virtual_devices: Virtual device classes to validate.
    package_name: Name of the package providing the extension classes.

  Raises:
    PackageRegistrationError: Device classes are invalid.
  """
  _assert_subclasses(ext_auxiliary_devices, _AuxiliaryDeviceBase, package_name,
                     "auxiliary device")
  _assert_subclasses(ext_primary_devices, _PrimaryDeviceBase, package_name,
                     "primary device")
  _assert_subclasses(ext_virtual_devices, _VirtualDeviceBase, package_name,
                     "virtual device")

  new_device_classes = (ext_auxiliary_devices
                        + ext_primary_devices
                        + ext_virtual_devices)
  known_device_classes = (extensions.auxiliary_devices
                          + extensions.primary_devices
                          + extensions.virtual_devices)
  new_device_types = tuple(device_class.DEVICE_TYPE
                           for device_class in new_device_classes)
  _assert_unique(new_device_types,
                 names_description="Device types",
                 classes_description="device classes",
                 package_name=package_name)
  redefined_device_types = set.intersection(
      {device_class.DEVICE_TYPE for device_class in new_device_classes},
      {device_class.DEVICE_TYPE for device_class in known_device_classes})
  if redefined_device_types:
    raise errors.PackageRegistrationError(
        f"Device types {redefined_device_types} are already defined in GDM.",
        package_name=package_name)

  conformance_issues = _get_device_class_conformance_issues(new_device_classes)
  if conformance_issues:
    issue_messages = []
    for cls, issues in conformance_issues:
      issue_message = "".join(f"\n\t{issue}" for issue in issues)
      issue_messages.append(f"{cls}{issue_message}")
    raise errors.PackageRegistrationError(
        "The following device class(es) are incompliant with GDM "
        "architecture:\n{}".format("\n".join(issue_messages)),
        package_name=package_name)
Ejemplo n.º 3
0
def _validate_detect_criteria(
    ext_detect_criteria: Mapping[str, _DetectQueryMapping],
    ext_communication_types: Collection[Type[Any]],
    package_name: str) -> None:
  """Validates the extension detection criteria.

  Args:
    ext_detect_criteria: Detection criteria to validate, where mapping keys are
      communication type names, and values are detection query mappings for each
      communication type.
    ext_communication_types: Communication types exported by the package.
    package_name: Name of the package providing the extensions.

  Raises:
    PackageRegistrationError: Detection criteria are invalid.
  """
  extension_comm_type_names = [comm_type.__name__
                               for comm_type in ext_communication_types]
  for comm_type_name, query_dict in ext_detect_criteria.items():
    for query_name, query in query_dict.items():
      base_error = ("Unable to register query {} for communication type {!r}. "
                    .format(query_name, comm_type_name))
      if not isinstance(query_name, detect_criteria.QueryEnum):
        raise errors.PackageRegistrationError(
            base_error + "Detection query keys must be {} instances.".format(
                detect_criteria.QueryEnum.__name__),
            package_name=package_name)
      if (not callable(query)
          or tuple(inspect.getfullargspec(query).args) != _EXPECTED_QUERY_ARGS):
        extra_error = ("Detection queries must be callable functions which "
                       "accept {} arguments: {}."
                       .format(len(_EXPECTED_QUERY_ARGS), _EXPECTED_QUERY_ARGS))
        raise errors.PackageRegistrationError(base_error + extra_error,
                                              package_name=package_name)

    if comm_type_name not in extensions.detect_criteria:
      if comm_type_name not in extension_comm_type_names:
        raise errors.PackageRegistrationError(
            "Unable to register detection criteria for communication type "
            f"{comm_type_name!r} as it has not been exported by the package.",
            package_name=package_name)
    else:
      redefined_queries = list(
          extensions.detect_criteria[comm_type_name].keys()
          & query_dict.keys())
      if redefined_queries:
        raise errors.PackageRegistrationError(
            f"Detection queries {redefined_queries} for communication type "
            f"{comm_type_name!r} are already defined in GDM.",
            package_name=package_name)
Ejemplo n.º 4
0
def _validate_manager_cli_mixin(manager_cli_mixin: Optional[Type[Any]],
                                package_name: str) -> None:
  """Validates the provided FireManager mixin.

  Args:
    manager_cli_mixin: None or a class object inheriting from FireManager.
    package_name: Name of the extension package.

  Raises:
    PackageRegistrationError: The provided FireManager mixin is invalid.
  """
  if manager_cli_mixin is None:
    return

  if inspect.isclass(manager_cli_mixin):  # Non-classes don't have __mro__.
    # fire_manager is not imported for a subclass check to prevent a circular
    # import.
    mro_class_names = [
        a_class.__name__ for a_class in manager_cli_mixin.__mro__]
    is_invalid = (inspect.isabstract(manager_cli_mixin)
                  or "FireManager" not in mro_class_names)
  else:
    is_invalid = True

  if is_invalid:
    raise errors.PackageRegistrationError(
        "Provided FireManager mixin class is invalid. It must be None or a "
        "concrete class object which inherits from gazoo_device.FireManager.",
        package_name=package_name)
Ejemplo n.º 5
0
def _validate_keys(keys: Iterable[data_types.KeyInfo],
                   package_name: str) -> None:
  """Validates the extension keys.

  Args:
    keys: Keys provided by the extension package.
    package_name: Name of the extension package.

  Raises:
    PackageRegistrationError: Provided keys are invalid.
  """
  if not all(isinstance(key, data_types.KeyInfo) for key in keys):
    raise errors.PackageRegistrationError(
        "Keys must be data_types.KeyInfo instances.",
        package_name=package_name)
  if not all(key.package == package_name for key in keys):
    raise errors.PackageRegistrationError(
        "KeyInfo.package attribute must match the name of the package "
        f"({package_name!r}).", package_name=package_name)
Ejemplo n.º 6
0
def _validate_capability_flavors(ext_capability_flavors: Iterable[Type[Any]],
                                 package_name: str) -> None:
  """Validates the extension capability flavors.

  Args:
    ext_capability_flavors: Capability flavor classes to validate.
    package_name: Name of the package providing the extension classes.

  Raises:
    PackageRegistrationError: Capability flavor classes are invalid.
  """
  _assert_subclasses(ext_capability_flavors, _CapabilityBase, package_name,
                     "capability flavor")
  new_capability_flavors = {
      common_utils.generate_name(flavor): flavor
      for flavor in ext_capability_flavors
  }
  _assert_unique(new_capability_flavors.keys(),
                 names_description="Capability flavor names",
                 classes_description="capability flavors",
                 package_name=package_name)
  _raise_if_redefined(new_classes=new_capability_flavors,
                      old_classes=extensions.capability_flavors,
                      classes_description="capability flavors",
                      package_name=package_name)

  _assert_unique((list(new_capability_flavors.keys())
                  + list(extensions.capability_interfaces.keys())
                  + list(extensions.capabilities.keys())),
                 names_description="Capability flavor and (capability "
                                   "interface or capability) names",
                 classes_description="capability flavors and (capability "
                                     "interfaces or capabilities)",
                 package_name=package_name)

  conformance_issues = _get_capability_flavor_conformance_issues(
      ext_capability_flavors)
  if conformance_issues:
    issue_messages = []
    for cls, issues in conformance_issues:
      issue_message = "".join(f"\n\t{issue}" for issue in issues)
      issue_messages.append(f"{cls}{issue_message}")
    raise errors.PackageRegistrationError(
        "The following capability class(es) are incompliant with GDM "
        "architecture:\n{}".format("\n".join(issue_messages)),
        package_name=package_name)
Ejemplo n.º 7
0
def _assert_subclasses(classes: Iterable[Type[Any]],
                       parent: Type[Any],
                       package_name: str,
                       class_description: str,
                       allow_abstract: bool = False) -> None:
  """Raises an error if classes are not (concrete) subclasses of parent.

  Args:
    classes: Iterable of class objects to check.
    parent: The class object which should be the parent for all of classes.
    package_name: Name of the package being registered.
    class_description: Description of the classes being registered.
      For example, "primary device".
    allow_abstract: If False, all of classes must be concrete. Otherwise,
      abstract classes are allowed.

  Raises:
    PackageRegistrationError: One or more classes are invalid (not a class
      object, not a subclass of parent, or abstract when allow_abstract=False).
  """
  error_messages = []
  not_classes = [a_class for a_class in classes if not inspect.isclass(a_class)]
  if not_classes:
    error_messages.append(f"{not_classes} must be class objects.")

  not_subclasses = [  # issubclass() only works on class objects.
      a_class for a_class in classes
      if inspect.isclass(a_class) and not issubclass(a_class, parent)]
  if not_subclasses:
    error_messages.append(
        f"{not_subclasses} must be subclasses of {parent.__name__}.")

  if not allow_abstract:
    abstract_classes = [a_class for a_class in classes
                        if inspect.isabstract(a_class)]
    if abstract_classes:
      error_messages.append(f"{abstract_classes} must not be abstract.")

  if error_messages:
    raise errors.PackageRegistrationError(
        "Provided {} classes are invalid. {}".format(class_description,
                                                     " ".join(error_messages)),
        package_name=package_name)
Ejemplo n.º 8
0
def _assert_unique(names: Iterable[str],
                   names_description: str,
                   classes_description: str,
                   package_name: str) -> None:
  """Raises an error if the names are not unique.

  Args:
    names: Class names to validate.
    names_description: Description of the class names.
    classes_description: Desciption of the classes.
    package_name: Name of the package being registered.

  Raises:
    PackageRegistrationError: Class names are not unique.
  """
  name_counts = collections.Counter(names)
  duplicate_names = [name for name, count in name_counts.items() if count > 1]
  if duplicate_names:
    raise errors.PackageRegistrationError(
        f"{names_description} {duplicate_names} are used by multiple "
        f"{classes_description}. {names_description} must be unique.",
        package_name=package_name)
Ejemplo n.º 9
0
def _raise_if_redefined(new_classes: Mapping[str, Type[Any]],
                        old_classes: Mapping[str, Type[Any]],
                        classes_description: str,
                        package_name: str) -> None:
  """Raises if the new class dictionary redefines items in the old one.

  Args:
    new_classes: New class name -> new class object mapping.
    old_classes: Known class name -> known class object mapping.
    classes_description: Human-readable description of the classes.
    package_name: Name of the package being registered.

  Raises:
    PackageRegistrationError: Some of the new classes share names with the
      already known classes.
  """
  redefined_names = list(old_classes.keys() & new_classes.keys())
  if redefined_names:
    offending = [new_classes[if_name] for if_name in redefined_names]
    redefined = [old_classes[if_name] for if_name in redefined_names]
    raise errors.PackageRegistrationError(
        f"New {classes_description} {offending} have same names "
        f"({redefined_names}) as existing {classes_description} {redefined}.",
        package_name=package_name)
Ejemplo n.º 10
0
class ParallelUtilsUnitTests(unit_test_case.UnitTestCase):
  """Unit tests for parallel_utils. Parallel processes are mocked."""

  @parameterized.named_parameters(
      ("factory_reset_success", parallel_utils.factory_reset, (), {}, False),
      ("factory_reset_failure", parallel_utils.factory_reset, (), {}, True),
      ("reboot_success", parallel_utils.reboot, (False,), {"method": "shell"},
       False),
      ("reboot_failure", parallel_utils.reboot, (False,), {"method": "shell"},
       True),
      ("upgrade_success", parallel_utils.upgrade, (),
       {"build_file": "/some/file", "forced_upgrade": False}, False),
      ("upgrade_failure", parallel_utils.upgrade, (),
       {"build_file": "/some/file", "forced_upgrade": False}, True))
  def test_convenience_parallel_function(
      self, function, method_args, method_kwargs, raises):
    """Tests one of the provided convenience parallel functions."""
    mock_manager = mock.MagicMock(spec=manager.Manager)
    mock_device = mock.MagicMock(spec=gazoo_device_base.GazooDeviceBase)
    mock_device.flash_build = mock.MagicMock(flash_build_base.FlashBuildBase)
    mock_device.name = "device-1234"
    mock_manager.create_device.return_value = mock_device

    if function is parallel_utils.factory_reset:
      device_method = mock_device.factory_reset
    elif function is parallel_utils.reboot:
      device_method = mock_device.reboot
    else:
      device_method = mock_device.flash_build.upgrade

    if raises:
      device_method.side_effect = errors.DeviceError("Failed")
      with self.assertRaisesRegex(errors.DeviceError, "Failed"):
        function(mock_manager, mock_device.name, *method_args, **method_kwargs)
    else:
      device_method.return_value = None
      self.assertIsNone(
          function(
              mock_manager, mock_device.name, *method_args, **method_kwargs))

    mock_manager.create_device.assert_called_once_with(mock_device.name)
    device_method.assert_called_once_with(*method_args, **method_kwargs)
    mock_device.close.assert_called_once()

  @mock.patch.object(gdm_logger, "initialize_child_process_logging")
  @mock.patch.object(gdm_logger, "get_logger")
  @mock.patch.object(package_registrar, "register")
  @mock.patch.object(importlib, "import_module")
  @mock.patch.object(manager, "Manager")
  def test_process_wrapper_successful_call(
      self, mock_manager_class, mock_import, mock_register, mock_get_logger,
      mock_initialize_logging):
    """Tests _process_wrapper for a process where there are no errors."""
    mock_manager = mock_manager_class.return_value
    mock_logger = mock_get_logger.return_value
    multiprocessing_queue = multiprocessing_utils.get_context().Queue()
    return_queue = mock.MagicMock(spec=multiprocessing_queue)
    error_queue = mock.MagicMock(spec=multiprocessing_queue)
    logging_queue = mock.MagicMock(spec=multiprocessing_queue)
    process_id = "1"
    mock_function = mock.MagicMock()
    mock_function.__name__ = "mock_function"
    args = (1, 2)
    kwargs = {"foo": "bar"}
    parallel_utils._process_wrapper(
        return_queue=return_queue,
        error_queue=error_queue,
        logging_queue=logging_queue,
        process_id=process_id,
        extension_package_import_paths=["foo.package", "bar.package"],
        call_spec=parallel_utils.CallSpec(mock_function, *args, **kwargs))

    mock_initialize_logging.assert_called_once_with(logging_queue)
    mock_get_logger.assert_called_once()
    mock_logger.debug.assert_called()
    mock_import.assert_has_calls(
        [mock.call("foo.package"), mock.call("bar.package")])
    self.assertEqual(mock_register.call_count, 2)
    mock_manager_class.assert_called_once()
    mock_function.assert_called_once_with(mock_manager, *args, **kwargs)
    return_queue.put.assert_called_once_with(
        (process_id, mock_function.return_value))
    error_queue.put.assert_not_called()
    mock_manager.close.assert_called_once()

  @mock.patch.object(gdm_logger, "initialize_child_process_logging")
  @mock.patch.object(gdm_logger, "get_logger")
  @mock.patch.object(
      package_registrar,
      "register",
      side_effect=errors.PackageRegistrationError(
          "Registration failed", "foo.package"))
  @mock.patch.object(importlib, "import_module", side_effect=[
      None, ImportError("Importing bar.package failed")])
  @mock.patch.object(manager, "Manager")
  def test_process_wrapper_exception_call(
      self, mock_manager_class, mock_import, mock_register, mock_get_logger,
      mock_initialize_logging):
    """Tests _process_wrapper for a process where function raises an error."""
    mock_manager = mock_manager_class.return_value
    mock_logger = mock_get_logger.return_value
    multiprocessing_queue = multiprocessing_utils.get_context().Queue()
    return_queue = mock.MagicMock(spec=multiprocessing_queue)
    error_queue = mock.MagicMock(spec=multiprocessing_queue)
    process_id = "1"
    mock_function = mock.MagicMock()
    mock_function.__name__ = "mock_function"
    mock_function.side_effect = RuntimeError("Something went wrong")
    args = (1, 2)
    kwargs = {"foo": "bar"}
    parallel_utils._process_wrapper(
        return_queue=return_queue,
        error_queue=error_queue,
        logging_queue=mock.MagicMock(spec=multiprocessing_queue),
        process_id=process_id,
        # "foo.package" imports but fails registration.
        # "bar.package" fails to import.
        extension_package_import_paths=["foo.package", "bar.package"],
        call_spec=parallel_utils.CallSpec(mock_function, *args, **kwargs))

    mock_import.assert_has_calls(
        [mock.call("foo.package"), mock.call("bar.package")])
    mock_register.assert_called_once()
    mock_manager_class.assert_called_once()
    mock_function.assert_called_once_with(mock_manager, *args, **kwargs)
    mock_logger.warning.assert_called()
    return_queue.put.assert_not_called()
    error_queue.put.assert_called_once_with(
        (process_id, (RuntimeError.__name__, "Something went wrong")))
    mock_manager.close.assert_called_once()
Ejemplo n.º 11
0
class PackageRegistrarTests(unit_test_case.UnitTestCase):
    """Unit tests for package_registrar.py."""
    def setUp(self):
        super().setUp()
        self.mock_package = mock.MagicMock(spec=[
            "__name__", "__version__", "export_extensions", "download_key"
        ])
        self.mock_package.__name__ = _TEST_PACKAGE_IMPORT_PATH
        self.mock_package.__version__ = "0.0.1"
        self.mock_package.export_extensions.return_value = {}

    def test_register_package_without_version(self):
        """Test registering a package without __version__ attribute."""
        del self.mock_package.__version__
        error_regex = r"Expected __version__ to be a string, found None"
        with self.assertRaisesRegex(errors.PackageRegistrationError,
                                    error_regex):
            package_registrar.register(self.mock_package)

    def test_register_package_without_required_functions(self):
        """Test registering a package which does not define required functions."""
        del self.mock_package.export_extensions  # Missing function.
        self.mock_package.download_key = None  # Not a function.
        regex = r"Package must define functions.*download_key.*export_extensions"
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    @mock.patch.object(extensions,
                       "package_info",
                       new={
                           _TEST_PACKAGE_NAME:
                           immutabledict.immutabledict({
                               "version":
                               "0.0.1",
                               "key_download_function":
                               lambda: None,
                               "import_path":
                               _TEST_PACKAGE_IMPORT_PATH,
                           })
                       })
    def test_register_already_known_package(self):
        """Test registering package which has already been registered."""
        error_regex = r"Package 'my_extension_package' has already been registered"
        with self.assertRaisesRegex(errors.PackageRegistrationError,
                                    error_regex):
            package_registrar.register(self.mock_package)

    def test_register_not_a_class(self):
        """Test registering an object which is not a class."""
        test_cases = [
            "auxiliary_devices", "primary_devices", "virtual_devices",
            "communication_types", "capability_interfaces",
            "capability_flavors"
        ]
        for extension in test_cases:
            with self.subTest(extension=extension):
                self.mock_package.export_extensions.return_value = {
                    extension: [None, "foo"]
                }
                with self.assertRaisesRegex(errors.PackageRegistrationError,
                                            r"must be class objects"):
                    package_registrar.register(self.mock_package)

    def test_register_abstract_class(self):
        """Test registering an abstract class."""
        test_cases = [("auxiliary_devices", [AbstractAuxiliaryDevice]),
                      ("primary_devices", [AbstractPrimaryDevice]),
                      ("virtual_devices", [AbstractVirtualDevice]),
                      ("communication_types", [AbstractCommunicationType]),
                      ("capability_flavors", [AbstractCapabilityFlavorDefault])
                      ]
        for extension, extension_classes in test_cases:
            with self.subTest(extension=extension,
                              extension_classes=extension_classes):
                self.mock_package.export_extensions.return_value = {
                    extension: extension_classes
                }
                with self.assertRaisesRegex(errors.PackageRegistrationError,
                                            r"must not be abstract"):
                    package_registrar.register(self.mock_package)

    def test_register_class_with_incorrect_base_class(self):
        """Test registering a class with unexpected base class."""
        test_cases = [
            ("auxiliary_devices", auxiliary_device.AuxiliaryDevice),
            ("primary_devices", gazoo_device_base.GazooDeviceBase),
            ("virtual_devices", gazoo_device_base.GazooDeviceBase),
            ("communication_types", communication_types.CommunicationType),
            ("capability_interfaces", capability_base.CapabilityBase),
            ("capability_flavors", capability_base.CapabilityBase)
        ]
        for extension, expected_base_class in test_cases:
            with self.subTest(extension=extension,
                              expected_base_class=expected_base_class):
                self.mock_package.export_extensions.return_value = {
                    extension: [ClassNotInheritingFromInterface]
                }
                regex = r"must be subclasses of {}".format(
                    expected_base_class.__name__)
                with self.assertRaisesRegex(errors.PackageRegistrationError,
                                            regex):
                    package_registrar.register(self.mock_package)

    @mock.patch.object(
        extensions,
        "communication_types",
        new={GoodCommunicationType.__name__: GoodCommunicationType})
    def test_register_duplicate_comm_type(self):
        """Test registering a duplicate communication type."""
        self.mock_package.export_extensions.return_value = {
            "communication_types": [GoodCommunicationType]
        }
        regex = (
            r"New communication types .*GoodCommunicationType.* have same "
            r"names \(\[.*GoodCommunicationType.*\]\) as existing "
            r"communication types .*GoodCommunicationType.*")
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    @mock.patch.object(extensions, "primary_devices", new=[GoodPrimaryDevice])
    def test_register_duplicate_device_class(self):
        """Test registering a duplicate device class."""
        self.mock_package.export_extensions.return_value = {
            "primary_devices": [GoodPrimaryDevice]
        }
        regex = r"Device types.*some_primary_device.*are already defined in GDM"
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    def test_register_nonconformant_device_class(self):
        """Test registering a device class which fails conformance checks."""
        err_template = (
            r"Failed to register package 'my_extension_package' with GDM "
            r"architecture\. The following device class\(es\) are incompliant with "
            r"GDM architecture:\n{cls}\n\t{err}")
        test_cases = (
            (BadPrimaryDeviceNoLogDecorator,
             (r"Public methods without return values must be decorated with "
              r"@decorators\.LogDecorator\(<logger>\)\. "
              r"Incompliant methods: \['factory_reset'\]")),
            (BadPrimaryDeviceSignatureOverride,
             (r"Methods may not fully override signatures inherited from parents\."
              r" Only extending the argument list is allowed\. Incompliant method "
              r"signatures: \[\"Method 'reboot', child signature "
              r".*BadPrimaryDeviceSignatureOverride\.reboot\(self\), inherited "
              r"signature\(s\) \['.*FakeGazooDeviceBase\.reboot\(self, no_wait, "
              r"method\)', '.*PrimaryDeviceBase.reboot\(self, no_wait, "
              r"method\)'\].\"\]")),
            (BadPrimaryDeviceNewPublicMethod,
             (r"New public methods are not allowed, except for health checks\. "
              r"Methods must either be private or, if public, moved into "
              r"capabilities\. Incompliant methods: \['new_method'\]")),
            (BadPrimaryDeviceUncategorizedProperty,
             (r"Public properties must be categorized as either "
              r"@decorators\.DynamicProperty, \.PersistentProperty, "
              r"\.OptionalProperty, or \.CapabilityDecorator\. "
              r"Incompliant properties: \['new_property'\]")),
            (BadPrimaryDeviceMisnamedHealthCheck,
             (r"Health checks must follow the <check_\.\.\.> naming convention\. "
              r"Incompliant health checks: \['misnamed_health_check'\]")),
            (BadPrimaryDeviceMissingClassConstants,
             (r"Class constants \['DEVICE_TYPE', 'COMMUNICATION_TYPE', "
              r"'DETECT_MATCH_CRITERIA', '_OWNER_EMAIL'\] are not set")),
            (BadPrimaryDeviceMisnamedCapability,
             (r"Capability definition\(s\) are invalid\. "
              r"Capability 'wrong_name': RuntimeError\(\"Attempting to define "
              r"capability flavor\(s\) .*EventParserDefault'.* under invalid name "
              r"wrong_name.*expected name: event_parser\.\"\)")),
        )
        for bad_device_class, expected_error_regex in test_cases:
            with self.subTest(device_class=bad_device_class):
                self.mock_package.export_extensions.return_value = {
                    "primary_devices": [bad_device_class]
                }
                error_regex = err_template.format(cls=bad_device_class,
                                                  err=expected_error_regex)
                with self.assertRaisesRegex(errors.PackageRegistrationError,
                                            error_regex):
                    package_registrar.register(self.mock_package)

    @mock.patch.object(extensions,
                       "capability_interfaces",
                       new={"good_capability_base": GoodCapabilityBase})
    @mock.patch.object(extensions,
                       "capability_flavors",
                       new={"good_capability_default": GoodCapabilityDefault})
    @mock.patch.object(extensions,
                       "capabilities",
                       new={"good_capability": "good_capability_base"})
    def test_register_duplicate_capability_interface(self):
        """Test registering a duplicate capability interface."""
        self.mock_package.export_extensions.return_value = {
            "capability_interfaces": [GoodCapabilityBase]
        }
        regex = (r"New capability interfaces .*GoodCapabilityBase.* have "
                 r"same names \(\[.*good_capability_base.*\]\) as "
                 r"existing capability interfaces .*GoodCapabilityBase.*")
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    @mock.patch.object(extensions,
                       "capability_interfaces",
                       new={"good_capability_base": GoodCapabilityBase})
    @mock.patch.object(extensions, "capability_flavors",
                       {"good_capability_default": GoodCapabilityDefault})
    @mock.patch.object(extensions, "capabilities",
                       {"good_capability": "good_capability_base"})
    def test_register_duplicate_capability_flavor(self):
        """Test registering a duplicate capability flavor."""
        self.mock_package.export_extensions.return_value = {
            "capability_flavors": [GoodCapabilityDefault]
        }
        regex = (r"New capability flavors .*GoodCapabilityDefault.* have "
                 r"same names \(\[.*good_capability_default.*\]\) as "
                 r"existing capability flavors .*GoodCapabilityDefault.*")
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    @mock.patch.object(extensions,
                       "capability_interfaces",
                       new={"good_capability_base": GoodCapabilityBase})
    @mock.patch.object(extensions,
                       "capability_flavors",
                       new={"good_capability_default": GoodCapabilityDefault})
    @mock.patch.object(extensions,
                       "capabilities",
                       new={"good_capability": "good_capability_base"})
    def test_register_capability_with_duplicate_name(self):
        """Test registering a capability with a duplicate name."""
        self.mock_package.export_extensions.return_value = {
            "capability_interfaces": [CapabilityWithSameNameBase]
        }
        regex = (r"New capabilities .*CapabilityWithSameNameBase.* have "
                 r"same names \(\[.*good_capability.*\]\) as "
                 r"existing capabilities .*GoodCapabilityBase.*")
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    @mock.patch.object(
        extensions,
        "communication_types",
        new={GoodCommunicationType.__name__: GoodCommunicationType})
    def test_register_detection_query_invalid_key_type(self):
        """Test registering detection query with invalid key type."""
        invalid_query_dict = {
            "foo_query": good_detection_query  # Invalid key type (str)
        }
        self.mock_package.export_extensions.return_value = {
            "detect_criteria":
            immutabledict.immutabledict(
                {GoodCommunicationType.__name__: invalid_query_dict})
        }
        regex = (r"Unable to register query {} for communication type {!r}. "
                 "Detection query keys must be {} instances.".format(
                     "foo_query", GoodCommunicationType.__name__,
                     detect_criteria.QueryEnum.__name__))
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    @mock.patch.object(
        extensions,
        "communication_types",
        new={GoodCommunicationType.__name__: GoodCommunicationType})
    def test_register_detection_query_invalid_query(self):
        """Test registering detection query with invalid query type."""
        invalid_query_dict_bad_query_type = {
            GoodQueryKey.some_valid_query: None  # Invalid query type (None)
        }
        self.mock_package.export_extensions.return_value = {
            "detect_criteria":
            immutabledict.immutabledict({
                GoodCommunicationType.__name__:
                invalid_query_dict_bad_query_type
            })
        }
        regex = (r"Unable to register query {} for communication type {!r}. "
                 "Detection queries must be callable".format(
                     GoodQueryKey.some_valid_query,
                     GoodCommunicationType.__name__))

        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

        invalid_query_dict_bad_args = {
            GoodQueryKey.some_valid_query: lambda: None  # Invalid query args
        }
        self.mock_package.export_extensions.return_value = {
            "detect_criteria":
            immutabledict.immutabledict(
                {GoodCommunicationType.__name__: invalid_query_dict_bad_args})
        }

        args = r"\('address', 'detect_logger', 'create_switchboard_func'\)"
        regex = (
            r"Unable to register query {} for communication type {!r}. "
            r"Detection queries must be callable functions which accept 3 "
            r"arguments: {}".format(GoodQueryKey.some_valid_query,
                                    GoodCommunicationType.__name__, args))

        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    @mock.patch.object(extensions, "communication_types", new={})
    def test_register_detect_query_for_unknown_comm_type(self):
        """Test registering a detect query comm type not known to GDM."""
        valid_query_dict = {
            GoodQueryKey.some_valid_query: good_detection_query
        }
        self.mock_package.export_extensions.return_value = {
            "detect_criteria":
            immutabledict.immutabledict(
                {"SomeUnknownCommunicationType": valid_query_dict})
        }
        regex = (
            r"Unable to register detection criteria for communication type "
            "{!r} as it has not been exported by the package.".format(
                "SomeUnknownCommunicationType"))
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    @mock.patch.object(
        extensions,
        "communication_types",
        new={GoodCommunicationType.__name__: GoodCommunicationType})
    @mock.patch.object(extensions,
                       "detect_criteria",
                       new={
                           GoodCommunicationType.__name__:
                           immutabledict.immutabledict({
                               GoodQueryKey.some_valid_query:
                               good_detection_query
                           })
                       })
    def test_register_duplicate_detect_query(self):
        """Test registering a duplicate detect query."""
        duplicate_query_dict = {
            GoodQueryKey.some_valid_query: good_detection_query
        }
        self.mock_package.export_extensions.return_value = {
            "detect_criteria":
            immutabledict.immutabledict(
                {GoodCommunicationType.__name__: duplicate_query_dict})
        }
        regex = re.escape(
            r"Detection queries {} for communication type {!r} are already "
            "defined in GDM.".format(list(duplicate_query_dict.keys()),
                                     GoodCommunicationType.__name__))
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    def test_register_invalid_key_types(self):
        """Test registering a package which exports invalid key types."""
        self.mock_package.export_extensions.return_value = {
            "keys": [GoodKey, 1, None]
        }
        regex = r"Keys must be data_types\.KeyInfo instances"
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    def test_register_mismatching_key_package_name(self):
        """Test registering a package with keys which have mismatching package."""
        self.mock_package.export_extensions.return_value = {
            "keys": [BadKeyMismatchingPackage]
        }
        regex = (
            r"KeyInfo\.package attribute must match the name of the package "
            r"\('my_extension_package'\)")
        with self.assertRaisesRegex(errors.PackageRegistrationError, regex):
            package_registrar.register(self.mock_package)

    def test_register_invalid_manager_cli_mixin_type(self):
        """Test registering a package with a bad Manager CLI mixin type."""
        test_cases = [
            1,  # Not a class object.
            int,  # Class object, but does not inherit from FireManager.
        ]

        for manager_cli_mixin in test_cases:
            with self.subTest(manager_cli_mixin=manager_cli_mixin):
                self.mock_package.export_extensions.return_value = {
                    "manager_cli_mixin": manager_cli_mixin,
                }
                regex = "FireManager mixin class is invalid"
                with self.assertRaisesRegex(errors.PackageRegistrationError,
                                            regex):
                    package_registrar.register(self.mock_package)

    @mock.patch.object(extensions, "primary_devices", new=[])
    @mock.patch.dict(extensions.capability_interfaces)
    @mock.patch.dict(extensions.capability_flavors)
    @mock.patch.dict(extensions.capabilities)
    def test_failed_registration_does_not_change_state(self):
        """Test that failed registration doesn't change known supported classes."""
        self.mock_package.export_extensions.return_value = {
            "primary_devices": [BadPrimaryDeviceNoLogDecorator],
            "capability_interfaces": [GoodCapabilityBase],
            "capability_flavors": [GoodCapabilityDefault]
        }
        primary_devices_before = extensions.primary_devices.copy()
        capability_interfaces_before = extensions.capability_interfaces.copy()
        capability_flavors_before = extensions.capability_flavors.copy()
        capabilities_before = extensions.capabilities.copy()

        with self.assertRaises(errors.PackageRegistrationError):
            package_registrar.register(self.mock_package)
        self.assertEqual(primary_devices_before, extensions.primary_devices)
        self.assertEqual(capability_interfaces_before,
                         extensions.capability_interfaces)
        self.assertEqual(capability_flavors_before,
                         extensions.capability_flavors)
        self.assertEqual(capabilities_before, extensions.capabilities)

    @mock.patch.object(extensions, "detect_criteria", new={})
    @mock.patch.object(extensions, "communication_types", new={})
    @mock.patch.object(extensions, "package_info", new={})
    @mock.patch.object(extensions, "auxiliary_devices", new=[])
    @mock.patch.object(extensions, "primary_devices", new=[])
    @mock.patch.object(extensions, "virtual_devices", new=[])
    @mock.patch.object(extensions, "keys", new=[])
    @mock.patch.object(extensions, "manager_cli_mixins", new=[])
    @mock.patch.dict(extensions.capability_interfaces)
    @mock.patch.dict(extensions.capability_flavors)
    @mock.patch.dict(extensions.capabilities)
    def test_valid_registration(self):
        """Test registering a valid extension dictionary."""
        self.mock_package.export_extensions.return_value = {
            "auxiliary_devices": [GoodAuxiliaryDevice],
            "primary_devices": [GoodPrimaryDevice],
            "virtual_devices": [GoodVirtualDevice],
            "communication_types": [GoodCommunicationType],
            "detect_criteria": {
                GoodCommunicationType.__name__:
                immutabledict.immutabledict(
                    {GoodQueryKey.some_valid_query: good_detection_query})
            },
            "capability_interfaces": [GoodCapabilityBase],
            "capability_flavors": [GoodCapabilityDefault],
            "keys": [GoodKey],
            "manager_cli_mixin": GoodManagerCliMixin,
        }

        package_registrar.register(self.mock_package)
        self.assertEqual(
            extensions.package_info, {
                _TEST_PACKAGE_NAME:
                immutabledict.immutabledict(
                    {
                        "version": self.mock_package.__version__,
                        "key_download_function":
                        self.mock_package.download_key,
                        "import_path": _TEST_PACKAGE_IMPORT_PATH,
                    })
            })
        self.assertEqual(extensions.auxiliary_devices, [GoodAuxiliaryDevice])
        self.assertEqual(extensions.primary_devices, [GoodPrimaryDevice])
        self.assertEqual(extensions.virtual_devices, [GoodVirtualDevice])
        self.assertEqual(
            extensions.communication_types,
            {GoodCommunicationType.__name__: GoodCommunicationType})
        self.assertEqual(
            extensions.detect_criteria, {
                GoodCommunicationType.__name__: {
                    GoodQueryKey.some_valid_query: good_detection_query
                }
            })
        self.assertEqual(
            extensions.capability_interfaces["good_capability_base"],
            GoodCapabilityBase)
        self.assertEqual(
            extensions.capability_flavors["good_capability_default"],
            GoodCapabilityDefault)
        self.assertEqual(extensions.capabilities["good_capability"],
                         "good_capability_base")
        self.assertEqual(extensions.keys, [GoodKey])

        # Test registering a different package which extends detect criteria for
        # communication type registered by the first package.
        self.mock_package.__name__ = "some_parent_package.another_extension_package"
        self.mock_package.export_extensions.return_value = {
            "detect_criteria": {
                GoodCommunicationType.__name__:
                immutabledict.immutabledict({
                    GoodQueryKey.another_valid_query:
                    another_good_detection_query,
                })
            }
        }
        package_registrar.register(self.mock_package)

        self.assertIn(_TEST_PACKAGE_NAME, extensions.package_info)
        self.assertIn("another_extension_package", extensions.package_info)
        self.assertEqual(
            extensions.detect_criteria, {
                GoodCommunicationType.__name__: {
                    GoodQueryKey.some_valid_query: good_detection_query,
                    GoodQueryKey.another_valid_query:
                    another_good_detection_query,
                }
            })

    @mock.patch.object(package_registrar, "register")
    @mock.patch.object(importlib,
                       "import_module",
                       side_effect=ImportError("Package not found"))
    @mock.patch.object(package_registrar.logger, "warning")
    def test_import_and_register_import_failure(self, mock_warning,
                                                mock_import, mock_register):
        """Test import_and_register() on a nonexistent package."""
        return_value = package_registrar.import_and_register(
            "nonexistent-package", include_cli_instructions=True)
        self.assertFalse(return_value)
        self.assertEqual(mock_warning.call_count, 2)
        mock_import.assert_called_once_with("nonexistent-package")
        mock_register.assert_not_called()

    @mock.patch.object(package_registrar,
                       "register",
                       side_effect=errors.PackageRegistrationError(
                           "Something failed", "some_package"))
    @mock.patch.object(importlib, "import_module")
    @mock.patch.object(package_registrar.logger, "warning")
    def test_import_and_register_registration_failure(self, mock_warning,
                                                      mock_import,
                                                      mock_register):
        """Test import_and_register() on an invalid package."""
        return_value = package_registrar.import_and_register(
            "invalid-package", include_cli_instructions=True)
        self.assertFalse(return_value)
        self.assertEqual(mock_warning.call_count, 2)
        mock_import.assert_called_once_with("invalid-package")
        mock_register.assert_called_once()

    @mock.patch.object(package_registrar, "register")
    @mock.patch.object(importlib, "import_module")
    @mock.patch.object(package_registrar.logger, "warning")
    def test_import_and_register_success(self, mock_warning, mock_import,
                                         mock_register):
        """Test import_and_register() on a valid package."""
        return_value = package_registrar.import_and_register("valid-package")
        self.assertTrue(return_value)
        mock_warning.assert_not_called()
        mock_import.assert_called_once_with("valid-package")
        mock_register.assert_called_once()

    @mock.patch.object(json, "load", return_value={})
    @mock.patch.object(builtins, "open")
    @mock.patch.object(os.path, "exists", return_value=False)
    def test_get_cli_extension_packages_no_config_file(self, mock_exists,
                                                       mock_open, mock_load):
        """Test get_cli_extension_packages() when config file is missing."""
        self.assertFalse(package_registrar.get_cli_extension_packages())
        mock_exists.assert_called_once()
        mock_open.assert_not_called()
        mock_load.assert_not_called()

    @mock.patch.object(json, "load", return_value={})
    @mock.patch.object(builtins, "open")
    @mock.patch.object(os.path, "exists", return_value=True)
    def test_get_cli_extension_packages_no_config_entry(
            self, mock_exists, mock_open, mock_load):
        """Test get_cli_extension_packages() when config doesn't have the entry."""
        self.assertFalse(package_registrar.get_cli_extension_packages())
        mock_exists.assert_called_once()
        mock_open.assert_called_once()
        mock_load.assert_called_once()

    @mock.patch.object(json,
                       "load",
                       return_value={
                           "cli_extension_packages":
                           ["package_one", "package_two"],
                       })
    @mock.patch.object(builtins, "open")
    @mock.patch.object(os.path, "exists", return_value=True)
    def test_get_cli_extension_packages_success(self, mock_exists, mock_open,
                                                mock_load):
        """Test get_cli_extension_packages() with some registered CLI packages."""
        self.assertCountEqual(package_registrar.get_cli_extension_packages(),
                              ["package_one", "package_two"])
        mock_exists.assert_called_once()
        mock_open.assert_called_once()
        mock_load.assert_called_once()

    @mock.patch.object(package_registrar,
                       "get_cli_extension_packages",
                       return_value=["package_one", "package_two"])
    @mock.patch.object(package_registrar, "import_and_register")
    def test_import_and_register_cli_extension_packages(
            self, mock_import_and_register, mock_get_cli_extension_packages):
        """Test import_and_register_cli_extension_packages() with some packages."""
        package_registrar.import_and_register_cli_extension_packages()
        mock_get_cli_extension_packages.assert_called_once()
        mock_import_and_register.assert_has_calls([
            mock.call("package_one", include_cli_instructions=True),
            mock.call("package_two", include_cli_instructions=True)
        ],
                                                  any_order=True)