async def test_get_integration_with_requirements_pip_install_fails_two_passes(hass): """Check getting an integration with loaded requirements and the pip install fails two passes.""" hass.config.skip_pip = False mock_integration( hass, MockModule("test_component_dep", requirements=["test-comp-dep==1.0.0"]) ) mock_integration( hass, MockModule( "test_component_after_dep", requirements=["test-comp-after-dep==1.0.0"] ), ) mock_integration( hass, MockModule( "test_component", requirements=["test-comp==1.0.0"], dependencies=["test_component_dep"], partial_manifest={"after_dependencies": ["test_component_after_dep"]}, ), ) def _mock_install_package(package, **kwargs): if package == "test-comp==1.0.0": return True return False # 1st pass with pytest.raises(RequirementsNotFound), patch( "homeassistant.util.package.is_installed", return_value=False ) as mock_is_installed, patch( "homeassistant.util.package.install_package", side_effect=_mock_install_package ) as mock_inst: integration = await async_get_integration_with_requirements( hass, "test_component" ) assert integration assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 3 assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", "test-comp==1.0.0", ] assert len(mock_inst.mock_calls) == 7 assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-after-dep==1.0.0", "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", "test-comp-dep==1.0.0", "test-comp-dep==1.0.0", "test-comp==1.0.0", ] # 2nd pass with pytest.raises(RequirementsNotFound), patch( "homeassistant.util.package.is_installed", return_value=False ) as mock_is_installed, patch( "homeassistant.util.package.install_package", side_effect=_mock_install_package ) as mock_inst: integration = await async_get_integration_with_requirements( hass, "test_component" ) assert integration assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 0 # On another attempt we remember failures and don't try again assert len(mock_inst.mock_calls) == 0 # Now clear the history and so we try again async_clear_install_history(hass) with pytest.raises(RequirementsNotFound), patch( "homeassistant.util.package.is_installed", return_value=False ) as mock_is_installed, patch( "homeassistant.util.package.install_package", side_effect=_mock_install_package ) as mock_inst: integration = await async_get_integration_with_requirements( hass, "test_component" ) assert integration assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 2 assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", ] assert len(mock_inst.mock_calls) == 6 assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-after-dep==1.0.0", "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", "test-comp-dep==1.0.0", "test-comp-dep==1.0.0", ] # Now clear the history and mock success async_clear_install_history(hass) with patch( "homeassistant.util.package.is_installed", return_value=False ) as mock_is_installed, patch( "homeassistant.util.package.install_package", return_value=True ) as mock_inst: integration = await async_get_integration_with_requirements( hass, "test_component" ) assert integration assert integration.domain == "test_component" assert len(mock_is_installed.mock_calls) == 2 assert sorted(mock_call[1][0] for mock_call in mock_is_installed.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", ] assert len(mock_inst.mock_calls) == 2 assert sorted(mock_call[1][0] for mock_call in mock_inst.mock_calls) == [ "test-comp-after-dep==1.0.0", "test-comp-dep==1.0.0", ]
async def async_check_ha_config_file( # noqa: C901 hass: HomeAssistant, ) -> HomeAssistantConfig: """Load and check if Home Assistant configuration file is valid. This method is a coroutine. """ result = HomeAssistantConfig() async_clear_install_history(hass) def _pack_error(package: str, component: str, config: ConfigType, message: str) -> None: """Handle errors from packages: _log_pkg_error.""" message = f"Package {package} setup failed. Component {component} {message}" domain = f"homeassistant.packages.{package}.{component}" pack_config = core_config[CONF_PACKAGES].get(package, config) result.add_error(message, domain, pack_config) def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None: """Handle errors from components: async_log_exception.""" result.add_error( _format_config_error(ex, domain, config)[0], domain, config) # Load configuration.yaml config_path = hass.config.path(YAML_CONFIG_FILE) try: if not await hass.async_add_executor_job(os.path.isfile, config_path): return result.add_error("File configuration.yaml not found.") assert hass.config.config_dir is not None config = await hass.async_add_executor_job( load_yaml_config_file, config_path, yaml_loader.Secrets(Path(hass.config.config_dir)), ) except FileNotFoundError: return result.add_error(f"File not found: {config_path}") except HomeAssistantError as err: return result.add_error(f"Error loading {config_path}: {err}") # Extract and validate core [homeassistant] config try: core_config = config.pop(CONF_CORE, {}) core_config = CORE_CONFIG_SCHEMA(core_config) result[CONF_CORE] = core_config except vol.Invalid as err: result.add_error(err, CONF_CORE, core_config) core_config = {} # Merge packages await merge_packages_config(hass, config, core_config.get(CONF_PACKAGES, {}), _pack_error) core_config.pop(CONF_PACKAGES, None) # Filter out repeating config sections components = {key.split(" ")[0] for key in config.keys()} # Process and validate config for domain in components: try: integration = await async_get_integration_with_requirements( hass, domain) except loader.IntegrationNotFound as ex: if not hass.config.safe_mode: result.add_error(f"Integration error: {domain} - {ex}") continue except RequirementsNotFound as ex: result.add_error(f"Integration error: {domain} - {ex}") continue try: component = integration.get_component() except ImportError as ex: result.add_error(f"Component error: {domain} - {ex}") continue # Check if the integration has a custom config validator config_validator = None try: config_validator = integration.get_platform("config") except ImportError as err: # Filter out import error of the config platform. # If the config platform contains bad imports, make sure # that still fails. if err.name != f"{integration.pkg_path}.config": result.add_error( f"Error importing config platform {domain}: {err}") continue if config_validator is not None and hasattr(config_validator, "async_validate_config"): try: result[domain] = ( await config_validator.async_validate_config( # type: ignore hass, config))[domain] continue except (vol.Invalid, HomeAssistantError) as ex: _comp_error(ex, domain, config) continue except Exception as err: # pylint: disable=broad-except logging.getLogger(__name__).exception( "Unexpected error validating config") result.add_error( f"Unexpected error calling config validator: {err}", domain, config.get(domain), ) continue config_schema = getattr(component, "CONFIG_SCHEMA", None) if config_schema is not None: try: config = config_schema(config) result[domain] = config[domain] except vol.Invalid as ex: _comp_error(ex, domain, config) continue component_platform_schema = getattr( component, "PLATFORM_SCHEMA_BASE", getattr(component, "PLATFORM_SCHEMA", None), ) if component_platform_schema is None: continue platforms = [] for p_name, p_config in config_per_platform(config, domain): # Validate component specific platform schema try: p_validated = component_platform_schema(p_config) except vol.Invalid as ex: _comp_error(ex, domain, config) continue # Not all platform components follow same pattern for platforms # So if p_name is None we are not going to validate platform # (the automation component is one of them) if p_name is None: platforms.append(p_validated) continue try: p_integration = await async_get_integration_with_requirements( hass, p_name) platform = p_integration.get_platform(domain) except loader.IntegrationNotFound as ex: if not hass.config.safe_mode: result.add_error( f"Platform error {domain}.{p_name} - {ex}") continue except ( RequirementsNotFound, ImportError, ) as ex: result.add_error(f"Platform error {domain}.{p_name} - {ex}") continue # Validate platform specific schema platform_schema = getattr(platform, "PLATFORM_SCHEMA", None) if platform_schema is not None: try: p_validated = platform_schema(p_validated) except vol.Invalid as ex: _comp_error(ex, f"{domain}.{p_name}", p_validated) continue platforms.append(p_validated) # Remove config for current component and add validated config back in. for filter_comp in extract_domain_configs(config, domain): del config[filter_comp] result[domain] = platforms return result