Files
lix/tests/functional2/testlib/fixtures/file_helper.py
T
2025-05-07 16:04:06 +02:00

190 lines
6.2 KiB
Python

import shutil
from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path
from typing import Any, Dict
import pytest
from functional2.testlib.fixtures.formatter import BalancedTemplater
class Fileish(ABC):
"""
Baseclass, which allows files to be copied declaratively
"""
@abstractmethod
def copy_to(self, path: Path, origin: Path) -> None:
"""
Copies this file to the given TempDir
:param path: TempDir for the test
:param origin: Directory the tests originates in. Used to adjust relative paths
"""
pass
class _ByContentFileish(Fileish, ABC):
def __init__(self, mode: int | None = None):
self.mode = mode
@abstractmethod
def get_content(self, origin: Path) -> str:
"""
Returns the content, which should be present in the current path
:return: content as a string
"""
pass
def copy_to(self, path: Path, origin: Path) -> None:
path.write_text(self.get_content(origin))
if self.mode is not None:
path.chmod(self.mode)
class File(_ByContentFileish):
def __init__(self, file_contents: str, mode: int | None = None):
"""
Declares a file by its content
:param file_contents: content of the file as a string
:param mode: Optionally change the mode of the file (e.g. to executable)
"""
super().__init__(mode)
self.file_contents = file_contents
def get_content(self, _: Path) -> str:
return self.file_contents
class CopyFile(Fileish):
def __init__(self, source: str):
"""
Declares a file as a copy of an existing file
:param source: Path to the file to be copied
"""
self.source = source
def copy_to(self, path: Path, origin: Path):
shutil.copyfile(origin / self.source, path)
class CopyTree(Fileish):
def __init__(self, tree_base: str):
"""
Declares a folder as a copy of an existing folder
:param tree_base: base folder of the tree being copied
"""
self.tree_base = tree_base
def copy_to(self, path: Path, origin: Path):
shutil.copytree(origin / self.tree_base, path, dirs_exist_ok=True)
class CopyTemplate(_ByContentFileish):
def __init__(self, template: str, values: Dict[str, Any], mode: int | None = None):
"""
Declares a file as an initiated version of the given file template
:param template: source template's file name. Parameters formatted as `{key_name}` are replaced by corresponding values
:param values: dictionary of key_name and value to be replaced in the template.
:param mode: Optionally change the mode of the file (e.g. to executable)
"""
self.template = template
self.values = values
self.content: str | None = None
super().__init__(mode)
def get_content(self, origin: Path) -> str:
template_path = origin / self.template
template_content = template_path.read_text()
self.content = BalancedTemplater(template_content).substitute(**self.values)
return self.content
class RelativeTo(str, Enum):
TEST = "test"
"""
Base for the given path is the directory of the test
"""
TARGET = "target"
"""
Base for the given path is the directory of the target / where all files are created
"""
ROOT = "root"
"""
Base for the given path is the root folder (/)
"""
SELF = "self"
"""
No base path is given, used to create relative symlinks (e.g. `"../test"`)
"""
class Symlink(Fileish):
def __init__(self, source: str, relative_to: RelativeTo = RelativeTo.TARGET):
"""
Declares a file as a symlink to a different location
NOTE: due to limitations of symlinks on Windows, tests using this might be flakey and fail!!
:param source: Path to the source where the symlink should be pointing
:raise ValueError: When the given source is an absolute Path, but relative to wasn't set to ROOT
"""
self.source = source
self.relative_to = relative_to
def copy_to(self, path: Path, origin: Path):
if self.relative_to is not RelativeTo.ROOT and Path(self.source).is_absolute():
msg = "absolute paths are only supported when using RelativeTo.ROOT"
raise ValueError(msg)
match self.relative_to:
case RelativeTo.TEST:
base = origin
case RelativeTo.TARGET:
base = path.parent
case RelativeTo.ROOT:
base = Path("/")
case RelativeTo.SELF:
base = None
case _:
msg = f"unknown relativity {self.relative_to}"
raise ValueError(msg)
target_path = base / self.source if base is not None else Path(self.source)
path.symlink_to(target_path)
type FileDeclaration = Dict[str, Fileish | "FileDeclaration"]
def _init_files(files: FileDeclaration, tmp_path: Path, request: pytest.FixtureRequest) -> None:
"""
This internal function is needed because one cannot call a fixture directly since pytest 4.0
"""
for name, definition in files.items():
destination = tmp_path / name
if isinstance(definition, Fileish):
definition.copy_to(destination, request.path.parent)
else:
# Initialize subdirectory
destination.mkdir()
_init_files(definition, destination, request)
@pytest.fixture
def files(tmp_path: Path, request: pytest.FixtureRequest) -> Path:
"""
Initializes the given files into the TempDir of the test.
This ensures all necessary files and only those are present
To use this add `@pytest.mark.parametrize("files", [list_of_your_files_to_test, more_files_to_test], indirect=True)` above your test.
The test is run once for each of the sets of files provided as the second argument.
Each Set of files should be of the :py:type:`FileDeclaration` type
:param tmp_path: TempDir of the test
:param request: Fixture information provided by pytest, used to parametrize the files
:return: Path to where the files were created
"""
_init_files(request.param, tmp_path, request)
return tmp_path