示例#1
0
def test_creation_of_backup(create_temp_files):
    """Existing external files should be backed up."""
    target, template = create_temp_files(2)

    # This file is the original and should be backed up
    target.write_text('original')

    # This is the new content compiled to target
    template.write_text('new')

    compile_dict = {
        'content': str(template.name),
        'target': str(target),
    }
    compile_action = CompileAction(
        options=compile_dict,
        directory=template.parent,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    # We replace the content by executing the action
    compile_action.execute()
    assert target.read_text() == 'new'

    # And when cleaning up the module, the backup should be restored
    CreatedFiles().cleanup(module='test')
    assert target.read_text() == 'original'
示例#2
0
def test_running_symlink_action_twice(create_temp_files):
    """Symlink action should be idempotent."""
    content, target = create_temp_files(2)
    content.write_text('content')
    target.write_text('target')

    symlink_options = {
        'content': str(content),
        'target': str(target),
    }
    symlink_action = SymlinkAction(
        options=symlink_options,
        directory=content.parent,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    # Symlink first time
    symlink_action.execute()
    assert target.is_symlink()
    assert target.read_text() == 'content'

    # A backup shoud be created
    backup = CreatedFiles().creations['test'][str(target)]['backup']
    assert Path(backup).read_text() == 'target'

    # Symlink one more time, and assert idempotency
    symlink_action.execute()
    assert target.is_symlink()
    assert target.read_text() == 'content'

    backup = CreatedFiles().creations['test'][str(target)]['backup']
    assert Path(backup).read_text() == 'target'
示例#3
0
def test_that_temporary_compile_targets_have_deterministic_paths(tmpdir):
    """Created compilation targets should be deterministic."""
    template_source = Path(tmpdir, 'template.tmp')
    template_source.write_text('content')

    compile_dict = {
        'content': str(template_source),
    }
    compile_action1 = CompileAction(
        options=compile_dict.copy(),
        directory=Path('/'),
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    compile_action2 = CompileAction(
        options=compile_dict.copy(),
        directory=Path('/'),
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    target1 = compile_action1.execute()[template_source]
    target2 = compile_action2.execute()[template_source]
    assert target1 == target2
示例#4
0
def test_backup_of_symlink_target(create_temp_files):
    """Overwritten copy targets should be backed up."""
    target, content = create_temp_files(2)

    # This file is the original and should be backed up
    target.write_text('original')

    # This is the new content which will be symlinked to
    content.write_text('new')

    symlink_options = {
        'content': str(content.name),
        'target': str(target),
    }
    symlink_action = SymlinkAction(
        options=symlink_options,
        directory=content.parent,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    # We replace the content by executing the action
    symlink_action.execute()
    assert target.resolve().read_text() == 'new'

    # And when cleaning up the module, the backup should be restored
    CreatedFiles().cleanup(module='test')
    assert target.read_text() == 'original'
示例#5
0
def test_run_timeout_specified_in_action_block(tmpdir):
    """
    Run actions can time out.

    The option `timeout` overrides any timeout providided to `execute()`.
    """
    temp_dir = Path(tmpdir)
    run_action = RunAction(
        options={
            'shell': 'sleep 0.1 && echo hi',
            'timeout': 0.05
        },
        directory=temp_dir,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    _, result = run_action.execute(default_timeout=10000)
    assert result == ''

    run_action = RunAction(
        options={
            'shell': 'sleep 0.1 && echo hi',
            'timeout': 0.2
        },
        directory=temp_dir,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    _, result = run_action.execute(default_timeout=0)
    assert result == 'hi'
示例#6
0
def test_run_timeout_specified_in_execute(tmpdir, caplog):
    """
    Run actions can time out, and should log this.

    The the option `timeout` is not specified, use `default_timeout` argument
    instead.
    """
    temp_dir = Path(tmpdir)
    run_action = RunAction(
        options={'shell': 'sleep 0.1 && echo hi'},
        directory=temp_dir,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    caplog.clear()
    _, result = run_action.execute(default_timeout=0.05)

    assert 'used more than 0.05 seconds' in caplog.record_tuples[1][2]
    assert result == ''

    run_action = RunAction(
        options={'shell': 'sleep 0.1 && echo hi'},
        directory=temp_dir,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    _, result = run_action.execute(default_timeout=0.2)
    assert result == 'hi'
示例#7
0
def test_cleanup_of_created_directory(create_temp_files, tmpdir):
    """Created directories should be cleaned up."""
    tmpdir = Path(tmpdir)
    [content] = create_temp_files(1)

    # The target requires a new directory to be created
    directory = tmpdir / 'dir'
    target = directory / 'target.tmp'

    # Execute the symlink action
    symlink_options = {
        'content': str(content.name),
        'target': str(target),
    }
    created_files = CreatedFiles().wrapper_for(module='test')
    symlink_action = SymlinkAction(
        options=symlink_options,
        directory=content.parent,
        replacer=lambda x: x,
        context_store={},
        creation_store=created_files,
    )
    symlink_action.execute()

    # The directory should now exist and be persisted
    assert directory.is_dir()
    assert directory in created_files.creation_store

    # But it should be deleted on cleanup
    CreatedFiles().cleanup(module='test')
    assert not directory.is_dir()
示例#8
0
def test_creating_created_files_object_for_specific_module(create_temp_files):
    """You should be able to construct a CreatedFiles wrapper for a module."""
    content, target = create_temp_files(2)
    created_files = CreatedFiles().wrapper_for(module='my_module')
    created_files.insert_creation(
        content=content,
        target=target,
        method=CreationMethod.SYMLINK,
    )
示例#9
0
def test_that_inserting_non_existent_file_is_skipped(create_temp_files, ):
    """When creations have been deleted they should be skipped."""
    content, target = create_temp_files(2)
    target.unlink()

    created_files = CreatedFiles()
    created_files.insert(
        module='name',
        creation_method=CreationMethod.COPY,
        contents=[content],
        targets=[target],
    )

    # No file has been created!
    assert created_files.by(module='name') == []
示例#10
0
def test_setting_permissions_on_target_copy(tmpdir):
    """If permissions is provided, use it for the target."""
    temp_dir = Path(tmpdir) / 'content'
    temp_dir.mkdir()

    target = Path(tmpdir) / 'target'
    target.mkdir()

    file1 = temp_dir / 'file1'
    file1.touch()
    file1.chmod(0o770)

    copy_options = {
        'content': str(file1),
        'target': str(target),
        'include': r'file1',
        'permissions': '777',
    }
    copy_action = CopyAction(
        options=copy_options,
        directory=temp_dir,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    copy_action.execute()

    assert ((target / 'file1').stat().st_mode & 0o000777) == 0o777
示例#11
0
def test_filtering_stowed_templates(test_config_directory, tmpdir):
    """Users should be able to restrict compilable templates with ignore."""
    temp_dir = Path(tmpdir)
    templates = \
        test_config_directory / 'test_modules' / 'using_all_actions'
    stow_dict = {
        'content': str(templates),
        'target': str(temp_dir),
        'templates': r'.+\.template',
        'non_templates': 'ignore',
    }
    stow_action = StowAction(
        options=stow_dict,
        directory=test_config_directory,
        replacer=lambda x: x,
        context_store={'geography': {
            'capitol': 'Berlin'
        }},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    # First testing if dry run is respected (too much work for a separate test)
    stow_action.execute(dry_run=True)
    assert len(list(temp_dir.iterdir())) == 0

    # We should have a total of two stowed files
    stow_action.execute()
    assert len(list(temp_dir.iterdir())) == 2
    assert len(list((temp_dir / 'recursive').iterdir())) == 1
    assert (temp_dir / 'module.template').is_file()
    assert (temp_dir / 'recursive' / 'empty.template').is_file()
示例#12
0
def test_symlinking_file_to_directory(tmpdir):
    """If symlinking from directory to file, place file in directory."""
    temp_dir = Path(tmpdir) / 'content'
    temp_dir.mkdir()

    target = Path(tmpdir) / 'target'
    target.mkdir()

    file1 = temp_dir / 'file1'
    file1.touch()

    symlink_options = {
        'content': str(file1),
        'target': str(target),
        'include': r'file1',
    }
    symlink_action = SymlinkAction(
        options=symlink_options,
        directory=temp_dir,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    symlink_action.execute()

    assert (target / 'file1').is_symlink()
    assert (target / 'file1').resolve() == file1
    assert symlink_action.symlinked_files == {
        file1: {target / 'file1'},
    }
示例#13
0
def test_that_dry_run_is_respected(tmpdir, caplog):
    """If dry_run is True, no commands should be executed, only logged."""
    temp_dir = Path(tmpdir)
    run_action = RunAction(
        options={
            'shell': 'touch touched.tmp',
            'timeout': 1
        },
        directory=temp_dir,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    caplog.clear()
    result = run_action.execute(dry_run=True)

    # Command to be run and empty string should be returned
    assert result == ('touch touched.tmp', '')

    # Command to be run should be logged
    assert 'SKIPPED: ' in caplog.record_tuples[0][2]
    assert 'touch touched.tmp' in caplog.record_tuples[0][2]

    # Check that the command has *not* been run
    assert not (temp_dir / 'touched.tmp').is_file()
示例#14
0
def test_filtering_compiled_templates(test_config_directory, tmpdir):
    """Users should be able to restrict compilable templates."""
    temp_dir = Path(tmpdir)
    templates = \
        test_config_directory / 'test_modules' / 'using_all_actions'
    compile_dict = {
        'content': str(templates),
        'target': str(temp_dir),
        'include': r'.+\.template',
    }
    compile_action = CompileAction(
        options=compile_dict,
        directory=test_config_directory,
        replacer=lambda x: x,
        context_store={'geography': {
            'capitol': 'Berlin'
        }},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    compile_action.execute()

    # We should have a total of two compiled files
    assert len(list(temp_dir.iterdir())) == 2
    assert len(list((temp_dir / 'recursive').iterdir())) == 1
    assert (temp_dir / 'module.template').is_file()
    assert (temp_dir / 'recursive' / 'empty.template').is_file()
示例#15
0
def test_renaming_templates(test_config_directory, tmpdir):
    """Templates targets should be renameable with a capture group."""
    temp_dir = Path(tmpdir)
    templates = \
        test_config_directory / 'test_modules' / 'using_all_actions'

    # Multiple capture groups should be allowed
    compile_dict = {
        'content': str(templates),
        'target': str(temp_dir),
        'include': r'(?:^template\.(.+)$|^(.+)\.template$)',
    }
    compile_action = CompileAction(
        options=compile_dict,
        directory=test_config_directory,
        replacer=lambda x: x,
        context_store={'geography': {
            'capitol': 'Berlin'
        }},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    compile_action.execute()

    # We should have a total of two compiled files
    assert len(list(temp_dir.iterdir())) == 2
    assert len(list((temp_dir / 'recursive').iterdir())) == 1
    assert (temp_dir / 'module').is_file()
    assert (temp_dir / 'recursive' / 'empty').is_file()
示例#16
0
def test_compiling_entire_directory(test_config_directory, tmpdir):
    """All directory contents should be recursively compiled."""
    temp_dir = Path(tmpdir).resolve()
    templates = \
        test_config_directory / 'test_modules' / 'using_all_actions'

    # TODO: Make this unecessary
    for file in templates.glob('**/*.tmp'):
        file.unlink()

    compile_dict = {
        'content': str(templates),
        'target': str(temp_dir),
    }
    compile_action = CompileAction(
        options=compile_dict,
        directory=test_config_directory,
        replacer=lambda x: x,
        context_store={'geography': {
            'capitol': 'Berlin'
        }},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    results = compile_action.execute()

    # Check if return content is correct, showing performed compilations
    assert templates / 'module.template' in results
    assert results[templates / 'module.template'] == \
        temp_dir / 'module.template'

    # Check if the templates actually have been compiled
    target_dir_content = list(temp_dir.iterdir())
    assert len(target_dir_content) == 6
    assert temp_dir / 'module.template' in target_dir_content
    assert (temp_dir / 'recursive' / 'empty.template').is_file()
示例#17
0
def test_retrieving_all_compiled_templates(template_directory, tmpdir):
    """Compile actions should return all compiled templates."""
    target1, target2 = Path(tmpdir) / 'target.tmp', Path(tmpdir) / 'target2'
    targets = [target1, target2]
    template = Path('no_context.template')
    compile_dict = {
        'content': str(template),
        'target': '{target}',
    }

    # First replace {target} with target1, then with target2, by doing some
    # trickery with the replacer function.
    compile_action = CompileAction(
        options=compile_dict,
        directory=template_directory,
        replacer=lambda x: x.format(target=targets.pop(), )
        if x == '{target}' else x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    assert compile_action.performed_compilations() == {}

    compile_action.execute()
    assert compile_action.performed_compilations() == {
        template_directory / template: {target2},
    }

    compile_action.execute()
    assert compile_action.performed_compilations() == {
        template_directory / template: {target1, target2},
    }
示例#18
0
def test_use_of_replacer(template_directory, tmpdir):
    """All options should be run through the replacer."""
    compile_dict = {
        'content': 'template',
        'target': 'target',
        'permissions': 'permissions',
    }

    template = template_directory / 'no_context.template'
    target = Path(tmpdir) / 'target'

    def replacer(string: str) -> str:
        """Trivial replacer."""
        if string == 'template':
            return template.name
        elif string == 'target':
            return str(target)
        elif string == 'permissions':
            return '777'
        else:
            return string

    compile_action = CompileAction(
        options=compile_dict,
        directory=template_directory,
        replacer=replacer,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    target = list(compile_action.execute().values())[0]
    assert target.read_text() == 'one\ntwo\nthree'
    assert (target.stat().st_mode & 0o777) == 0o777
示例#19
0
def test_compilation_with_context(template_directory):
    """
    Templates should be compiled with the context store.

    It should compile differently after mutatinig the store.
    """
    compile_dict = {
        'content': 'test_template.conf',
    }
    context_store = {}

    compile_action = CompileAction(
        options=compile_dict,
        directory=template_directory,
        replacer=lambda x: x,
        context_store=context_store,
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    context_store['fonts'] = {2: 'ComicSans'}
    target = list(compile_action.execute().values())[0]

    username = os.environ.get('USER')
    assert target.read_text() == f'some text\n{username}\nComicSans'

    context_store['fonts'] = {2: 'TimesNewRoman'}
    target = list(compile_action.execute().values())[0]
    assert target.read_text() == f'some text\n{username}\nTimesNewRoman'
示例#20
0
def test_symlinking_non_templates(test_config_directory, tmpdir):
    """Non-templates files should be implicitly symlinked."""
    temp_dir = Path(tmpdir)
    templates = \
        test_config_directory / 'test_modules' / 'using_all_actions'
    stow_dict = {
        'content': str(templates),
        'target': str(temp_dir),
        'templates': r'.+\.template',
    }
    stow_action = StowAction(
        options=stow_dict,
        directory=test_config_directory,
        replacer=lambda x: x,
        context_store={'geography': {
            'capitol': 'Berlin'
        }},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    stow_action.execute()

    # Templates should still stowed
    target_dir_content = list(temp_dir.iterdir())
    assert len(target_dir_content) == 6
    assert temp_dir / 'module.template' in target_dir_content
    assert not (temp_dir / 'module.template').is_symlink()
    assert (temp_dir / 'recursive' / 'empty.template').is_file()

    # The rest should be symlinked
    assert (temp_dir / 'modules.yml').is_symlink()
    assert (temp_dir / 'modules.yml').resolve() == templates / 'modules.yml'

    # Symlinked files should be not considered as a managed file, as it is
    # self-updating.
    assert templates / 'modules.yml' not in stow_action.managed_files()
示例#21
0
def test_running_shell_command_with_environment_variable(caplog):
    """Shell commands should have access to the environment."""
    run_action = RunAction(
        options={
            'shell': 'echo $USER',
            'timeout': 2
        },
        directory=Path('/'),
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    caplog.clear()
    run_action.execute()
    assert caplog.record_tuples == [
        (
            'astrality.actions',
            logging.INFO,
            f'Running command "echo {os.environ["USER"]}".',
        ),
        (
            'astrality.utils',
            logging.INFO,
            os.environ['USER'],
        ),
    ]
示例#22
0
def test_that_dry_run_skips_compilation(template_directory, tmpdir, caplog):
    """If dry_run is True, skip compilation of template"""
    compilation_target = Path(tmpdir, 'target.tmp')
    template = template_directory / 'no_context.template'
    compile_dict = {
        'content': 'no_context.template',
        'target': str(compilation_target),
    }
    compile_action = CompileAction(
        options=compile_dict,
        directory=template_directory,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    caplog.clear()
    compilations = compile_action.execute(dry_run=True)

    # Check that the "compilation" is actually logged
    assert 'SKIPPED:' in caplog.record_tuples[0][2]
    assert str(template) in caplog.record_tuples[0][2]
    assert str(compilation_target) in caplog.record_tuples[0][2]

    # The template should still be returned
    assert template in compilations

    # And the compilation pair should be persisted
    assert compile_action.performed_compilations() == {
        template: {compilation_target},
    }

    # But the file should not be compiled
    assert not compilations[template].exists()
示例#23
0
def test_symlink_dry_run(create_temp_files, caplog):
    """If dry_run is True, only log and not symlink."""
    content, target = create_temp_files(2)
    symlink_action = SymlinkAction(
        options={
            'content': str(content),
            'target': str(target)
        },
        directory=Path('/'),
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    caplog.clear()
    result = symlink_action.execute(dry_run=True)

    # We should log the symlink that had been performed
    assert 'SKIPPED:' in caplog.record_tuples[0][2]
    assert str(content) in caplog.record_tuples[0][2]
    assert str(target) in caplog.record_tuples[0][2]

    # We should also still return the intended result
    assert result == {content: target}

    # But the symlink should not be created in a dry run
    assert not target.is_symlink()
def test_that_replacer_is_run_every_time(context_directory):
    """
    The replacer should be run a new every time self.execute() is invoked.
    """
    context_import_dict = {
        'from_path': 'several_sections.yml',
        'from_section': 'section1',
        'to_section': 'whatever',
    }
    context_store = Context()

    class Replacer:
        def __init__(self) -> None:
            self.invoke_number = 0

        def __call__(self, option: str) -> str:
            self.invoke_number += 1
            return option

    replacer = Replacer()
    import_context_action = ImportContextAction(
        options=context_import_dict,
        directory=context_directory,
        replacer=replacer,
        context_store=context_store,
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    import_context_action.execute()
    assert replacer.invoke_number == 3

    import_context_action.execute()
    assert replacer.invoke_number == 6
def test_importing_entire_file(context_directory):
    """
    Test importing all sections from context file.

    All context sections should be imported in the absence of `from_section`.
    """
    context_import_dict = {
        'from_path': 'several_sections.yml',
    }
    context_store = Context()
    import_context_action = ImportContextAction(
        options=context_import_dict,
        directory=context_directory,
        replacer=lambda x: x,
        context_store=context_store,
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    import_context_action.execute()

    expected_context = {
        'section1': {
            'k1_1': 'v1_1',
            'k1_2': 'v1_2',
        },
        'section2': {
            'k2_1': 'v2_1',
            'k2_2': 'v2_2',
        },
    }
    assert context_store == expected_context
示例#26
0
def test_if_dry_run_is_respected(create_temp_files, caplog):
    """When dry_run is True, the copy action should only be logged."""
    content, target = create_temp_files(2)
    content.write_text('content')
    target.write_text('target')

    copy_action = CopyAction(
        options={'content': str(content), 'target': str(target)},
        directory=Path('/'),
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )

    caplog.clear()
    result = copy_action.execute(dry_run=True)

    # We should still return the copy pair
    assert result == {content: target}

    # We should log what would have been done
    assert 'SKIPPED:' in caplog.record_tuples[0][2]
    assert str(content) in caplog.record_tuples[0][2]
    assert str(target) in caplog.record_tuples[0][2]

    # But we should not copy the file under a dry run
    assert target.read_text() == 'target'
示例#27
0
def test_copying_file_to_directory(tmpdir):
    """If copying from directory to file, place file in directory."""
    temp_dir = Path(tmpdir) / 'content'
    temp_dir.mkdir()

    target = Path(tmpdir) / 'target'
    target.mkdir()

    file1 = temp_dir / 'file1'
    file1.touch()

    copy_options = {
        'content': str(file1),
        'target': str(target),
        'include': r'file1',
    }
    copy_action = CopyAction(
        options=copy_options,
        directory=temp_dir,
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    copy_action.execute()

    assert (target / 'file1').read_text() == file1.read_text()
def test_null_object_pattern():
    """Test initializing action with no behaviour."""
    import_context_action = ImportContextAction(
        options={},
        directory=Path('/'),
        replacer=lambda x: x,
        context_store=Context(),
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    import_context_action.execute()
示例#29
0
def test_null_object_pattern():
    """Copy actions without options should do nothing."""
    symlink_action = SymlinkAction(
        options={},
        directory=Path('/'),
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    symlink_action.execute()
示例#30
0
def test_null_object_pattern():
    """Null objects should be executable."""
    run_action = RunAction(
        options={},
        directory=Path('/'),
        replacer=lambda x: x,
        context_store={},
        creation_store=CreatedFiles().wrapper_for(module='test'),
    )
    run_action.execute()