def test_all_three_actions_in_on_modified_block( three_watchable_files, test_config_directory, ): file1, file2, file3 = three_watchable_files car_template = test_config_directory / 'templates' / 'a_car.template' mercedes_context = test_config_directory / 'context' / 'mercedes.yml' tesla_context = test_config_directory / 'context' / 'tesla.yml' modules = { 'car': { 'on_startup': { 'import_context': { 'from_path': str(mercedes_context), }, 'compile': { 'content': str(car_template), 'target': str(file1), }, }, 'on_modified': { str(file2): { 'import_context': { 'from_path': str(tesla_context), }, 'compile': { 'content': str(car_template), 'target': str(file1), }, 'run': { 'shell': 'touch ' + str(file3) }, }, }, }, } module_manager = ModuleManager(modules=modules) # Sanity check before beginning testing assert not file1.is_file() assert not file2.is_file() assert not file3.is_file() # Now finish tasks, i.e. on_startup block module_manager.finish_tasks() assert file1.is_file() assert not file2.is_file() assert not file3.is_file() # Check that the correct context is inserted with open(file1) as file: assert file.read() == 'My car is a Mercedes' # Now modify file2 such that the on_modified block is triggered file2.write_text('some new content') # The on_modified run command should now have been executed assert Retry()(lambda: file3.is_file()) module_manager.exit()
def test_recompile_templates_when_modified_overridden( three_watchable_files, default_global_options, _runtime, ): """ If a file is watched in a on_modified block, it should override the recompile_modified_templates option. """ template, target, touch_target = three_watchable_files template.touch() application_config = { 'module/module_name': { 'on_startup': { 'compile': { 'source': str(template), 'target': str(target), }, }, 'on_modified': { str(template): { 'run': { 'shell': 'touch ' + str(touch_target) }, }, }, }, 'context/section': { 1: 'value', }, } application_config.update(default_global_options) application_config.update(_runtime) application_config['config/modules'] = { 'recompile_modified_templates': True, } module_manager = ModuleManager(application_config) # Sanity check before beginning testing with open(template) as file: assert file.read() == '' assert not target.is_file() # Compile the template module_manager.finish_tasks() with open(target) as file: assert file.read() == '' # Now write to the template and see if it is *compiled*, but the on_modified # command is run instead template.write_text('{{ section.2 }}') time.sleep(0.7) with open(target) as file: assert file.read() == '' assert touch_target.is_file() module_manager.exit()
def test_stowing( action_block_factory, create_temp_files, module_factory, ): """ModuleManager should stow properly.""" template, target = create_temp_files(2) template.write_text('{{ env.EXAMPLE_ENV_VARIABLE }}') symlink_target = template.parent / 'symlink_me' symlink_target.touch() action_block = action_block_factory(stow={ 'content': str(template.parent), 'target': str(target.parent), 'templates': r'file(0).temp', 'non_templates': 'symlink', }, ) module = module_factory(on_exit=action_block, ) module_manager = ModuleManager() module_manager.modules = {'test': module} module_manager.exit() # Check if template has been compiled assert Path(target.parent / '0').read_text() == 'test_value' # Check if non_template has been symlinked assert (template.parent / 'symlink_me').resolve() == symlink_target
def test_recompile_templates_when_modified_overridden( three_watchable_files, test_config_directory, ): """ If a file is watched in a on_modified block, it should override the reprocess_modified_files option. """ template, target, touch_target = three_watchable_files template.touch() modules = { 'module_name': { 'on_startup': { 'compile': { 'content': str(template), 'target': str(target), }, }, 'on_modified': { str(template): { 'run': {'shell': 'touch ' + str(touch_target)}, }, }, }, } application_config = {'modules': {'reprocess_modified_files': True}} module_manager = ModuleManager( config=application_config, modules=modules, context=Context({ 'section': {1: 'value'}, }), directory=test_config_directory, ) # Sanity check before beginning testing with open(template) as file: assert file.read() == '' assert not target.is_file() # Compile the template module_manager.finish_tasks() with open(target) as file: assert file.read() == '' # Now write to the template and see if it is *compiled*, but the on_modified # command is run instead template.write_text('{{ section.2 }}') retry = Retry() assert retry(lambda: target.read_text() == '') assert retry(lambda: touch_target.is_file()) module_manager.exit()
def test_recompile_templates_when_modified( three_watchable_files, default_global_options, _runtime, ): template, target, _ = three_watchable_files template.touch() application_config = { 'module/module_name': { 'on_startup': { 'compile': { 'source': str(template), 'target': str(target), }, }, }, 'context/section': { 1: 'value', }, } application_config.update(default_global_options) application_config.update(_runtime) application_config['config/modules'] = { 'recompile_modified_templates': True, } module_manager = ModuleManager(application_config) # Sanity check before beginning testing with open(template) as file: assert file.read() == '' assert not target.is_file() # Compile the template module_manager.finish_tasks() with open(target) as file: assert file.read() == '' # Now write to the template and see if it is recompiled template.write_text('{{ section.2 }}') time.sleep(0.7) with open(target) as file: assert file.read() == 'value' module_manager.exit()
def test_that_all_exit_actions_are_correctly_performed( default_global_options, _runtime, test_config_directory, test_target, ): application_config = { 'module/car': { 'on_startup': { 'import_context': { 'from_path': 'context/mercedes.yml', }, 'compile': { 'source': 'templates/a_car.template', 'target': str(test_target), }, }, 'on_exit': { 'import_context': { 'from_path': 'context/tesla.yml', }, 'compile': { 'source': 'templates/a_car.template', 'target': str(test_target), }, }, }, } application_config.update(default_global_options) application_config.update(_runtime) module_manager = ModuleManager(application_config) # Before we start, the template target should not exist assert not test_target.is_file() # We finish tasks, reslulting in Mercedes being compiled module_manager.finish_tasks() with open(test_target) as file: assert file.read() == 'My car is a Mercedes' # We now exit, and check if the context import and compilation has been # performed module_manager.exit() with open(test_target) as file: assert file.read() == 'My car is a Tesla'
def test_running_module_exit_command_when_no_command_is_specified( self, simple_application_config, caplog, ): simple_application_config['module/test_module']['on_exit'].pop('run') module_manager = ModuleManager(simple_application_config) caplog.clear() module_manager.exit() assert caplog.record_tuples == [ ( 'astrality', logging.INFO, '[module/test_module] Running exit commands.', ), ]
def test_that_stowed_templates_are_also_watched(three_watchable_files): """Stowing template instead of compiling it should still be watched.""" template, target, _ = three_watchable_files template.touch() modules = { 'module_name': { 'on_startup': { 'stow': { 'content': str(template), 'target': str(target), 'templates': '(.+)', 'non_templates': 'ignore', }, }, }, } application_config = {'modules': {'reprocess_modified_files': True}} module_manager = ModuleManager( config=application_config, modules=modules, context=Context({ 'section': { 1: 'value' }, }), ) # Sanity check before beginning testing with open(template) as file: assert file.read() == '' assert not target.is_file() # Stow the template module_manager.finish_tasks() with open(target) as file: assert file.read() == '' # Now write to the template and see if it is recompiled template.write_text('{{ section.2 }}') assert Retry()(lambda: target.read_text() == 'value') module_manager.exit()
def test_recompile_templates_when_modified(three_watchable_files): template, target, _ = three_watchable_files template.touch() modules = { 'module_name': { 'on_startup': { 'compile': { 'content': str(template), 'target': str(target), }, }, }, } application_config = {'modules': {'reprocess_modified_files': True}} module_manager = ModuleManager( config=application_config, modules=modules, context=Context({ 'section': { 1: 'value' }, }), ) # Sanity check before beginning testing with open(template) as file: assert file.read() == '' assert not target.is_file() # Compile the template module_manager.finish_tasks() with open(target) as file: assert file.read() == '' # Now write to the template and see if it is recompiled template.write_text('{{ section.2 }}') assert Retry()(lambda: target.read_text() == 'value') module_manager.exit() module_manager.directory_watcher.stop()
def main( modules: List[str] = [], logging_level: str = 'INFO', dry_run: bool = False, test: bool = False, ): """ Run the main process for Astrality. :param modules: Modules to be enabled. If empty, use astrality.yml. :param logging_level: Loging level. :param dry_run: If file system actions should be printed and skipped. :param test: If True, return after one iteration loop. """ if 'ASTRALITY_LOGGING_LEVEL' in os.environ: # Override logging level if env variable is set logging_level = os.environ['ASTRALITY_LOGGING_LEVEL'] # Set the logging level to the configured setting logging.basicConfig( level=logging.getLevelName(logging_level), # type: ignore ) if not modules and not dry_run and not test: # Quit old astrality instances kill_old_astrality_processes() # How to quit this process def exit_handler(signal=None, frame=None) -> None: """ Cleanup all temporary files and run module exit handlers. The temp directory is left alone, for two reasons: 1: An empty directory uses neglible disk space. 2: If this process is interrupted by another Astrality instance, we might experience race conditions when the exit handler deletes the temporary directory *after* the new Astrality instance creates it. """ logger.critical('Astrality was interrupted') logger.info('Cleaning up temporary files before exiting...') try: # Run all the module exit handlers module_manager.exit() except NameError: # The module_manager instance has not been assigned yet. pass try: sys.exit(0) except SystemExit: os._exit(0) # Some SIGINT signals are not properly interupted by python and converted # into KeyboardInterrupts, so we have to register a signal handler to # safeguard against such cases. This seems to be the case when conky is # launched as a subprocess, making it the process that receives the SIGINT # signal and not python. These signal handlers cause issues for \ # NamedTemporaryFile.close() though, so they are only registrered when # we are not testing. if not test: signal.signal(signal.SIGINT, exit_handler) # Also catch kill-signkal from OS, # e.g. `kill $(pgrep -f "python astrality.py")` signal.signal(signal.SIGTERM, exit_handler) try: ( config, module_configs, global_context, directory, ) = user_configuration() if modules: config['modules']['enabled_modules'] = [{ 'name': module_name } for module_name in modules] # Delay further actions if configuration says so time.sleep(config['astrality']['startup_delay']) module_manager = ModuleManager( config=config, modules=module_configs, context=global_context, directory=directory, dry_run=dry_run, ) module_manager.finish_tasks() while True: if module_manager.has_unfinished_tasks(): # TODO: Log which new event which has been detected logger.info('New event detected.') module_manager.finish_tasks() logger.info(f'Event change routine finished.') if test or dry_run: logger.debug('Main loop interupted due to --dry-run.') return elif not module_manager.keep_running: logger.info( 'No more tasks to be performed. ' 'Executing on_exit blocks.', ) module_manager.exit() return else: logger.info( f'Waiting {module_manager.time_until_next_event()} ' 'until next event change and ensuing update.', ) # Weird bug related to sleeping more than 10e7 seconds # on MacOS, causing OSError: Invalid Argument wait = module_manager.time_until_next_event().total_seconds() if wait >= 10e7: wait = 10e7 time.sleep(wait) except KeyboardInterrupt: # pragma: no cover exit_handler()