def test_shell_command_timeout(): """Shell commands can time out.""" default_times_out = Requirement( requirements={'shell': 'sleep 0.1'}, directory=Path('/'), timeout=0.01, ) assert not default_times_out default_does_not_timeout = Requirement( requirements={'shell': 'sleep 0.1'}, directory=Path('/'), timeout=0.2, ) assert default_does_not_timeout specifed_does_not_timeout = Requirement( requirements={ 'shell': 'sleep 0.1', 'timeout': 0.2 }, directory=Path('/'), timeout=0.5, ) assert specifed_does_not_timeout specified_does_timeout = Requirement( requirements={ 'shell': 'sleep 0.1', 'timeout': 0.05 }, directory=Path('/'), timeout=1000, ) assert not specified_does_timeout
def test_installed_requirement(): """Requirement should be truthy when value is in $PATH.""" successful_installed_requirement = Requirement( requirements={'installed': 'ls'}, directory=Path('/'), ) assert successful_installed_requirement unsuccessful_installed_requirement = Requirement( requirements={'installed': 'does_not_exist'}, directory=Path('/'), ) assert not unsuccessful_installed_requirement
def test_environment_variable_requirement(): """Requirement should be truthy when environment variable is available.""" successful_env_requirement = Requirement( requirements={'env': 'EXAMPLE_ENV_VARIABLE'}, directory=Path('/'), ) assert successful_env_requirement unsuccessful_env_requirement = Requirement( requirements={'env': 'THIS_IS_NOT_SET'}, directory=Path('/'), ) assert not unsuccessful_env_requirement
def test_that_shell_commands_are_run_in_correct_diretory(): """All shell commands should be run from 'directory'""" successful_shell_requirement = Requirement( requirements={'shell': 'ls tmp'}, directory=Path('/'), ) assert successful_shell_requirement unsuccessful_shell_requirement = Requirement( requirements={'shell': 'ls does_not_exist'}, directory=Path('/'), ) assert not unsuccessful_shell_requirement
def test_shell_command_requirement(): """Requirement should be truthy when command returns 0 exit code.""" successful_shell_requirement = Requirement( requirements={'shell': 'command -v ls'}, directory=Path('/'), ) assert successful_shell_requirement unsuccessful_shell_requirement = Requirement( requirements={'shell': 'command -v does_not_exist'}, directory=Path('/'), ) assert not unsuccessful_shell_requirement
def test_null_object_pattern(): """Empty requirements should be considered satisfied.""" successful_shell_requirement = Requirement( requirements={}, directory=Path('/'), ) assert successful_shell_requirement
def test_recursive_module_requirements(): """Missing dependencies should propagate.""" moduleA = Module( name='A', module_config={'requires': [{ 'module': 'B' }]}, module_directory=Path(__file__).parent, ) moduleB = Module( name='B', module_config={'requires': [{ 'module': 'C' }]}, module_directory=Path(__file__).parent, ) moduleC = Module( name='C', module_config={'requires': [{ 'module': 'D' }]}, module_directory=Path(__file__).parent, ) assert Requirement.pop_missing_module_dependencies(modules={ 'A': moduleA, 'B': moduleB, 'C': moduleC }, ) == {}
def valid_module( name: str, config: ModuleConfigDict, requires_timeout: Union[int, float], requires_working_directory: Path, ) -> bool: """ Check if the given dict represents a valid enabled module. The method determines this by inspecting any requires items the module might have specified. :param name: Name of module, used for logging purposes. :param config: Configuration dictionary of the module. :param requires_timeout: Time to wait for shell command requirements. :param requires_working_directory: CWD for shell commands. :return: True if module should be enabled. """ if not config.get('enabled', True): return False # The module is enabled, now check if all requirements are satisfied requires: List[RequirementDict] = cast_to_list( config.get( 'requires', {}, ), ) requirements = [ Requirement( requirements=requirements_dict, directory=requires_working_directory, timeout=requires_timeout, ) for requirements_dict in requires ] if all(requirements): return True else: logger.warning( f'[module/{name}] ' + ', '.join([ repr(requirement) for requirement in requirements ]) + '!', ) return False
def valid_class_section( section: ModuleConfig, requires_timeout: Union[int, float], requires_working_directory: Path, ) -> bool: """Check if the given dict represents a valid enabled module.""" if not len(section) == 1: raise RuntimeError( 'Tried to check module section with dict ' 'which does not have exactly one item.', ) try: module_name = next(iter(section.keys())) valid_module_name = \ module_name.split('/')[0].lower() == 'module' # type: ignore enabled = section[module_name].get('enabled', True) if not (valid_module_name and enabled): return False except KeyError: return False # The module is enabled, now check if all requirements are satisfied requires: List[RequirementDict] = cast_to_list( section[module_name].get( 'requires', {}, ), ) requirements = [ Requirement( requirements=requirements_dict, directory=requires_working_directory, timeout=requires_timeout, ) for requirements_dict in requires ] if all(requirements): return True else: logger.warning( f'[{module_name}] ' + ", ".join([repr(requirement) for requirement in requirements]) + '!', ) return False
def test_requiring_a_global_module(): """You should be able to require global modules.""" moduleA = Module( name='A', module_config={'requires': [{ 'module': 'B' }]}, module_directory=Path(__file__).parent, ) assert moduleA.depends_on == ('B', ) moduleB = Module( name='B', module_config={'run': { 'shell': 'echo hi!' }}, module_directory=Path(__file__).parent, ) assert moduleB.depends_on == tuple() moduleC = Module( name='C', module_config={'requires': [{ 'module': 'D' }]}, module_directory=Path(__file__).parent, ) assert moduleC.depends_on == ('D', ) assert Requirement.pop_missing_module_dependencies(modules={ 'A': moduleA, 'B': moduleB, 'C': moduleC }, ) == { 'A': moduleA, 'B': moduleB }
def __init__( self, config: AstralityYAMLConfigDict = {}, modules: Dict[str, ModuleConfigDict] = {}, context: Context = Context(), directory: Path = Path(__file__).parent / 'tests' / 'test_config', dry_run: bool = False, ) -> None: """Initialize a ModuleManager object from `astrality.yml` dict.""" self.config_directory = directory self.application_config = config self.application_context = context self.dry_run = dry_run self.startup_done = False self.last_module_events: Dict[str, str] = {} # Get module configurations which are externally defined self.global_modules_config = GlobalModulesConfig( config=config.get('modules', {}), config_directory=self.config_directory, ) self.reprocess_modified_files = \ self.global_modules_config.reprocess_modified_files self.modules: Dict[str, Module] = {} # Insert externally managed modules for external_module_source \ in self.global_modules_config.external_module_sources: # Insert context defined in external configuration module_context = external_module_source.context( context=self.application_context, ) self.application_context.reverse_update(module_context) module_configs = external_module_source.modules( context=self.application_context, ) module_directory = external_module_source.directory for module_name, module_config in module_configs.items(): if module_name \ not in self.global_modules_config.enabled_modules: continue if not Module.valid_module( name=module_name, config=module_config, requires_timeout=self.global_modules_config. requires_timeout, # noqa requires_working_directory=module_directory, ): continue module = Module( name=module_name, module_config=module_config, module_directory=module_directory, replacer=self.interpolate_string, context_store=self.application_context, global_modules_config=self.global_modules_config, dry_run=dry_run, ) self.modules[module.name] = module # Insert modules defined in `astrality.yml` for module_name, module_config in modules.items(): # Check if this module should be included if module_name not in self.global_modules_config.enabled_modules: continue if not Module.valid_module( name=module_name, config=module_config, requires_timeout=self.global_modules_config.requires_timeout, requires_working_directory=self.config_directory, ): continue module = Module( name=module_name, module_config=module_config, module_directory=self.config_directory, replacer=self.interpolate_string, context_store=self.application_context, global_modules_config=self.global_modules_config, dry_run=dry_run, ) self.modules[module.name] = module # Remove modules which depends on other missing modules Requirement.pop_missing_module_dependencies(self.modules) # Initialize the config directory watcher, but don't start it yet self.directory_watcher = DirectoryWatcher( directory=self.config_directory, on_modified=self.file_system_modified, ) logger.info('Enabled modules: ' + ', '.join(self.modules.keys()))