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)
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)
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)
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)
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)
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)
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)
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)
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)
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()
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)