Exemple #1
0
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()
Exemple #2
0
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()
Exemple #3
0
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
Exemple #4
0
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()
Exemple #5
0
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()
Exemple #6
0
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'
Exemple #7
0
    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.',
            ),
        ]
Exemple #8
0
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()
Exemple #9
0
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()
Exemple #10
0
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()