def _process_wrapper(return_queue: multiprocessing.Queue, error_queue: multiprocessing.Queue, logging_queue: multiprocessing.Queue, process_id: str, extension_package_import_paths: Sequence[str], call_spec: CallSpec) -> None: """Executes the provided function in a parallel process.""" gdm_logger.initialize_child_process_logging(logging_queue) short_description = f"{call_spec.function.__name__} in process {os.getpid()}" logger = gdm_logger.get_logger() logger.debug(f"{short_description}: starting execution of {call_spec}...") # The state of the main process (such as registered extension packages) is not # copied over when using "forkserver" or "spawn" as the process start method. for import_path in extension_package_import_paths: try: package_registrar.register(importlib.import_module(import_path)) except (ImportError, errors.PackageRegistrationError) as e: logger.debug( f"{short_description}: failed to import and register GDM " f"extension package with import path {import_path}. " f"Error: {e!r}. Proceeding despite the failure.") manager_inst = manager.Manager() try: return_value = call_spec.function(manager_inst, *call_spec.args, **call_spec.kwargs) logger.debug(f"{short_description}: execution succeeded. " f"Return value: {return_value}.") return_queue.put((process_id, return_value)) except Exception as e: # pylint: disable=broad-except logger.warning( f"{short_description}: execution raised an error: {e!r}.") error_queue.put((process_id, (type(e).__name__, str(e)))) finally: manager_inst.close()
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)
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_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_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_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)
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)
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)
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)
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_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)
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_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)
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)
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_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)
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)
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)
from gazoo_device import manager from gazoo_device import mobly_controller from gazoo_device import package_registrar from gazoo_device.utility import multiprocessing_utils from gazoo_device.utility import signal_utils Manager = manager.Manager register = package_registrar.register version = _version.version __version__ = _version.version # For Mobly integration MOBLY_CONTROLLER_CONFIG_NAME = "GazooDevice" create = mobly_controller.create destroy = mobly_controller.destroy get_info = mobly_controller.get_info multiprocessing_utils.configure_multiprocessing() # Defend against inadvertent basicConfig, which adds log noise logging.getLogger().addHandler(logging.NullHandler()) # Ensure that 'finally' clauses and atexit handlers run when killed by SIGTERM. signal.signal(signal.SIGTERM, signal_utils.handle_sigterm) # Set up logger gdm_logger.initialize_logger() # Register device classes and capabilities built into GDM package_registrar.register(gazoo_device_controllers)
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_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, } })