mirror of
https://github.com/LnL7/nix-darwin.git
synced 2025-03-31 04:04:45 +00:00
Merge 1a68896a7a
into 75f8e4dbc5
This commit is contained in:
commit
ec0ab1a2bf
18 changed files with 1385 additions and 0 deletions
|
@ -36,6 +36,7 @@
|
|||
./system/defaults/ActivityMonitor.nix
|
||||
./system/defaults/WindowManager.nix
|
||||
./system/etc.nix
|
||||
./system/files
|
||||
./system/keyboard.nix
|
||||
./system/launchd.nix
|
||||
./system/nvram.nix
|
||||
|
|
|
@ -80,6 +80,7 @@ in
|
|||
# We run `etcChecks` again just in case someone runs `activate`
|
||||
# directly without `activate-user`.
|
||||
${cfg.activationScripts.etcChecks.text}
|
||||
${cfg.activationScripts.filesChecks.text}
|
||||
${cfg.activationScripts.extraActivation.text}
|
||||
${cfg.activationScripts.groups.text}
|
||||
${cfg.activationScripts.users.text}
|
||||
|
@ -96,6 +97,7 @@ in
|
|||
${cfg.activationScripts.keyboard.text}
|
||||
${cfg.activationScripts.fonts.text}
|
||||
${cfg.activationScripts.nvram.text}
|
||||
${cfg.activationScripts.files.text}
|
||||
|
||||
${cfg.activationScripts.postActivation.text}
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ in
|
|||
|
||||
ln -s ${cfg.build.patches}/patches $out/patches
|
||||
ln -s ${cfg.build.etc}/etc $out/etc
|
||||
ln -s ${cfg.build.files} $out/links.json
|
||||
ln -s ${cfg.path} $out/sw
|
||||
|
||||
mkdir -p $out/Library
|
||||
|
|
76
modules/system/files/default.nix
Normal file
76
modules/system/files/default.nix
Normal file
|
@ -0,0 +1,76 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
text = import ./write-text.nix {
|
||||
inherit lib;
|
||||
mkTextDerivation = name: text: pkgs.writeText "system-file-${name}" text;
|
||||
};
|
||||
|
||||
rawFiles = lib.filterAttrs (n: v: v.enable) config.system.file;
|
||||
|
||||
files = lib.mapAttrs' (name: value: lib.nameValuePair value.target {
|
||||
type = "link";
|
||||
inherit (value) source;
|
||||
}) rawFiles;
|
||||
|
||||
linksJSON = pkgs.writeText "system-files.json" (builtins.toJSON {
|
||||
version = 1;
|
||||
inherit files;
|
||||
});
|
||||
|
||||
emptyJSON = pkgs.writeText "empty-files.json" (builtins.toJSON {
|
||||
version = 1;
|
||||
files = {};
|
||||
});
|
||||
|
||||
linker = lib.getExe (pkgs.callPackage ./linker {});
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
system.file = lib.mkOption {
|
||||
type = lib.types.attrsOf (lib.types.submodule text);
|
||||
default = {};
|
||||
description = ''
|
||||
Set of files that have to be linked/copied out of the Nix store.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
assertions =
|
||||
let
|
||||
targets = lib.mapAttrsToList (name: value: value.target) rawFiles;
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion = lib.allUnique targets;
|
||||
message = ''
|
||||
Multiple files used with the same target path!
|
||||
|
||||
This was likely caused by explicitly setting the `target` attribute of a file.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
system.build.files = linksJSON;
|
||||
|
||||
system.activationScripts.filesChecks.text = ''
|
||||
echo "checking for systemwide file collisions..." >&2
|
||||
OLD=/run/current-system/links.json
|
||||
if [ ! -e "$OLD" ]; then
|
||||
OLD=${emptyJSON}
|
||||
fi
|
||||
CHECK_ONLY=1 ${linker} "$OLD" "$systemConfig"/links.json
|
||||
'';
|
||||
|
||||
system.activationScripts.files.text = ''
|
||||
echo "setting up files systemwide..." >&2
|
||||
OLD=/run/current-system/links.json
|
||||
if [ ! -e "$OLD" ]; then
|
||||
OLD=${emptyJSON}
|
||||
fi
|
||||
${linker} "$OLD" "$systemConfig"/links.json
|
||||
'';
|
||||
};
|
||||
}
|
22
modules/system/files/linker/default.nix
Normal file
22
modules/system/files/linker/default.nix
Normal file
|
@ -0,0 +1,22 @@
|
|||
{ lib, python3Packages }:
|
||||
|
||||
python3Packages.buildPythonApplication {
|
||||
pname = "linker";
|
||||
version = "0.1.0";
|
||||
pyproject = true;
|
||||
|
||||
src = ./.;
|
||||
|
||||
build-system = [ python3Packages.poetry-core ];
|
||||
|
||||
nativeCheckInputs = [
|
||||
python3Packages.pytestCheckHook
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Link files into place across a nix-darwin system";
|
||||
maintainers = [ lib.maintainers.samasaur ];
|
||||
mainProgram = "linker";
|
||||
platforms = lib.platforms.darwin;
|
||||
};
|
||||
}
|
262
modules/system/files/linker/linker/__init__.py
Normal file
262
modules/system/files/linker/linker/__init__.py
Normal file
|
@ -0,0 +1,262 @@
|
|||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
class FileType(StrEnum):
|
||||
LINK = "link"
|
||||
class TransactionType(StrEnum):
|
||||
LINK = "link"
|
||||
REMOVE = "remove"
|
||||
|
||||
@classmethod
|
||||
def from_file_type(cls, f: FileType):
|
||||
return cls(f.value)
|
||||
|
||||
@dataclass
|
||||
class FileInfo:
|
||||
"""The configuration for a managed path on-disk.
|
||||
|
||||
They have a source path in the Nix store.
|
||||
|
||||
They can be of the following types:
|
||||
- LINK: a symlink from the on-disk path pointing to the Nix store
|
||||
"""
|
||||
source: Path
|
||||
type: FileType
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, d):
|
||||
return cls(Path(d['source']), FileType(d['type']))
|
||||
|
||||
@dataclass
|
||||
class Transaction:
|
||||
"""An action that must be taken to synchronize one file's on-disk state.
|
||||
|
||||
Transactions always have an on-disk path. Unless they are of type REMOVE, they also have a path in the Nix store.
|
||||
|
||||
They can be of the following types:
|
||||
- LINK: create a symlink from the on-disk path pointing to the Nix store
|
||||
- REMOVE: remove any file/link at the on-disk path
|
||||
"""
|
||||
in_store: Path | None
|
||||
on_disk: Path
|
||||
type: TransactionType
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Transaction):
|
||||
return False
|
||||
if self.on_disk != other.on_disk:
|
||||
return False
|
||||
if self.type != other.type:
|
||||
return False
|
||||
# store path doesn't matter for remove transactions
|
||||
if self.in_store != other.in_store:
|
||||
return self.type == TransactionType.REMOVE
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def remove(cls, path):
|
||||
return cls(None, path, TransactionType.REMOVE)
|
||||
|
||||
def main():
|
||||
check_args()
|
||||
old_files, new_files = parse_links_files()
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
if len(problems) > 0:
|
||||
print("Detected problems at paths:")
|
||||
for problem in problems:
|
||||
print(f"- {problem}")
|
||||
print("Aborting")
|
||||
sys.exit(1)
|
||||
if "CHECK_ONLY" in os.environ.keys():
|
||||
sys.exit(0)
|
||||
removals = find_old_files(new_files, old_files)
|
||||
enclosing_directories = perform_transactions(transactions + removals, "DRY_RUN" in os.environ.keys())
|
||||
emptied_directories = only_empty(enclosing_directories)
|
||||
if len(emptied_directories) > 0:
|
||||
print("The following directories have been emptied; you may want to remove them")
|
||||
for directory in emptied_directories:
|
||||
print(f"- {directory}")
|
||||
|
||||
def check_args():
|
||||
"""Check that the linker was given exactly two arguments"""
|
||||
if len(sys.argv) != 3:
|
||||
print(f"Usage: {sys.argv[0]} <old_system_links.json> <new_system_links.json>")
|
||||
sys.exit(1)
|
||||
|
||||
def parse_links_files() -> tuple[dict[Path, FileInfo], dict[Path, FileInfo]]:
|
||||
"""Parse the first and second arguments as links files"""
|
||||
old = parse_links_file(sys.argv[1])
|
||||
new = parse_links_file(sys.argv[2])
|
||||
return old, new
|
||||
|
||||
def parse_links_file(filePath: str) -> dict[Path, FileInfo]:
|
||||
"""Read the given file, parse as JSON, and convert to a links dictionary"""
|
||||
with open(filePath, "r") as file:
|
||||
data = json.load(file)
|
||||
if data['version'] != 1:
|
||||
print(f"Unknown schema version in {filePath}")
|
||||
sys.exit(1)
|
||||
theDict: dict[Path, FileInfo] = {
|
||||
Path(k): FileInfo.from_dict(v)
|
||||
for
|
||||
(k,v)
|
||||
in
|
||||
data['files'].items()
|
||||
}
|
||||
return theDict
|
||||
|
||||
def check_files(new_files: dict[Path, FileInfo], old_files: dict[Path, FileInfo], adopt_identical_links: bool = False) -> tuple[list[Transaction], list[Path]]:
|
||||
"""Check the current state of the filesystem against the new links, generating a list of transactions to perform and problems that will occur.
|
||||
|
||||
This function will generate a list of transactions and problems incurred in the process of ensuring that every file in new_files is correct.
|
||||
It will not generate transactions to remove the remaining files in old_files.
|
||||
"""
|
||||
transactions: list[Transaction] = []
|
||||
problems: list[Path] = []
|
||||
path: Path
|
||||
|
||||
# Go through all files in the new generation
|
||||
for path in new_files:
|
||||
new_file: FileInfo = new_files[path]
|
||||
if not path.exists(follow_symlinks=False):
|
||||
# There is no file at this path
|
||||
transactions.append(Transaction(new_file.source, path, TransactionType.from_file_type(new_file.type)))
|
||||
else:
|
||||
# There is a file at this path
|
||||
# It could be a regular file or a symlink (including broken symlinks)
|
||||
|
||||
if not path.is_symlink():
|
||||
# The file is a regular file
|
||||
problems.append(path)
|
||||
else:
|
||||
# The file is a symlink
|
||||
|
||||
if path not in old_files:
|
||||
# The old generation did not have a file at this path.
|
||||
|
||||
link_target = path.readlink()
|
||||
# This handles both relative and absolute symlinks
|
||||
# If the link is relative, we need to prepend the parent
|
||||
# If the link is absolute, the prepended parent is ignored
|
||||
if path.parent / link_target == new_file.source:
|
||||
# The link already points to the new target
|
||||
if adopt_identical_links:
|
||||
# We are allowed to "adopt" these links and pretend as if we created them
|
||||
continue
|
||||
else:
|
||||
# We must treat this as a problem, so that undoing this generation will not remove this file created before this generation
|
||||
problems.append(path)
|
||||
else:
|
||||
# The link points somewhere else
|
||||
problems.append(path)
|
||||
else:
|
||||
# The old generation had a file at this path
|
||||
if old_files[path].type != FileType.LINK:
|
||||
# The old generation's file was not a link.
|
||||
# Because we know that the file on disk is a link,
|
||||
# we know that we can't overwrite this file
|
||||
problems.append(path)
|
||||
else:
|
||||
# The old generation's file was a link
|
||||
link_target = path.readlink()
|
||||
if path.parent / link_target == old_files[path].source:
|
||||
# The link has not changed since last system activation, so we can overwrite it
|
||||
transactions.append(Transaction(new_file.source, path, TransactionType.from_file_type(new_file.type)))
|
||||
elif path.parent / link_target == new_file.source:
|
||||
# The link already points to the new target
|
||||
if adopt_identical_links:
|
||||
# We are allowed to "adopt" these links and pretend as if we created them
|
||||
continue
|
||||
else:
|
||||
# We must treat this as a problem, so that undoing this generation will not remove this file created before this generation
|
||||
problems.append(path)
|
||||
else:
|
||||
# The link is to somewhere else
|
||||
problems.append(path)
|
||||
|
||||
return transactions, problems
|
||||
|
||||
def find_old_files(new_files: dict[Path, FileInfo], old_files: dict[Path, FileInfo]) -> list[Transaction]:
|
||||
"""Check the current state of the filesystem against the old links, generating a list of transactions to perform in order to remove them.
|
||||
|
||||
This function will generate a list of transactions incurred in the process of removing every file in old_files that does not exist in new_files and has not been modified.
|
||||
It will not generate transactions to ensure that new_files is correct. Additionally, it will not generate problems for files on disk that have changed since old_files.
|
||||
"""
|
||||
transactions: list[Transaction] = []
|
||||
|
||||
# Remove all remaining files from the old generation that aren't in the new generation
|
||||
path: Path
|
||||
for path in old_files:
|
||||
old_file: FileInfo = old_files[path]
|
||||
if path in new_files:
|
||||
# Already handled when we iterated through new_files above
|
||||
continue
|
||||
if not path.exists(follow_symlinks=False):
|
||||
# There's no file at this path anymore, so we have nothing to do anyway
|
||||
continue
|
||||
else:
|
||||
# There is a file at this path
|
||||
# It could be a regular file or a symlink (including broken symlinks)
|
||||
|
||||
if not path.is_symlink():
|
||||
# The file is a regular file
|
||||
continue
|
||||
else:
|
||||
# The file is a symlink
|
||||
|
||||
if old_file.type != FileType.LINK:
|
||||
# This files wasn't a link at last activation, which means that the user changed it
|
||||
# Therefore we don't touch it
|
||||
continue
|
||||
|
||||
# Check that its destination remains the same
|
||||
link_target = path.readlink()
|
||||
if path.parent / link_target == old_file.source:
|
||||
# The link has not changed since last system activation, so we can overwrite it
|
||||
transactions.append(Transaction.remove(path))
|
||||
else:
|
||||
# The link is to somewhere else, so leave it alone
|
||||
continue
|
||||
|
||||
return transactions
|
||||
|
||||
def perform_transactions(transactions: list[Transaction], DRY_RUN: bool) -> list[Path]:
|
||||
"""Perform the given list of transactions (subject to the DRY_RUN variable), returning a list of directories that have had entries removed"""
|
||||
enclosingDirectories: list[Path] = []
|
||||
|
||||
# Perform all transactions
|
||||
for t in transactions:
|
||||
if DRY_RUN:
|
||||
match t.type:
|
||||
case TransactionType.LINK:
|
||||
print(f"ln -s {t.in_store} {t.on_disk}")
|
||||
case TransactionType.REMOVE:
|
||||
print(f"rm {t.on_disk}")
|
||||
case _:
|
||||
print(f"Unknown transaction type {t.type}")
|
||||
else:
|
||||
match t.type:
|
||||
case TransactionType.LINK:
|
||||
# Ensure parent directory exists
|
||||
t.on_disk.parent.mkdir(parents=True,exist_ok=True)
|
||||
# Remove the file if it exists (we should only get to this case if it's an old symlink we're replacing)
|
||||
# This does not properly handle race conditions, but I think we'd fail in the checking stage
|
||||
# if your config has a race condition
|
||||
t.on_disk.unlink(missing_ok=True)
|
||||
# Link the file into place
|
||||
t.on_disk.symlink_to(t.in_store)
|
||||
case TransactionType.REMOVE:
|
||||
enclosingDirectories.append(t.on_disk.parent)
|
||||
t.on_disk.unlink()
|
||||
case _:
|
||||
print(f"Unknown transaction type {t.type}")
|
||||
|
||||
return enclosingDirectories
|
||||
|
||||
def only_empty(enclosing_directories: list[Path]) -> list[Path]:
|
||||
"""Keep only the directories that are empty out of the given list"""
|
||||
return list(filter(lambda directory: not any(directory.iterdir()), enclosing_directories))
|
74
modules/system/files/linker/poetry.lock
generated
Normal file
74
modules/system/files/linker/poetry.lock
generated
Normal file
|
@ -0,0 +1,74 @@
|
|||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "24.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2"
|
||||
|
||||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "bf6f49ec64a608c5f7ee13c03b8e8e4815bf0fae5dbda81ea9fb3408fe08a277"
|
18
modules/system/files/linker/pyproject.toml
Normal file
18
modules/system/files/linker/pyproject.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[tool.poetry]
|
||||
name = "linker"
|
||||
version = "0.1.0"
|
||||
description = "Link files into place across a nix-darwin system"
|
||||
authors = ["Sam <30577766+Samasaur1@users.noreply.github.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.3.4"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
linker = "linker:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
0
modules/system/files/linker/tests/__init__.py
Normal file
0
modules/system/files/linker/tests/__init__.py
Normal file
52
modules/system/files/linker/tests/mock_path.py
Normal file
52
modules/system/files/linker/tests/mock_path.py
Normal file
|
@ -0,0 +1,52 @@
|
|||
from pathlib import Path
|
||||
from typing import Generator, Callable
|
||||
|
||||
class MockPath(Path):
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name)
|
||||
self.exists_action: Callable[[bool], bool] | None = None
|
||||
self.is_symlink_action: Callable[[], bool] | None = None
|
||||
self.readlink_action: Callable[[], Path] | None = None
|
||||
self.mkdir_action: Callable[[bool, bool], None] | None = None
|
||||
self.unlink_action: Callable[[bool], None] | None = None
|
||||
self.symlink_to_action: Callable[[str | Path], None] | None = None
|
||||
self.contents: list[Path] | None = None
|
||||
self._parent: Path | None = None
|
||||
self.concat_action: Callable[[Path, Path], Path] | None = None
|
||||
|
||||
def exists(self, follow_symlinks: bool = True) -> bool:
|
||||
return self.exists_action(follow_symlinks)
|
||||
|
||||
def is_symlink(self) -> bool:
|
||||
return self.is_symlink_action()
|
||||
|
||||
def readlink(self) -> Path:
|
||||
return self.readlink_action()
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
if self._parent is not None:
|
||||
return self._parent
|
||||
else:
|
||||
raise Exception
|
||||
|
||||
def __truediv__(self, other) -> Path:
|
||||
return self.concat_action(self, other)
|
||||
|
||||
# perform_transactions
|
||||
|
||||
def mkdir(self, parents: bool = False, exist_ok: bool = False):
|
||||
return self.mkdir_action(parents, exist_ok)
|
||||
|
||||
def unlink(self, missing_ok: bool = False):
|
||||
return self.unlink_action(missing_ok)
|
||||
|
||||
def symlink_to(self, target: str | Path):
|
||||
return self.symlink_to_action(target)
|
||||
|
||||
# only_empty
|
||||
|
||||
def iterdir(self) -> Generator[Path, None, None]:
|
||||
for element in self.contents:
|
||||
yield element
|
||||
return
|
329
modules/system/files/linker/tests/test_check_files.py
Normal file
329
modules/system/files/linker/tests/test_check_files.py
Normal file
|
@ -0,0 +1,329 @@
|
|||
from linker import check_files, FileInfo, FileType, Transaction, TransactionType
|
||||
from .mock_path import MockPath
|
||||
|
||||
def test_empty_to_empty():
|
||||
transactions, problems = check_files({}, {})
|
||||
assert transactions == []
|
||||
assert problems == []
|
||||
|
||||
def test_empty_to_single_file_nothing_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: False
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
t = Transaction(in_store, on_disk, TransactionType.LINK)
|
||||
assert transactions == [t]
|
||||
assert problems == []
|
||||
|
||||
def test_empty_to_single_file_broken_symlink_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: not follow_symlinks
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/nonexistent.txt")
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == [on_disk]
|
||||
|
||||
def test_empty_to_single_file_working_symlink_to_correct_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: in_store
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == [on_disk]
|
||||
|
||||
def test_empty_to_single_file_working_symlink_to_correct_file_on_disk_adopting_links():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: in_store
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files, adopt_identical_links=True)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == []
|
||||
|
||||
def test_empty_to_single_file_working_symlink_to_incorrect_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/other.txt")
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == [on_disk]
|
||||
|
||||
def test_empty_to_single_file_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: True
|
||||
on_disk.is_symlink_action = lambda: False
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == [on_disk]
|
||||
|
||||
def test_single_file_to_empty_broken_symlink_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: not follow_sylinks
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/nonexistent.txt")
|
||||
|
||||
new_files = {}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == []
|
||||
|
||||
def test_single_file_to_empty_working_symlink_to_correct_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: in_store
|
||||
|
||||
new_files = {}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == []
|
||||
|
||||
def test_single_file_to_empty_working_symlink_to_incorrect_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/other.txt")
|
||||
|
||||
new_files = {}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == []
|
||||
|
||||
def test_single_file_to_empty_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: False
|
||||
|
||||
new_files = {}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == []
|
||||
|
||||
def test_update_single_file_broken_symlink_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
old_in_store = MockPath("/nix/store/old.txt")
|
||||
new_in_store = MockPath("/nix/store/new.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: not follow_sylinks
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/nonexistent.txt")
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
new_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
old_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == [on_disk]
|
||||
|
||||
def test_update_single_file_working_symlink_to_correct_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
old_in_store = MockPath("/nix/store/old.txt")
|
||||
new_in_store = MockPath("/nix/store/new.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: old_in_store
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
new_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
old_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
t = Transaction(new_in_store, on_disk, TransactionType.LINK)
|
||||
assert transactions == [t]
|
||||
assert problems == []
|
||||
|
||||
def test_update_single_file_working_symlink_to_incorrect_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
old_in_store = MockPath("/nix/store/old.txt")
|
||||
new_in_store = MockPath("/nix/store/new.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/other.txt")
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
new_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
old_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == [on_disk]
|
||||
|
||||
def test_update_single_file_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
old_in_store = MockPath("/nix/store/old.txt")
|
||||
new_in_store = MockPath("/nix/store/new.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: False
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
new_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
old_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions, problems = check_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
assert problems == [on_disk]
|
282
modules/system/files/linker/tests/test_find_old_files.py
Normal file
282
modules/system/files/linker/tests/test_find_old_files.py
Normal file
|
@ -0,0 +1,282 @@
|
|||
from linker import find_old_files, FileInfo, FileType, Transaction, TransactionType
|
||||
from .mock_path import MockPath
|
||||
|
||||
def test_empty_to_empty():
|
||||
transactions = find_old_files({}, {})
|
||||
assert transactions == []
|
||||
|
||||
def test_empty_to_single_file_nothing_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: False
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_empty_to_single_file_broken_symlink_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: not follow_symlinks
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/nonexistent.txt")
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_empty_to_single_file_working_symlink_to_correct_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: in_store
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_empty_to_single_file_working_symlink_to_incorrect_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/other.txt")
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_empty_to_single_file_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_symlinks: True
|
||||
on_disk.is_symlink_action = lambda: False
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_single_file_to_empty_broken_symlink_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: not follow_sylinks
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/nonexistent.txt")
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_single_file_to_empty_working_symlink_to_correct_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: in_store
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
t = Transaction.remove(on_disk)
|
||||
assert transactions == [t]
|
||||
|
||||
def test_single_file_to_empty_working_symlink_to_incorrect_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/other.txt")
|
||||
parent = MockPath(".") # in_store is absolute, so these three lines don't have any effect
|
||||
parent.concat_action = lambda this, other: other
|
||||
on_disk._parent = parent
|
||||
|
||||
new_files = {}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_single_file_to_empty_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
in_store = MockPath("/nix/store/file.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: False
|
||||
|
||||
new_files = {}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_update_single_file_broken_symlink_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
old_in_store = MockPath("/nix/store/old.txt")
|
||||
new_in_store = MockPath("/nix/store/new.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: not follow_sylinks
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/nonexistent.txt")
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
new_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
old_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_update_single_file_working_symlink_to_correct_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
old_in_store = MockPath("/nix/store/old.txt")
|
||||
new_in_store = MockPath("/nix/store/new.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: old_in_store
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
new_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
old_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_update_single_file_working_symlink_to_incorrect_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
old_in_store = MockPath("/nix/store/old.txt")
|
||||
new_in_store = MockPath("/nix/store/new.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: True
|
||||
on_disk.readlink_action = lambda: MockPath("/tmp/other.txt")
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
new_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
old_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
||||
|
||||
def test_update_single_file_file_on_disk():
|
||||
on_disk = MockPath("/tmp/file.txt")
|
||||
old_in_store = MockPath("/nix/store/old.txt")
|
||||
new_in_store = MockPath("/nix/store/new.txt")
|
||||
on_disk.exists_action = lambda follow_sylinks: True
|
||||
on_disk.is_symlink_action = lambda: False
|
||||
|
||||
new_files = {
|
||||
on_disk: FileInfo(
|
||||
new_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
old_files = {
|
||||
on_disk: FileInfo(
|
||||
old_in_store,
|
||||
FileType.LINK
|
||||
)
|
||||
}
|
||||
|
||||
transactions = find_old_files(new_files, old_files)
|
||||
|
||||
assert transactions == []
|
22
modules/system/files/linker/tests/test_only_empty.py
Normal file
22
modules/system/files/linker/tests/test_only_empty.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from linker import only_empty
|
||||
from .mock_path import MockPath
|
||||
|
||||
def test_no_enclosing_directories():
|
||||
assert only_empty([]) == []
|
||||
|
||||
def test_one_empty_dir():
|
||||
empty_dir = MockPath("empty_dir")
|
||||
empty_dir.contents = []
|
||||
assert only_empty([empty_dir]) == [empty_dir]
|
||||
|
||||
def test_one_full_dir():
|
||||
full_dir = MockPath("full_dir")
|
||||
full_dir.contents = [MockPath("file_in_dir"), MockPath("another_file_in_dir")]
|
||||
assert only_empty([full_dir]) == []
|
||||
|
||||
def test_mix():
|
||||
empty_dir = MockPath("empty_dir")
|
||||
empty_dir.contents = []
|
||||
full_dir = MockPath("full_dir")
|
||||
full_dir.contents = [MockPath("file_in_dir")]
|
||||
assert only_empty([empty_dir, full_dir]) == [empty_dir]
|
101
modules/system/files/linker/tests/test_parse_links_file.py
Normal file
101
modules/system/files/linker/tests/test_parse_links_file.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
from linker import parse_links_file, FileInfo, FileType
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
import json
|
||||
|
||||
def test_nonexistent_file():
|
||||
file = Path(f"/tmp/{uuid4()}")
|
||||
file.unlink(missing_ok=True)
|
||||
|
||||
try:
|
||||
theDict = parse_links_file(filePath=file)
|
||||
except:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_nonjson_file():
|
||||
file = Path(f"/tmp/{uuid4()}")
|
||||
file.unlink(missing_ok=True)
|
||||
file.touch()
|
||||
|
||||
try:
|
||||
theDict = parse_links_file(filePath=file)
|
||||
except:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_json_missing_version():
|
||||
file = Path(f"/tmp/{uuid4()}")
|
||||
file.unlink(missing_ok=True)
|
||||
with open(file, "w") as f:
|
||||
json.dump({}, f)
|
||||
|
||||
try:
|
||||
theDict = parse_links_file(filePath=file)
|
||||
except:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_json_missing_files():
|
||||
file = Path(f"/tmp/{uuid4()}")
|
||||
file.unlink(missing_ok=True)
|
||||
with open(file, "w") as f:
|
||||
json.dump({"version": 1}, f)
|
||||
|
||||
try:
|
||||
theDict = parse_links_file(filePath=file)
|
||||
except:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_json_no_files():
|
||||
file = Path(f"/tmp/{uuid4()}")
|
||||
file.unlink(missing_ok=True)
|
||||
with open(file, "w") as f:
|
||||
json.dump({"version": 1, "files": {}}, f)
|
||||
|
||||
try:
|
||||
theDict = parse_links_file(filePath=file)
|
||||
assert theDict == {}
|
||||
except:
|
||||
assert False
|
||||
|
||||
def test_json_one_file():
|
||||
file = Path(f"/tmp/{uuid4()}")
|
||||
file.unlink(missing_ok=True)
|
||||
with open(file, "w") as f:
|
||||
json.dump({"version": 1, "files": {"/tmp/file.txt": {"source": "/nix/store/file.txt", "type": "link"}}}, f)
|
||||
|
||||
try:
|
||||
theDict = parse_links_file(filePath=file)
|
||||
assert len(theDict) == 1
|
||||
el = theDict[Path("/tmp/file.txt")]
|
||||
assert el != None
|
||||
assert el.source == Path("/nix/store/file.txt")
|
||||
assert el.type == FileType.LINK
|
||||
except:
|
||||
assert False
|
||||
|
||||
def test_json_two_files():
|
||||
file = Path(f"/tmp/{uuid4()}")
|
||||
file.unlink(missing_ok=True)
|
||||
with open(file, "w") as f:
|
||||
json.dump({"version": 1, "files": {"/tmp/file.txt": {"source": "/nix/store/file.txt", "type": "link"}, "/tmp/other.txt": {"source": "/nix/store/other.txt", "type": "link"}}}, f)
|
||||
|
||||
try:
|
||||
theDict = parse_links_file(filePath=file)
|
||||
assert len(theDict) == 2
|
||||
el = theDict[Path("/tmp/file.txt")]
|
||||
assert el != None
|
||||
assert el.source == Path("/nix/store/file.txt")
|
||||
assert el.type == FileType.LINK
|
||||
el = theDict[Path("/tmp/other.txt")]
|
||||
assert el != None
|
||||
assert el.source == Path("/nix/store/other.txt")
|
||||
assert el.type == FileType.LINK
|
||||
except:
|
||||
assert False
|
|
@ -0,0 +1,58 @@
|
|||
from linker import perform_transactions, Transaction, TransactionType
|
||||
from .mock_path import MockPath
|
||||
from pathlib import Path
|
||||
|
||||
def test_no_transactions():
|
||||
assert perform_transactions([], False) == []
|
||||
assert perform_transactions([], True) == []
|
||||
|
||||
def test_link_transaction():
|
||||
source = MockPath("/nix/store/file.txt")
|
||||
destination = MockPath("/tmp/file.txt")
|
||||
parent = MockPath("/tmp/parent")
|
||||
did_symlink = False
|
||||
def action(target: Path):
|
||||
nonlocal did_symlink
|
||||
did_symlink = True
|
||||
destination.symlink_to_action = action
|
||||
destination._parent = parent
|
||||
destination.unlink_action = lambda missing_ok: None
|
||||
parent.mkdir_action = lambda parents, exist_ok: None
|
||||
t = Transaction(source, destination, TransactionType.LINK)
|
||||
assert perform_transactions([t], False) == []
|
||||
assert did_symlink
|
||||
|
||||
def test_link_transaction_dry_run():
|
||||
source = MockPath("/nix/store/file.txt")
|
||||
destination = MockPath("/tmp/file.txt")
|
||||
did_symlink = False
|
||||
def action(target: Path):
|
||||
nonlocal did_symlink
|
||||
did_symlink = True
|
||||
destination.symlink_to_action = action
|
||||
t = Transaction(source, destination, TransactionType.LINK)
|
||||
assert perform_transactions([t], True) == []
|
||||
assert not did_symlink
|
||||
|
||||
def test_remove_transaction():
|
||||
source = MockPath("/tmp/file.txt")
|
||||
did_remove = False
|
||||
def action(missing_ok: bool):
|
||||
nonlocal did_remove
|
||||
did_remove = True
|
||||
source.unlink_action = action
|
||||
source._parent = MockPath("/tmp/parent")
|
||||
t = Transaction.remove(source)
|
||||
assert perform_transactions([t], False) == [MockPath("/tmp/parent")]
|
||||
assert did_remove
|
||||
|
||||
def test_remove_transaction_dry_run():
|
||||
source = MockPath("/tmp/file.txt")
|
||||
did_remove = False
|
||||
def action(missing_ok: bool):
|
||||
nonlocal did_remove
|
||||
did_remove = True
|
||||
source.unlink_action = action
|
||||
t = Transaction.remove(source)
|
||||
assert perform_transactions([t], True) == []
|
||||
assert not did_remove
|
52
modules/system/files/write-text.nix
Normal file
52
modules/system/files/write-text.nix
Normal file
|
@ -0,0 +1,52 @@
|
|||
{ lib, mkTextDerivation }:
|
||||
|
||||
{ config, name, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
fileName = file: last (splitString "/" file);
|
||||
mkDefaultIf = cond: value: mkIf cond (mkDefault value);
|
||||
|
||||
drv = mkTextDerivation (fileName name) config.text;
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether this file should be generated.
|
||||
This option allows specific files to be disabled.
|
||||
'';
|
||||
};
|
||||
|
||||
text = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Text of the file.
|
||||
'';
|
||||
};
|
||||
|
||||
target = mkOption {
|
||||
type = types.str;
|
||||
default = "/${name}";
|
||||
description = ''
|
||||
Name of symlink. Defaults to the attribute name preceded by a slash (the root directory).
|
||||
'';
|
||||
};
|
||||
|
||||
source = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
Path of the source file.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
source = mkDefault drv;
|
||||
};
|
||||
}
|
|
@ -123,6 +123,7 @@ in {
|
|||
tests.system-defaults-write = makeTest ./tests/system-defaults-write.nix;
|
||||
tests.system-environment = makeTest ./tests/system-environment.nix;
|
||||
tests.system-keyboard-mapping = makeTest ./tests/system-keyboard-mapping.nix;
|
||||
tests.system-files = makeTest ./tests/system-files.nix;
|
||||
tests.system-packages = makeTest ./tests/system-packages.nix;
|
||||
tests.system-path = makeTest ./tests/system-path.nix;
|
||||
tests.system-shells = makeTest ./tests/system-shells.nix;
|
||||
|
|
32
tests/system-files.nix
Normal file
32
tests/system-files.nix
Normal file
|
@ -0,0 +1,32 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
contents = "Hello, world!";
|
||||
jq = pkgs.lib.getExe pkgs.jq;
|
||||
in
|
||||
|
||||
{
|
||||
system.file."tmp/hello.txt".text = contents;
|
||||
|
||||
test = ''
|
||||
echo 'checking that system links file exists' >&2
|
||||
test -e ${config.out}/links.json
|
||||
|
||||
echo 'checking links file version' >&2
|
||||
test "$(${jq} .version ${config.out}/links.json)" = "1"
|
||||
|
||||
echo 'checking that test file is in links.json' >&2
|
||||
test ! "$(${jq} '.files."/tmp/hello.txt"' ${config.out}/links.json)" = "null"
|
||||
|
||||
echo 'checking that test file is a link' >&2
|
||||
test "$(${jq} '.files."/tmp/hello.txt".type == "link"' ${config.out}/links.json)" = "true"
|
||||
|
||||
echo 'checking that the link source is correct' >&2
|
||||
diff <(${jq} -r '.files."/tmp/hello.txt".source' ${config.out}/links.json) <(echo ${config.system.file."tmp/hello.txt".source})
|
||||
|
||||
# I wanted to check the contents of the file as well, but I am prevented from doing so, I think by the sandbox
|
||||
# echo 'checking that the link source has the correct contents' >&2
|
||||
# diff "$(${jq} '.files."/tmp/hello.txt".source' ${config.out}/links.json)" <(echo ${contents}) # >/dev/null
|
||||
# # grep '${contents}' "$(${jq} '.files."/tmp/hello.txt".source' ${config.out}/links.json)"
|
||||
'';
|
||||
}
|
Loading…
Add table
Reference in a new issue