tests/functional2: Make symlink handling less confusing
The current `RelativeTo` design is both more complex and more confusing than necessary. Its four variants are now reduced to only two. They are now also represented as different classes, to better communicate the difference in semantics and also intent. Change-Id: Ia60fc7a2dfa0f62bdef90dde347fd8603fd3fbf9
This commit is contained in:
@@ -167,7 +167,8 @@ The input type is [FileDeclaration](./testlib/fixtures/file_helper.py), a dict f
|
||||
- `CopyFile("input/path")`: Copy the specified local file
|
||||
- `CopyTree("input/path")`: Like `CopyFile`, but recursively
|
||||
- `CopyTemplate("input/path", { "replace": "with"})`: Like `CopyFile`, but with an additional set of substitutions where each instance of "@key@" in the input file will be replaced by its associated value.
|
||||
- `Symlink("target/path")`
|
||||
- `Symlink("target/path")`: Create a symlink with the specified target. Therefore, relative paths are relative to the symlink's location.
|
||||
- `AssetSymlink("source/path)`: Create a symlink pointing to a local asset file within the test suite. Paths must be relative and will be resolved relative to the current Python file. The created symlink will be absolute.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
@@ -199,12 +200,12 @@ In order for the golden values to actually update within the code base (compared
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from functional2.testlib.fixtures.file_helper import Symlink, RelativeTo
|
||||
from functional2.testlib.fixtures.file_helper import AssetSymlink
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"files",
|
||||
# Setup the symlink so that the golden value will update
|
||||
[ { "out": Symlink("assets/test_example/out.exp", RelativeTo.TEST), } ],
|
||||
[ { "out": AssetSymlink("assets/test_example/out.exp"), } ],
|
||||
indirect=True,
|
||||
)
|
||||
def test_example(files, snapshot):
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Any
|
||||
import toml
|
||||
from toml import TomlDecodeError
|
||||
|
||||
from functional2.testlib.fixtures.file_helper import FileDeclaration, CopyFile, Symlink, RelativeTo
|
||||
from functional2.testlib.fixtures.file_helper import FileDeclaration, CopyFile, AssetSymlink
|
||||
from functional2.testlib.utils import test_base_folder
|
||||
|
||||
|
||||
@@ -87,12 +87,8 @@ class LangTest:
|
||||
files = {
|
||||
"in.nix": CopyFile(f"{self.folder}/in{self.suffix}.nix"),
|
||||
"lib.nix": CopyFile("lib.nix"),
|
||||
"out.exp": Symlink(
|
||||
f"{self.folder}/{self.test_name}{self.suffix}.out.exp", relative_to=RelativeTo.TEST
|
||||
),
|
||||
"err.exp": Symlink(
|
||||
f"{self.folder}/{self.test_name}{self.suffix}.err.exp", relative_to=RelativeTo.TEST
|
||||
),
|
||||
"out.exp": AssetSymlink(f"{self.folder}/{self.test_name}{self.suffix}.out.exp"),
|
||||
"err.exp": AssetSymlink(f"{self.folder}/{self.test_name}{self.suffix}.err.exp"),
|
||||
}
|
||||
for file in self.extra_files:
|
||||
# Make sure to add the extra-files requested by the test.toml
|
||||
|
||||
@@ -4,7 +4,7 @@ from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
from functional2.testlib.commands import Command
|
||||
from functional2.testlib.fixtures.file_helper import File, RelativeTo, Symlink
|
||||
from functional2.testlib.fixtures.file_helper import File, AssetSymlink
|
||||
from functional2.testlib.fixtures.snapshot import Snapshot
|
||||
from functional2.testlib.utils import get_functional2_lang_files
|
||||
|
||||
@@ -111,13 +111,11 @@ def test_skips_py_files(files: Path, pytest_command: Command):
|
||||
"n_suffix": {
|
||||
"in-1.nix": File("{}"),
|
||||
"in-2.nix": File("{}"),
|
||||
"eval-okay-1.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay-1.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
"eval-okay-2.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay-2.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -148,13 +146,11 @@ def test_collects_with_n_suffix(pytest_command: Command):
|
||||
"short_string_suffix": {
|
||||
"in-speaker.nix": File("{}"),
|
||||
"in-microphone.nix": File("{}"),
|
||||
"eval-okay-speaker.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay-speaker.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
"eval-okay-microphone.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay-microphone.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -185,13 +181,11 @@ def test_collects_short_string_suffix(pytest_command: Command):
|
||||
"dash_suffix": {
|
||||
"in-string-with-dash.nix": File("{}"),
|
||||
"in-some-more--dashes.nix": File("{}"),
|
||||
"eval-okay-string-with-dash.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay-string-with-dash.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
"eval-okay-some-more--dashes.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay-some-more--dashes.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -223,13 +217,11 @@ def test_collects_string_suffix_with_dash(pytest_command: Command):
|
||||
"bad_naming": {
|
||||
"in-&x.nix": File("{}"),
|
||||
"in-.nix": File("{}"),
|
||||
"eval-okay-&x.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay-&x.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
"eval-okay-.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay-.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -260,24 +252,20 @@ def test_collection_fails_with_bad_naming(pytest_command: Command):
|
||||
"lang": {
|
||||
"infra_okay_runners": {
|
||||
"in.nix": File("{}"),
|
||||
"eval-okay.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-okay.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp"
|
||||
),
|
||||
"parse-okay.out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_po.out.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"parse-okay.out.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_po.out.exp"
|
||||
),
|
||||
},
|
||||
"infra_fail_runners": {
|
||||
"in.nix": File("{"),
|
||||
"eval-fail.err.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_ef.err.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"eval-fail.err.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_ef.err.exp"
|
||||
),
|
||||
"parse-fail.err.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_pf.err.exp",
|
||||
relative_to=RelativeTo.TEST,
|
||||
"parse-fail.err.exp": AssetSymlink(
|
||||
"assets/test_lang_infra/runner_pf.err.exp"
|
||||
),
|
||||
},
|
||||
}
|
||||
@@ -495,9 +483,7 @@ def test_invalid_toml(pytest_command: Command):
|
||||
"update_test": {"in.nix": File("{}"), "eval-okay.out.exp": File("old")}
|
||||
}
|
||||
},
|
||||
"out.exp": Symlink(
|
||||
"assets/test_lang_infra/runner_eo.out.exp", relative_to=RelativeTo.TEST
|
||||
),
|
||||
"out.exp": AssetSymlink("assets/test_lang_infra/runner_eo.out.exp"),
|
||||
}
|
||||
),
|
||||
(["-k", "update_test", "--accept-tests"], False),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import shutil
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
@@ -103,54 +102,37 @@ class CopyTemplate(_ByContentFileish):
|
||||
return self.content
|
||||
|
||||
|
||||
class RelativeTo(StrEnum):
|
||||
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):
|
||||
def __init__(self, target: str):
|
||||
"""
|
||||
Declares a file as a symlink to a different location
|
||||
Declares a file as a symlink to a different location.
|
||||
|
||||
NOTE: due to limitations of symlinks on Windows, tests using this might be flaky and fail!!
|
||||
:param target: Path to the target where the symlink should be pointing
|
||||
"""
|
||||
self.target = target
|
||||
|
||||
def copy_to(self, path: Path, _origin: Path):
|
||||
path.symlink_to(self.target)
|
||||
|
||||
|
||||
class AssetSymlink(Fileish):
|
||||
def __init__(self, source: str):
|
||||
"""
|
||||
Declares a file as a symlink to a local asset file.
|
||||
|
||||
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
|
||||
:param source: Path to the source where the symlink should be pointing.
|
||||
Paths are relative to the current test's module.
|
||||
:raise ValueError: When the given source path is absolute
|
||||
"""
|
||||
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"
|
||||
if Path(self.source).is_absolute():
|
||||
msg = "absolute paths are not allowed"
|
||||
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)
|
||||
target_path = origin / self.source
|
||||
path.symlink_to(target_path)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from functional2.testlib.fixtures.file_helper import (
|
||||
CopyTree,
|
||||
File,
|
||||
Symlink,
|
||||
RelativeTo,
|
||||
AssetSymlink,
|
||||
)
|
||||
|
||||
|
||||
@@ -190,11 +190,9 @@ def test_mode_setting(files: Path):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"files",
|
||||
[{"a": Symlink("assets/test_file_helper/copy_file_test.txt", RelativeTo.TEST)}],
|
||||
indirect=True,
|
||||
"files", [{"a": AssetSymlink("assets/test_file_helper/copy_file_test.txt")}], indirect=True
|
||||
)
|
||||
def test_file_symlink(files: Path):
|
||||
def test_asset_symlink(files: Path):
|
||||
file = files / "a"
|
||||
assert file.exists(follow_symlinks=False)
|
||||
|
||||
@@ -206,23 +204,7 @@ def test_file_symlink(files: Path):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"files",
|
||||
[{"a": Symlink("test_folder/b.txt", RelativeTo.TARGET), "test_folder": {"b.txt": File("zzz")}}],
|
||||
indirect=True,
|
||||
)
|
||||
def test_file_symlink_target(files: Path):
|
||||
file = files / "a"
|
||||
assert file.exists(follow_symlinks=False)
|
||||
|
||||
assert file.is_symlink()
|
||||
|
||||
assert file.readlink().exists()
|
||||
|
||||
assert file.read_text() == "zzz"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"files", [{"a": Symlink("assets/test_file_helper/test_folder", RelativeTo.TEST)}], indirect=True
|
||||
"files", [{"a": AssetSymlink("assets/test_file_helper/test_folder")}], indirect=True
|
||||
)
|
||||
def test_dir_symlink(files: Path):
|
||||
folder = files / "a"
|
||||
@@ -238,23 +220,29 @@ def test_dir_symlink(files: Path):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"files",
|
||||
[{"a": Symlink("assets/test_file_helper/this_does_not_exist", RelativeTo.TEST)}],
|
||||
indirect=True,
|
||||
"files", [{"a": AssetSymlink("assets/test_file_helper/this_does_not_exist")}], indirect=True
|
||||
)
|
||||
def test_invalid_symlink(files: Path):
|
||||
def test_invalid_asset_symlink(files: Path):
|
||||
link = files / "a"
|
||||
assert link.exists(follow_symlinks=False)
|
||||
assert link.is_symlink()
|
||||
assert not link.readlink().exists()
|
||||
|
||||
|
||||
@pytest.mark.xfail(raises=ValueError)
|
||||
@pytest.mark.parametrize(
|
||||
"files",
|
||||
[{"tg": File("Hello World"), "folder": {"link": Symlink("../tg", RelativeTo.SELF)}}],
|
||||
[{"a": AssetSymlink("/absolute/assets/test_file_helper/this_does_not_exist")}],
|
||||
indirect=True,
|
||||
)
|
||||
def test_direct_relativity_symlink(files: Path):
|
||||
def test_absolute_asset_symlink(files: Path):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"files", [{"tg": File("Hello World"), "folder": {"link": Symlink("../tg")}}], indirect=True
|
||||
)
|
||||
def test_file_symlink(files: Path):
|
||||
assert (files / "tg").exists()
|
||||
link = files / "folder" / "link"
|
||||
assert link.exists(follow_symlinks=False)
|
||||
|
||||
Reference in New Issue
Block a user