aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/default.nix182
-rwxr-xr-xtests/fixtures.sh498
-rw-r--r--tests/lib.sh118
-rw-r--r--tests/rsync.nix50
-rw-r--r--tests/scenarios.nix2407
-rw-r--r--tests/vm.nix204
6 files changed, 104 insertions, 3355 deletions
diff --git a/tests/default.nix b/tests/default.nix
index 0e59e26..475ab5d 100644
--- a/tests/default.nix
+++ b/tests/default.nix
@@ -1,88 +1,118 @@
-# NixOS VM tests for populate: one test per source type, one for features
-# spanning sources, one for edge cases of flakes, one for staying inside the
-# target directory, and one for writeDeploy's arguments to nixos-rebuild.
-# Build all with nix-build tests, or one with, e.g., nix-build tests -A flake.
-# The exhaustive tests add the combinations of options that no code path
-# tells apart from the ones the others cover; build them with nix-build tests
-# -A exhaustive, or one with, e.g., nix-build tests -A exhaustive.flake.
-# The smoke test runs a few scenarios covering each source type and each kind
-# of target in a single VM test, for checking changes quickly; build it with
-# nix-build tests -A smoke. nix-build tests -A only --argstr only REGEX runs
-# the scenarios whose names match the regular expression REGEX in a single
-# VM test, e.g. --argstr only 'lock-hooks-.*'.
+# NixOS VM tests for krops, with the VM machinery and the scenarios of
+# populate's tests, in ../pkgs/populate/tests: deploy, for writeDeploy's
+# arguments to nixos-rebuild, and populate, for pkgs.populate, running
+# populate's scenarios that expect populate to refuse its arguments, which
+# pkgs.populate does during evaluation here, and the scenarios of
+# populate's smoke test, with target attribute sets as mkTarget returns
+# them. Build all with nix-build tests, or one with, e.g., nix-build tests
+# -A deploy. The exhaustive tests add the combinations of options that no
+# code path tells apart from the ones the others cover; build them with
+# nix-build tests -A exhaustive. The smoke test runs a few scenarios of
+# each in a single VM test; build it with nix-build tests -A smoke.
+# nix-build tests -A only --argstr only REGEX runs the scenarios whose
+# names match the regular expression REGEX, of all of populate's scenarios
+# and krops' own, in a single VM test, e.g. --argstr only 'lock-hooks-.*'.
+# populate's own tests run the program directly; see
+# ../pkgs/populate/README.md.
{ pkgs ? import ../pkgs {}, only ? null }:
let
- # HEAD of /fixtures/gitsrc, which fixtures.sh creates deterministically,
- # and its parent. fixtures.sh fails if HEAD differs, naming the actual one.
- gitHead = "e42d44ef9cdb76ba5a84dff4c89c38222703bf0c";
- gitFirst = "8d47013fa0888948cf0ffcc4e3c69a7b5f180562";
+ inherit (pkgs) lib;
+ krops = import ../lib;
+ populateTests = ../pkgs/populate/tests;
- # rsync versions the scenarios depending on rsync's version run with.
- # callPackage adds override and overrideDerivation, which aren't versions.
- rsyncs = pkgs.lib.filterAttrs
- (_: pkgs.lib.isDerivation)
- (pkgs.callPackage ./rsync.nix {});
+ inherit (import (populateTests + "/fixtures.nix")) gitHead gitFirst;
- # The scenarios, with populate using rsync.
- scenariosWith = rsync: let
- populate = pkgs.populate.override { inherit rsync; };
- in import ./scenarios.nix {
- inherit (pkgs) lib;
- krops = import ../lib;
- inherit populate;
- inherit (pkgs.krops.override { inherit populate; }) writeDeploy;
+ remoteHost = "target";
+
+ # Targets of each kind for a path, as mkTarget returns them.
+ targets = {
+ local = path: krops.mkTarget "" // { host = "localhost"; inherit path; };
+ remote = path: {
+ user = "root"; host = remoteHost; port = null; inherit path;
+ sudo = false; extraOptions = [];
+ };
+ sudo = path: {
+ user = "deploy"; host = remoteHost; port = null; inherit path;
+ sudo = true; extraOptions = [];
+ };
+ };
+
+ # A phase runs pkgs.populate, or writeDeploy with a stand-in for
+ # nixos-rebuild.
+ run = {
+ source, backup, force, prune, deploy, targetKind, targetPath,
+ }: let
+ args = {
+ inherit source backup force prune;
+ target = targets.${targetKind} targetPath;
+ };
+ in
+ if deploy == null then pkgs.populate args
+ else pkgs.krops.writeDeploy "deploy"
+ (args // { useNixOutputMonitor = false; } // deploy);
+
+ builderArgs = {
+ inherit lib run;
+ inherit (krops) evalSource;
+ checksDuringEvaluation = true;
url = "http://deployer:8000";
gitUrl = "git://deployer/gitsrc.git";
- remoteHost = "target";
+ inherit remoteHost;
agePath = "${pkgs.age}/bin/age";
inherit gitHead gitFirst;
- rsyncVersion = rsync.version;
+ rsyncVersion = pkgs.rsync.version;
+ };
+
+ populateScenarios = lib.concatLists (builtins.attrValues
+ (import (populateTests + "/scenarios.nix") builderArgs));
+ kropsScenarios = import ./scenarios.nix (builderArgs // { inherit targets; });
+
+ # Names of the scenarios of the smoke test, besides populate's.
+ smokeNames = import (populateTests + "/smoke.nix") ++ [
+ "deploy-local-buildTarget0-etcFlake0-flakefile"
+ "deploy-sudo-allocateTTY"
+ ];
+
+ scenarios = {
+ inherit (kropsScenarios) deploy;
+ populate = builtins.filter
+ (scenario: scenario.refusals || builtins.elem scenario.name smokeNames)
+ populateScenarios;
+ };
+ allScenarios = populateScenarios ++ lib.concatLists (builtins.attrValues kropsScenarios);
+
+ # The stand-in for nixos-rebuild replaces it on both machines.
+ module = { pkgs, ... }: {
+ environment.systemPackages = [
+ (pkgs.writeScriptBin "nixos-rebuild" (builtins.readFile ./nixos-rebuild.sh))
+ ];
+ system.disableInstallerTools = true;
};
- scenarios = scenariosWith pkgs.rsync;
+
+ vm = name: scenarios: import (populateTests + "/vm.nix") {
+ inherit pkgs gitHead scenarios module;
+ name = "krops-${name}";
+ };
+
# VM tests of those of the groups of scenarios that are, or aren't,
# exhaustive, named after the groups with prefix in front.
- testsOf = exhaustive: prefix: pkgs.lib.mapAttrs
- (name: scenarios: import ./vm.nix {
- inherit pkgs gitHead scenarios;
- name = prefix + name;
- })
- (pkgs.lib.filterAttrs (_: scenarios: scenarios != [])
+ testsOf = exhaustive: prefix: lib.mapAttrs
+ (name: scenarios: vm (prefix + name) scenarios)
+ (lib.filterAttrs (_: scenarios: scenarios != [])
(builtins.mapAttrs
(_: builtins.filter (scenario: scenario.exhaustive == exhaustive))
scenarios));
- # Names of the scenarios of the smoke test.
- smokeNames = [
- "file-local-checksum0-deleteExcluded0-filters0-inputsnone"
- "file-sudo-checksum0-deleteExcluded0-filters1-inputsnone"
- "git-remote-cleanExclude0-fetchAlways0-inputsnone-shallow0"
- "git-local-shallow1-refs"
- "symlink-local-absolute0-inputs0"
- "pass-remote-storepass"
- "passage-sudo-agedefault-dirstore"
- "pipe-local"
- "derivation-remote"
- "flake-local-fetchOnTarget0-inputsrewrite-kindgit-materialize0"
- "flake-sudo-fetchOnTarget1-inputsnone-kindgit-materialize0"
- "flake-input-dep-remote-fetchOnTarget0-inputsgenerated-materialize0"
- "flake-input-sub-local-fetchOnTarget0-materialize0"
- "state-sentinel-local"
- "prune-remote"
- "names-local"
- "paths-local"
- "deploy-local-buildTarget0-etcFlake0-flakefile"
- "deploy-sudo-allocateTTY"
- ];
- allScenarios = pkgs.lib.concatLists (builtins.attrValues scenarios);
smokeScenarios = let
selected = builtins.filter (scenario: builtins.elem scenario.name smokeNames) allScenarios;
- missing = pkgs.lib.subtractLists (map (scenario: scenario.name) selected) smokeNames;
+ missing = lib.subtractLists (map (scenario: scenario.name) selected) smokeNames;
in
if missing != [] then
throw "tests: smoke test names missing scenarios: ${toString missing}"
else
selected;
+
# The scenarios whose names match the regular expression only.
onlyScenarios = let
selected = builtins.filter
@@ -96,32 +126,16 @@ let
else
selected;
in
+
+# The remote target must not count as local wherever this gets evaluated.
+assert !(krops.isLocalTarget (targets.remote "/"));
+
testsOf false ""
# Not built by nix-build tests either, which runs these scenarios anyway.
// {
- smoke.smoke = import ./vm.nix {
- inherit pkgs gitHead;
- name = "smoke";
- scenarios = smokeScenarios;
- };
- only.only = import ./vm.nix {
- inherit pkgs gitHead;
- name = "only";
- scenarios = onlyScenarios;
- };
+ smoke.smoke = vm "smoke" smokeScenarios;
+ only.only = vm "only" onlyScenarios;
}
# An attribute set without recurseForDerivations, so nix-build tests
# doesn't build them.
// { exhaustive = testsOf true "exhaustive-"; }
- // pkgs.lib.mapAttrs'
- (version: rsync: {
- name = "rsync-${builtins.replaceStrings [ "." ] [ "-" ] version}";
- value = import ./vm.nix {
- inherit pkgs gitHead rsync;
- name = "rsync-${version}";
- scenarios = builtins.filter
- (scenario: pkgs.lib.hasInfix "insecureLinks" scenario.name)
- (scenariosWith rsync).file;
- };
- })
- rsyncs
diff --git a/tests/fixtures.sh b/tests/fixtures.sh
deleted file mode 100755
index 85701a1..0000000
--- a/tests/fixtures.sh
+++ /dev/null
@@ -1,498 +0,0 @@
-#! /bin/sh
-# Creates the fixtures the tests deploy from in /fixtures.
-#
-# usage: fixtures.sh URL GIT_HEAD
-#
-# URL is where /fixtures/www gets served via HTTP, e.g. http://deployer:8000.
-# The tarball flakes there get locked against it, so it must be served while
-# this script runs. /fixtures/git gets served by git daemon. GIT_HEAD is the
-# commit the tests expect as HEAD of /fixtures/gitsrc.
-set -efu
-
-url=$1
-git_head=$2
-
-nix() {
- command nix --extra-experimental-features 'nix-command flakes' "$@"
-}
-
-export GIT_AUTHOR_NAME=krops GIT_AUTHOR_EMAIL=spam@krebsco.de
-export GIT_COMMITTER_NAME=krops GIT_COMMITTER_EMAIL=spam@krebsco.de
-export GIT_AUTHOR_DATE='1000000000 +0000' GIT_COMMITTER_DATE='1000000000 +0000'
-# Configuration such as commit.gpgsign would change the commits.
-export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
-
-# Commits everything in the current directory. Commits are deterministic, so
-# the tests can refer to them by hash.
-commit() {
- git add -A
- git commit -q -m "$1"
-}
-
-tarball() {
- tar -C /fixtures/www/src --sort=name --mtime=@1 --owner=0 --group=0 \
- --numeric-owner -czf /fixtures/www/"$1".tar.gz "$1"
-}
-
-rm -fR /fixtures
-mkdir -p /fixtures/www/src
-
-# Tarball flakes, fetched via HTTP: inner, dep (with input inner), mono (with
-# a flake in subdirectory sub), and remote (with input dep).
-mkdir /fixtures/www/src/inner
-cat > /fixtures/www/src/inner/flake.nix <<'NIX'
-{ outputs = _: { who = "inner"; }; }
-NIX
-tarball inner
-
-mkdir /fixtures/www/src/dep
-cat > /fixtures/www/src/dep/flake.nix <<NIX
-{
- inputs.inner.url = "$url/inner.tar.gz";
- outputs = { inner, ... }: { greeting = "dep+\${inner.who}"; };
-}
-NIX
-(cd /fixtures/www/src/dep && nix flake lock)
-tarball dep
-
-mkdir -p /fixtures/www/src/mono/sub
-echo mono > /fixtures/www/src/mono/README
-cat > /fixtures/www/src/mono/sub/flake.nix <<'NIX'
-{ outputs = _: { who = "sub"; }; }
-NIX
-tarball mono
-
-mkdir /fixtures/www/src/remote
-cat > /fixtures/www/src/remote/flake.nix <<NIX
-{
- inputs.dep.url = "$url/dep.tar.gz";
- outputs = { dep, ... }: { msg = "remote:\${dep.greeting}"; };
-}
-NIX
-(cd /fixtures/www/src/remote && nix flake lock)
-tarball remote
-
-# top: a flake in a Git work tree, with inputs dep and sub (a flake in a
-# subdirectory).
-mkdir /fixtures/top
-cat > /fixtures/top/flake.nix <<NIX
-{
- inputs.dep.url = "$url/dep.tar.gz";
- inputs.sub.url = "$url/mono.tar.gz?dir=sub";
- outputs = { dep, sub, ... }: { msg = "top:\${dep.greeting},\${sub.who}"; };
-}
-NIX
-(cd /fixtures/top && nix flake lock && git init -q && commit top)
-
-# mono: a Git work tree with a flake in subdirectory sub.
-cp -R /fixtures/www/src/mono /fixtures/mono
-(cd /fixtures/mono && git init -q && commit mono)
-
-# alt: a flake to draw inputs from.
-mkdir /fixtures/alt
-cat > /fixtures/alt/flake.nix <<'NIX'
-{ outputs = _: { greeting = "alt"; who = "alt"; }; }
-NIX
-
-# usegone: a flake with an input locked to a path that doesn't exist anymore.
-mkdir /fixtures/gone /fixtures/usegone
-cat > /fixtures/gone/flake.nix <<'NIX'
-{ outputs = _: {}; }
-NIX
-cat > /fixtures/usegone/flake.nix <<'NIX'
-{
- inputs.gone.url = "path:/fixtures/gone";
- outputs = _: {};
-}
-NIX
-(cd /fixtures/usegone && nix flake lock)
-rm -R /fixtures/gone
-
-# kropsremote: a registry entry for remote, so it has an indirect reference.
-nix registry add kropsremote "$url/remote.tar.gz"
-
-# monoin: a Git work tree with a flake in subdirectory sub that has an input.
-mkdir -p /fixtures/monoin/sub
-echo monoin > /fixtures/monoin/README
-cat > /fixtures/monoin/sub/flake.nix <<NIX
-{
- inputs.dep.url = "$url/dep.tar.gz";
- outputs = { dep, ... }: { msg = "monoin:\${dep.greeting}"; };
-}
-NIX
-(cd /fixtures/monoin/sub && nix flake lock)
-(cd /fixtures/monoin && git init -q && commit monoin)
-
-# dirty: a copy of top with an uncommitted change and an untracked file.
-cp -R /fixtures/top /fixtures/dirty
-sed -i 's/top:/dirty:/' /fixtures/dirty/flake.nix
-echo untracked > /fixtures/dirty/untracked
-
-# unlocked: a flake whose lock file is missing.
-mkdir /fixtures/unlocked
-cat > /fixtures/unlocked/flake.nix <<NIX
-{
- inputs.inner.url = "$url/inner.tar.gz";
- outputs = { inner, ... }: { msg = inner.who; };
-}
-NIX
-
-# uselocal: a flake with an input locked to a path on the deploying machine.
-mkdir /fixtures/uselocal
-cat > /fixtures/uselocal/flake.nix <<'NIX'
-{
- inputs.a.url = "path:/fixtures/alt";
- outputs = { a, ... }: { msg = "uselocal:${a.who}"; };
-}
-NIX
-(cd /fixtures/uselocal && nix flake lock)
-
-# follow: a flake whose input dep draws its input inner from the flake's own
-# input inner, which is inner2, so the lock file records a follows path, and
-# dep's own lock file pins another inner than the flake's.
-mkdir /fixtures/www/src/inner2
-cat > /fixtures/www/src/inner2/flake.nix <<'NIX'
-{ outputs = _: { who = "inner2"; }; }
-NIX
-tarball inner2
-mkdir /fixtures/follow
-cat > /fixtures/follow/flake.nix <<NIX
-{
- inputs.inner.url = "$url/inner2.tar.gz";
- inputs.dep.url = "$url/dep.tar.gz";
- inputs.dep.inputs.inner.follows = "inner";
- outputs = { dep, ... }: { msg = "follow:\${dep.greeting}"; };
-}
-NIX
-(cd /fixtures/follow && nix flake lock)
-
-# nofollow: a flake that removes an input of its input, so the lock file
-# records an empty follows path.
-mkdir /fixtures/nofollow
-cat > /fixtures/nofollow/flake.nix <<NIX
-{
- inputs.dep.url = "$url/dep.tar.gz";
- inputs.dep.inputs.inner.follows = "";
- outputs = _: { msg = "nofollow"; };
-}
-NIX
-(cd /fixtures/nofollow && nix flake lock)
-
-# cyc: a flake whose inputs cyca and cycb follow each other.
-for name in cyca cycb; do
- other=$(test $name = cyca && echo cycb || echo cyca)
- mkdir /fixtures/www/src/$name
- cat > /fixtures/www/src/$name/flake.nix <<NIX
-{
- inputs.other.url = "$url/$other.tar.gz";
- outputs = _: { who = "$name"; };
-}
-NIX
- tarball $name
-done
-mkdir /fixtures/cyc
-cat > /fixtures/cyc/flake.nix <<NIX
-{
- inputs.a.url = "$url/cyca.tar.gz";
- inputs.b.url = "$url/cycb.tar.gz";
- inputs.a.inputs.other.follows = "b";
- inputs.b.inputs.other.follows = "a";
- outputs = { a, b, ... }: { msg = "cyc:\${a.who},\${b.who}"; };
-}
-NIX
-(cd /fixtures/cyc && nix flake lock)
-
-# rel: a flake in a Git work tree with an input in a subdirectory, referred
-# to by a relative path.
-mkdir -p /fixtures/rel/lib
-cat > /fixtures/rel/lib/flake.nix <<'NIX'
-{ outputs = _: { who = "lib"; }; }
-NIX
-cat > /fixtures/rel/flake.nix <<'NIX'
-{
- inputs.lib.url = "path:./lib";
- outputs = { lib, ... }: { msg = "rel:${lib.who}"; };
-}
-NIX
-(cd /fixtures/rel && nix flake lock && git init -q && commit rel)
-
-# two, c1, c2: a flake with two inputs, and two flakes to draw them from.
-mkdir /fixtures/two /fixtures/c1 /fixtures/c2
-cat > /fixtures/two/flake.nix <<NIX
-{
- inputs.p.url = "$url/inner.tar.gz";
- inputs.q.url = "$url/inner.tar.gz";
- outputs = { p, q, ... }: { greeting = "two:\${p.who}+\${q.who}"; };
-}
-NIX
-(cd /fixtures/two && nix flake lock)
-cat > /fixtures/c1/flake.nix <<'NIX'
-{ outputs = _: { who = "c1"; }; }
-NIX
-cat > /fixtures/c2/flake.nix <<'NIX'
-{ outputs = _: { who = "c2"; }; }
-NIX
-
-# usedata, data: a flake with an input that isn't a flake, and a directory
-# to draw it from.
-mkdir /fixtures/usedata /fixtures/data
-cat > /fixtures/usedata/flake.nix <<'NIX'
-{
- inputs.data = { url = "path:/nonexistent"; flake = false; };
- outputs = { data, ... }: { msg = "data:${builtins.readFile (data + "/value")}"; };
-}
-NIX
-printf hello > /fixtures/data/value
-
-# pipe2: a command for the pipe source type whose output the tests change.
-cat > /fixtures/pipe2 <<'SH'
-#! /bin/sh
-cat /fixtures/pipe2-data
-SH
-chmod +x /fixtures/pipe2
-echo piped > /fixtures/pipe2-data
-
-# alt2: like alt, with other outputs, to change a source to.
-mkdir /fixtures/alt2
-cat > /fixtures/alt2/flake.nix <<'NIX'
-{ outputs = _: { greeting = "alt2"; who = "alt2"; }; }
-NIX
-
-# filtered, filtered-link: a directory with an .rsync-filter, and a symlink to
-# it that neither root nor the user deploying owns, through which rsync 3.5.0
-# and later can't read that file.
-mkdir /fixtures/filtered
-printf -- '- /garbage\n' > /fixtures/filtered/.rsync-filter
-echo keep > /fixtures/filtered/keep
-echo garbage > /fixtures/filtered/garbage
-ln -s filtered /fixtures/filtered-link
-chown -h nobody /fixtures/filtered-link
-
-# noflake: a directory without flake.nix.
-mkdir /fixtures/noflake
-echo noflake > /fixtures/noflake/README
-
-# filesrc: a copy of top, with files for testing the file source type's
-# options. same has the same size and modification time as the file the
-# tests put on the target beforehand, while skip-ex and skip-f have the same
-# size but an older modification time than theirs, so rsync's quick check
-# tells them apart.
-mkdir /fixtures/filesrc
-cp /fixtures/top/flake.nix /fixtures/top/flake.lock /fixtures/filesrc
-echo keep > /fixtures/filesrc/keep
-echo src > /fixtures/filesrc/skip-ex
-echo src > /fixtures/filesrc/skip-f
-echo AAAA > /fixtures/filesrc/same
-touch -d @1000000000 /fixtures/filesrc/same
-touch -d @999999999 /fixtures/filesrc/skip-ex /fixtures/filesrc/skip-f
-
-# gitsrc: a Git repository with two commits, served by git daemon. Its flake
-# has an input that only exists when drawn from a source.
-mkdir /fixtures/gitsrc
-cat > /fixtures/gitsrc/flake.nix <<'NIX'
-{
- inputs.dep.url = "path:/nonexistent";
- outputs = { dep, ... }: { msg = "git:${dep.greeting}"; };
-}
-NIX
-(cd /fixtures/gitsrc && git init -q -b master && commit one)
-echo two > /fixtures/gitsrc/two
-(cd /fixtures/gitsrc && commit two)
-if test "$(git -C /fixtures/gitsrc rev-parse HEAD)" != "$git_head"; then
- >&2 printf 'error: HEAD of /fixtures/gitsrc is %s, but the tests expect %s\n' \
- "$(git -C /fixtures/gitsrc rev-parse HEAD)" "$git_head"
- exit 1
-fi
-mkdir /fixtures/git
-git clone -q --bare /fixtures/gitsrc /fixtures/git/gitsrc.git
-git -C /fixtures/git/gitsrc.git config uploadpack.allowReachableSHA1InWant true
-
-# withsub: a Git repository with gitsrc as its submodule sub, served by git
-# daemon like gitsrc, which the tests reach as git://deployer/gitsrc.git.
-mkdir /fixtures/withsub
-cat > /fixtures/withsub/.gitmodules <<'GIT'
-[submodule "sub"]
- path = sub
- url = git://deployer/gitsrc.git
-GIT
-(
- cd /fixtures/withsub
- git init -q -b master
- git update-index --add --cacheinfo 160000,"$git_head",sub
- git add .gitmodules
- git commit -q -m withsub
-)
-git clone -q --bare /fixtures/withsub /fixtures/git/withsub.git
-
-# pass: password stores with x/a, without and with Git.
-if ! gpg --list-secret-keys spam@krebsco.de >/dev/null 2>&1; then
- gpg --batch --passphrase '' --quick-gen-key spam@krebsco.de default default never
-fi
-for store in pass pass-git; do
- export PASSWORD_STORE_DIR=/fixtures/$store
- pass init spam@krebsco.de >/dev/null
- if test $store = pass-git; then
- pass git init >/dev/null
- fi
- printf 'secret-a\n' | pass insert -m x/a >/dev/null
-done
-unset PASSWORD_STORE_DIR
-
-# passage: a passage store with x/a.
-mkdir -p /fixtures/passage/store
-age-keygen -o /fixtures/passage/identities 2>/dev/null
-age-keygen -y /fixtures/passage/identities > /fixtures/passage/store/.age-recipients
-printf 'secret-a\n' |
- PASSAGE_DIR=/fixtures/passage/store \
- PASSAGE_IDENTITIES_FILE=/fixtures/passage/identities \
- passage insert -m x/a >/dev/null
-
-# pipe: a command for the pipe source type.
-cat > /fixtures/pipe <<'SH'
-#! /bin/sh
-echo piped
-SH
-chmod +x /fixtures/pipe
-
-# pipe-slow: a command that takes longer than the tests wait for it.
-cat > /fixtures/pipe-slow <<'SH'
-#! /bin/sh
-sleep 60
-echo piped
-SH
-chmod +x /fixtures/pipe-slow
-
-# patches: hello and hello2 change dep's greeting, hello2 only after hello;
-# sub changes the flake in mono's subdirectory; bad applies to nothing.
-mkdir -p /fixtures/patches
-# Prints a patch of file $1 in the directory $2, replacing $3 by $4.
-patch_of() {
- sed "s/$3/$4/" "$2/$1" > /fixtures/patches/new
- diff -u --label a/"$1" --label b/"$1" "$2/$1" /fixtures/patches/new || test $? = 1
- rm /fixtures/patches/new
-}
-patch_of flake.nix /fixtures/www/src/dep 'dep+' 'hello+' > /fixtures/patches/hello.patch
-mkdir /fixtures/patches/dep-hello
-sed 's/dep+/hello+/' /fixtures/www/src/dep/flake.nix > /fixtures/patches/dep-hello/flake.nix
-patch_of flake.nix /fixtures/patches/dep-hello 'hello+' 'hello2+' > /fixtures/patches/hello2.patch
-rm -R /fixtures/patches/dep-hello
-patch_of flake.nix /fixtures/www/src/mono/sub '"sub"' '"patched"' > /fixtures/patches/sub.patch
-cat > /fixtures/patches/bad.patch <<'PATCH'
---- a/flake.nix
-+++ b/flake.nix
-@@ -1 +1 @@
--nothing like this
-+anything
-PATCH
-
-# broken: a flake with an input that can't be fetched anymore.
-mkdir /fixtures/www/src/vanishing
-cat > /fixtures/www/src/vanishing/flake.nix <<'NIX'
-{ outputs = _: { who = "vanishing"; }; }
-NIX
-tarball vanishing
-mkdir /fixtures/broken
-cat > /fixtures/broken/flake.nix <<NIX
-{
- inputs.vanishing.url = "$url/vanishing.tar.gz";
- outputs = { vanishing, ... }: { msg = "broken:\${vanishing.who}"; };
-}
-NIX
-(cd /fixtures/broken && nix flake lock)
-
-# pinned, usepinned: a tarball flake whose own lock file pins an input that
-# can't be fetched anymore, and a flake that draws that input from its own
-# input instead, so nothing it evaluates needs the pin.
-mkdir /fixtures/www/src/pinned
-cat > /fixtures/www/src/pinned/flake.nix <<NIX
-{
- inputs.inner.url = "$url/vanishing.tar.gz";
- outputs = { inner, ... }: { greeting = "pinned+\${inner.who}"; };
-}
-NIX
-(cd /fixtures/www/src/pinned && nix flake lock)
-tarball pinned
-mkdir /fixtures/usepinned
-cat > /fixtures/usepinned/flake.nix <<NIX
-{
- inputs.inner.url = "$url/inner.tar.gz";
- inputs.dep.url = "$url/pinned.tar.gz";
- inputs.dep.inputs.inner.follows = "inner";
- outputs = { dep, ... }: { msg = "usepinned:\${dep.greeting}"; };
-}
-NIX
-(cd /fixtures/usepinned && nix flake lock)
-rm /fixtures/www/vanishing.tar.gz
-
-# stale, usestale: a tarball flake whose lock file lacks an input its
-# flake.nix has, and a flake with it as input, whose own lock file is
-# complete.
-mkdir /fixtures/www/src/stale
-cat > /fixtures/www/src/stale/flake.nix <<NIX
-{
- inputs.inner.url = "$url/inner.tar.gz";
- outputs = { inner, ... }: { greeting = "stale+\${inner.who}"; };
-}
-NIX
-(cd /fixtures/www/src/stale && nix flake lock)
-cat > /fixtures/www/src/stale/flake.nix <<NIX
-{
- inputs.inner.url = "$url/inner.tar.gz";
- inputs.extra.url = "$url/inner2.tar.gz";
- outputs = { inner, extra, ... }: { greeting = "stale+\${inner.who}+\${extra.who}"; };
-}
-NIX
-tarball stale
-mkdir /fixtures/usestale
-cat > /fixtures/usestale/flake.nix <<NIX
-{
- inputs.dep.url = "$url/stale.tar.gz";
- outputs = { dep, ... }: { msg = "usestale:\${dep.greeting}"; };
-}
-NIX
-(cd /fixtures/usestale && nix flake lock)
-
-# offline: a flake with an input of its own, which the tests make
-# unreachable between deploys.
-mkdir /fixtures/www/src/gap
-cat > /fixtures/www/src/gap/flake.nix <<'NIX'
-{ outputs = _: { who = "gap"; }; }
-NIX
-tarball gap
-mkdir /fixtures/offline
-cat > /fixtures/offline/flake.nix <<NIX
-{
- inputs.gap.url = "$url/gap.tar.gz";
- outputs = { gap, ... }: { msg = "offline:\${gap.who}"; };
-}
-NIX
-(cd /fixtures/offline && nix flake lock)
-
-# c3: a flake with an input, to draw another flake's input from.
-mkdir /fixtures/c3
-cat > /fixtures/c3/flake.nix <<NIX
-{
- inputs.z.url = "$url/inner.tar.gz";
- outputs = { z, ... }: { who = "c3(\${z.who})"; };
-}
-NIX
-(cd /fixtures/c3 && nix flake lock)
-
-# filtered-parent, filtered-parent-link: like filtered, but the filter file
-# applies to a subdirectory, which is what gets synced.
-mkdir -p /fixtures/filtered-parent/sub
-printf -- '- garbage\n' > /fixtures/filtered-parent/.rsync-filter
-echo keep > /fixtures/filtered-parent/sub/keep
-echo garbage > /fixtures/filtered-parent/sub/garbage
-ln -s filtered-parent /fixtures/filtered-parent-link
-chown -h nobody /fixtures/filtered-parent-link
-
-# filtered-name, filtered-name-link: like filtered, but the filter file is in
-# a subdirectory whose name has a byte that rsync prints escaped in the C
-# locale.
-mkdir -p /fixtures/filtered-name/bär
-printf -- '- garbage\n' > /fixtures/filtered-name/bär/.rsync-filter
-echo keep > /fixtures/filtered-name/bär/keep
-echo garbage > /fixtures/filtered-name/bär/garbage
-ln -s filtered-name /fixtures/filtered-name-link
-chown -h nobody /fixtures/filtered-name-link
diff --git a/tests/lib.sh b/tests/lib.sh
deleted file mode 100644
index a8f2594..0000000
--- a/tests/lib.sh
+++ /dev/null
@@ -1,118 +0,0 @@
-# Helpers for preparing and checking scenarios, sourced on the machine
-# populate deploys to.
-set -efu
-
-fail() {
- printf 'FAIL: %s\n' "$*" >&2
- exit 1
-}
-
-# usage: expect_eq WHAT ACTUAL EXPECTED
-expect_eq() {
- if test "$2" != "$3"; then
- fail "$1: expected '$3', got '$2'"
- fi
-}
-
-nix() {
- command nix --extra-experimental-features 'nix-command flakes' "$@"
-}
-
-# Evaluates a flake output without network access to the fixtures, so all
-# inputs must be in the store. An empty cache directory keeps Nix from
-# restoring inputs from its fetcher cache.
-nix_eval() {
- nix_eval_cache=$(mktemp -d)
- nix_eval_status=0
- env \
- XDG_CACHE_HOME="$nix_eval_cache" \
- http_proxy=http://127.0.0.1:9 https_proxy=http://127.0.0.1:9 \
- HTTP_PROXY=http://127.0.0.1:9 HTTPS_PROXY=http://127.0.0.1:9 \
- all_proxy=http://127.0.0.1:9 ALL_PROXY=http://127.0.0.1:9 \
- no_proxy= NO_PROXY= \
- nix --extra-experimental-features 'nix-command flakes' eval --raw "$@" ||
- nix_eval_status=$?
- rm -fR "$nix_eval_cache"
- return "$nix_eval_status"
-}
-
-# Evaluates a flake output, fetching missing inputs.
-nix_eval_online() {
- nix eval --raw "$@"
-}
-
-# Collects garbage if KROPS_TEST_GC is set, so evaluating afterwards checks
-# that delivered store paths are registered as garbage collector roots.
-gc() {
- if test -n "${KROPS_TEST_GC-}"; then
- nix-store --gc >/dev/null
- fi
-}
-
-# Canaries are files outside the target directories, which populate must
-# never change, whatever the target directories contain. Some scenarios
-# point links at them. The manifest lists every entry with its type, mode,
-# modification time, and link target, and every file with its checksum.
-# Entries beside the target directories whose names start like the
-# canaries' count as well, so files created next to them show.
-canary_manifest() {
- set -- /var/krops-outside $(
- find /var/krops -mindepth 1 -maxdepth 1 \
- \( -name 'canary*' -o -name 'sibling*' \) 2>/dev/null
- )
- {
- find "$@" -printf '%y %m %T@ %p -> %l\n'
- find "$@" -type f -exec sha256sum {} +
- } 2>&1 | sort
-}
-
-# Creates the canaries, unless they are there unchanged: a file beside the
-# target directories, a directory beside them, and a directory elsewhere
-# with files, a read-only directory, and a Git work tree with a changed and
-# an untracked file, which resetting and cleaning would lose.
-canary_setup() {
- if test -e /var/krops-outside.manifest &&
- canary_manifest | cmp -s - /var/krops-outside.manifest; then
- return
- fi
- rm -fR /var/krops/canary /var/krops/sibling /var/krops-outside
- mkdir -p /var/krops/sibling /var/krops-outside/dir/sub
- echo canary > /var/krops/canary
- echo sibling > /var/krops/sibling/file
- echo outside > /var/krops-outside/file
- echo outside > /var/krops-outside/dir/file
- echo outside > /var/krops-outside/dir/sub/file
- chmod a-w /var/krops-outside/dir/sub/file /var/krops-outside/dir/sub
- git init -q /var/krops-outside/repo
- (
- cd /var/krops-outside/repo
- echo tracked > tracked
- git add tracked
- GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 \
- git -c user.name=krops -c user.email=spam@krebsco.de commit -q -m canary
- echo changed > tracked
- echo untracked > untracked
- )
- canary_manifest > /var/krops-outside.manifest
-}
-
-# Fails if a canary changed, or if a store path that an entry of the target
-# directory $T links to doesn't match its hash anymore.
-canary_check() {
- if ! canary_manifest | cmp -s - /var/krops-outside.manifest; then
- canary_manifest | diff /var/krops-outside.manifest - >&2 || :
- fail "something outside the target directory changed"
- fi
- if test -n "${T-}" && test -d "$T"; then
- ls -A "$T" | while IFS= read -r canary_name; do
- canary_link=$(readlink "$T/$canary_name" 2>/dev/null) || continue
- case $canary_link in
- /nix/store/*)
- nix-store --verify-path \
- "$(printf '%s\n' "$canary_link" | cut -d/ -f1-4)" ||
- fail "$T/$canary_name links to a store path that changed"
- ;;
- esac
- done
- fi
-}
diff --git a/tests/rsync.nix b/tests/rsync.nix
deleted file mode 100644
index 6c14048..0000000
--- a/tests/rsync.nix
+++ /dev/null
@@ -1,50 +0,0 @@
-# rsync versions whose behaviour around .rsync-filter files differs: 3.4.1
-# reads them like the files it transfers, and 3.5.0 opens them more
-# strictly. The tests run the scenarios depending on that with each,
-# whatever version nixpkgs has. rsync's own test suite doesn't run: these
-# builds only need to behave like the releases.
-{ lib, stdenv, fetchurl, fetchpatch, perl, updateAutotoolsGnuConfigScriptsHook
-, acl, libiconv, popt, zlib
-}:
-
-let
- rsync = { version, hash, patches ? [] }: stdenv.mkDerivation {
- pname = "rsync";
- inherit version patches;
- src = fetchurl {
- # signed with key 9FEF 112D CE19 A0DC 7E88 2CB8 1BB2 4997 A853 5F6F
- url = "mirror://samba/rsync/src/rsync-${version}.tar.gz";
- inherit hash;
- };
- nativeBuildInputs = [ perl updateAutotoolsGnuConfigScriptsHook ];
- # With ACLs and extended attributes, which backups use (rsync -aAX).
- buildInputs = [ acl libiconv popt zlib ];
- configureFlags = [
- "--disable-lz4"
- "--disable-openssl"
- "--disable-xxhash"
- "--disable-zstd"
- "--enable-ipv6"
- "--with-included-zlib=no"
- "--with-nobody-group=nogroup"
- ];
- enableParallelBuilding = true;
- meta.mainProgram = "rsync";
- };
-in {
- "3.4.1" = rsync {
- version = "3.4.1";
- hash = "sha256-KSS8s6Hti1UfwQH3QLnw/gogKxFQJ2R89phQ1l/YjFI=";
- patches = [
- # Compilation with GCC 15.
- (fetchpatch {
- url = "https://github.com/RsyncProject/rsync/commit/a4b926dcdce96b0f2cc0dc7744e95747b233500a.patch";
- hash = "sha256-UiEQJ+p2gtIDYNJqnxx4qKgItKIZzCpkHnvsgoxBmSE=";
- })
- ];
- };
- "3.5.0" = rsync {
- version = "3.5.0";
- hash = "sha256-x//R72U+mVQPZh5HywC3+crR7muXI5mxb5PWcmVuDTM=";
- };
-}
diff --git a/tests/scenarios.nix b/tests/scenarios.nix
index 92f94fd..ad4617d 100644
--- a/tests/scenarios.nix
+++ b/tests/scenarios.nix
@@ -1,723 +1,13 @@
-# Test scenarios for populate: every combination of each source type's
-# options, deployed to the local machine, to a remote target as root, and to
-# a remote target using sudo, plus scenarios for features spanning sources.
-#
-# A scenario prepares the target directory, then runs its phases. Each phase
-# runs populate, or writeDeploy with a stand-in for nixos-rebuild, expecting
-# it to succeed or to fail with a message, and checks the result on the
-# target. Phases expecting evaluation to fail get checked here and don't run.
-{
- # nixpkgs' lib.
- lib,
- # krops' lib.
- krops,
- # Function from populate's arguments to the command running populate.
- populate,
- # krops' writeDeploy.
- writeDeploy,
- # Where the deploying machine serves /fixtures/www via HTTP.
- url,
- # URL of the Git repository /fixtures/git/gitsrc.git.
- gitUrl,
- # Host name of the remote target.
- remoteHost,
- # Absolute path of age on the deploying machine.
- agePath,
- # HEAD of /fixtures/gitsrc, and its parent.
- gitHead,
- gitFirst,
- # Version of rsync on the deploying machine.
- rsyncVersion,
-}:
-
-let
- inherit (lib)
- attrNames concatMap concatStringsSep foldl' isBool mapAttrsToList
- optional optionalAttrs optionals optionalString;
-
- prelude = ". ${./lib.sh}\n";
-
- cartesian = attrs: foldl'
- (acc: name: concatMap
- (combination: map
- (value: combination // { ${name} = value; })
- attrs.${name})
- acc)
- [{}]
- (attrNames attrs);
-
- flagsName = flags: concatStringsSep "-" (mapAttrsToList
- (name: value: name + (if isBool value then (if value then "1" else "0") else value))
- flags);
-
- targets = {
- local = path: krops.mkTarget "" // { host = "localhost"; inherit path; };
- remote = path: {
- user = "root"; host = remoteHost; port = null; inherit path;
- sudo = false; extraOptions = [];
- };
- sudo = path: {
- user = "deploy"; host = remoteHost; port = null; inherit path;
- sudo = true; extraOptions = [];
- };
- };
-
- alt = { alt.file = "/fixtures/alt"; };
-
- # A path on a target of kind targetKind, as populate names it in what it
- # prints.
- targetName = targetKind: path: {
- local = "";
- remote = "${remoteHost}:";
- sudo = "deploy@${remoteHost}:";
- }.${targetKind} + path;
-
- # How a source draws inputs from other sources: not at all, by rewriting
- # the lock file of its flake, or by a generated flake.
- inputModes = [ "none" "rewrite" "generated" ];
-
- # The options of a source drawing inputs in mode.
- drawInputs = mode: inputs: optionalAttrs (mode != "none") ({
- inherit inputs;
- } // optionalAttrs (mode == "generated") {
- rewriteLock = false;
- });
-
- # The flake of source x that has its inputs drawn in mode.
- flakeOf = mode:
- if mode == "generated" then ''"$T"/.populate/flakes/x'' else ''"$T"/x'';
-
- # Builds a scenario from a name, a target kind, and phases with sources.
- # Isolated scenarios start with no target directories and a collected store
- # on both machines, so no other scenario's store paths or roots stand in for
- # missing ones. An exhaustive scenario covers a combination that no code
- # path tells apart from the ones the other scenarios cover, so it only runs
- # in the exhaustive tests.
- scenario = {
- name, targetKind, node ? null, isolate ? false, exhaustive ? false,
- prepare ? "", prepareDeployer ? null, phases,
- }: let
- path = "/var/krops/${name}";
- target = targets.${targetKind} path;
- vars = "T=${path}\n";
- # A phase expecting evaluation to fail has a control: sources that
- # differ only in what makes evaluation fail, or a function returning them
- # for the phase's sources, and that must evaluate. So the phase doesn't
- # pass for failing for an unrelated reason, e.g. a misspelt option.
- # targetPath deploys to another path than the scenario's target
- # directory, and controlTargetPath is the control for a path that makes
- # evaluation fail. env sets environment variables for populate, as
- # arguments of env. interrupt ends populate after that many seconds,
- # which it's expected not to finish before. output lists what a
- # succeeding populate is expected to print, and unexpected what populate
- # is expected not to print, succeeding or not. expect.fail is what a
- # failing populate is expected to print, and expect.status, if given,
- # its exit status.
- phase = {
- sources, backup ? false, prune ? false, deploy ? null,
- targetPath ? path, env ? "", interrupt ? null, output ? [],
- unexpected ? [],
- expect ? "success", control ? null, controlTargetPath ? null,
- check ? "", mutate ? null, checkDeployer ? null, mutateDeployer ? null,
- }: let
- command = sources: targetPath: let
- args = {
- source = krops.evalSource [ sources ];
- inherit backup prune;
- target = target // { path = targetPath; };
- force = true;
- };
- in
- if deploy == null then populate args
- else writeDeploy "deploy" (args // { useNixOutputMonitor = false; } // deploy);
- evaluates = sources: targetPath:
- (builtins.tryEval
- (builtins.seq (toString (command sources targetPath)) true)).success;
- script = text: if text == null then null else prelude + vars + text;
- in
- if expect == "eval-error" then
- if control == null && controlTargetPath == null then
- throw "scenario ${name}: expecting evaluation to fail needs a control"
- else if evaluates sources targetPath then
- throw "scenario ${name}: populate evaluates, but is expected to fail"
- else if !(
- if controlTargetPath != null then evaluates sources controlTargetPath
- else evaluates
- (if lib.isFunction control then control sources else control)
- targetPath
- ) then
- throw "scenario ${name}: the control doesn't evaluate either"
- else
- null
- else {
- populate = toString (command sources targetPath);
- inherit env interrupt output unexpected;
- expect = if expect == "success" then null else expect.fail;
- status = if expect == "success" then null else expect.status or null;
- # No phase gets around checking the canaries.
- check = script (check + "\ncanary_check\n");
- mutate = script mutate;
- checkDeployer = script checkDeployer;
- mutateDeployer = script mutateDeployer;
- };
- in {
- inherit name isolate exhaustive;
- node =
- if node != null then node
- else if targetKind == "local" then "deployer"
- else "target";
- prepare = prelude + vars + ''
- rm -fR "$T"
- '' + prepare;
- prepareDeployer =
- if prepareDeployer == null then null else prelude + vars + prepareDeployer;
- # For all machines: before the scenario, and after it for the ones its
- # checks don't run on.
- canarySetup = prelude + "canary_setup\n";
- canaryCheck = prelude + "canary_check\n";
- phases = lib.filter (x: x != null) (map phase phases);
- };
+# Test scenarios for krops, besides populate's, which run through
+# pkgs.populate here: writeDeploy's arguments to nixos-rebuild, which a
+# stand-in records. The arguments are those of populate's builder.nix,
+# and targets, which give the target of each kind for a path.
+{ targets, ... }@args:
- # Same sources twice, so the second phase checks redeploying.
- twice = phase: [ phase phase ];
-
- targetKinds = [ "local" "remote" "sudo" ];
-
- # Scenarios for all combinations of options and target kinds. f gets the
- # options and the scenario's name, target kind, and target directory, and
- # returns the scenario's arguments, or null for invalid combinations.
- matrix = type: options: f:
- concatMap
- (targetKind: lib.filter (x: x != null) (map
- (flags: let
- name = "${type}-${targetKind}" +
- optionalString (flags != {}) "-${flagsName flags}";
- args = f flags { inherit name targetKind; path = "/var/krops/${name}"; };
- in
- if args == null then null
- else scenario ({ inherit name targetKind; } // args))
- (cartesian options)))
- targetKinds;
-
- # Checks that the source at $T/x is a link to a store path, or to the
- # subdirectory sub therein, or a writable directory if materialized, and
- # evaluates the attribute attr of the flake there, or of its generated
- # flake, after collecting garbage.
- checkFlake = {
- attr, expected, subdir ? false, generated ? false, materialized ? false,
- }: (if materialized then ''
- test -d "$T"/x && ! test -L "$T"/x || fail "x isn't a directory"
- test -w "$T"/x/flake.nix || fail "flake.nix isn't writable"
- '' else ''
- link=$(readlink "$T"/x)
- ${if subdir then ''
- expect_eq subdirectory "$(basename "$link")" sub
- link=$(dirname "$link")
- '' else ""}
- expect_eq "store directory" "$(dirname "$link")" /nix/store
- '') + ''
- test ! -e "$T"/.populate/flake-uploads || fail "upload directory remains"
- gc
- expect_eq ${attr} "$(nix_eval ${
- if generated then ''"$T"/.populate/flakes/x'' else ''"$T"/x''
- }#${attr})" ${lib.escapeShellArg expected}
- '';
-
- copyingWithSudo = flags: ctx:
- ctx.targetKind == "sudo" && !(flags.fetchOnTarget or false);
-
- # The sources with fetchOnTarget set for all flake and flake-input sources,
- # the control for copying flakes to targets using sudo.
- withFetchOnTarget = lib.mapAttrs (_: lib.mapAttrs (type: options:
- if lib.elem type [ "flake" "flake-input" ]
- then options // { fetchOnTarget = true; }
- else options));
-
- # A phase copying flakes to a target using sudo, which isn't supported,
- # and gets refused during evaluation.
- refuseCopyingWithSudo = {
- expect = "eval-error";
- control = withFetchOnTarget;
- };
-
-in
-
-# The remote target must not count as local wherever this gets evaluated.
-assert !(krops.isLocalTarget (targets.remote "/"));
+with import ../pkgs/populate/tests/builder.nix (removeAttrs args [ "targets" ]);
{
- derivation = matrix "derivation" {} (_: _: {
- phases = twice {
- sources.x.derivation = ''
- derivation {
- name = "krops-test";
- system = builtins.currentSystem;
- builder = "/bin/sh";
- args = [ "-c" "echo built > $out" ];
- }
- '';
- check = ''
- gc
- expect_eq x "$(cat "$T"/x)" built
- '';
- };
- });
-
- file = let
- f = flags: _: {
- # Drawing inputs happens after rsync, so it doesn't depend on rsync's
- # options.
- exhaustive = flags.inputs != "none" &&
- (flags.checksum || flags.deleteExcluded || flags.filters);
- prepare = ''
- mkdir -p "$T"/x
- echo old > "$T"/x/skip-ex
- echo old > "$T"/x/skip-f
- echo stale > "$T"/x/stale
- echo BBBB > "$T"/x/same
- touch -d @1000000000 "$T"/x/same
- '';
- phases = twice {
- sources = {
- x.file = {
- path = "/fixtures/filesrc";
- useChecksum = flags.checksum;
- inherit (flags) deleteExcluded;
- exclude = optional (flags.exclude or false) "skip-ex";
- filters = optional flags.filters { type = "exclude"; pattern = "skip-f"; };
- } // drawInputs flags.inputs { dep.follows = "alt"; };
- } // optionalAttrs (flags.inputs != "none") alt;
- check = let
- excluded = file: set: ''
- ${if !set then ''
- expect_eq ${file} "$(cat "$T"/x/${file})" src
- '' else if flags.deleteExcluded then ''
- test ! -e "$T"/x/${file} || fail "${file} not deleted"
- '' else ''
- expect_eq ${file} "$(cat "$T"/x/${file})" old
- ''}
- '';
- in ''
- expect_eq keep "$(cat "$T"/x/keep)" keep
- ${excluded "skip-ex" (flags.exclude or false)}
- ${excluded "skip-f" flags.filters}
- ${if flags.deleteExcluded then ''
- test ! -e "$T"/x/stale || fail "stale not deleted"
- '' else ''
- expect_eq stale "$(cat "$T"/x/stale)" stale
- ''}
- expect_eq same "$(cat "$T"/x/same)" ${if flags.checksum then "AAAA" else "BBBB"}
- '' + optionalString (flags.inputs != "none") ''
- # Inputs not drawn from sources aren't delivered, so this needs the
- # network and can't collect garbage before.
- expect_eq msg "$(nix_eval_online ${flakeOf flags.inputs}#msg)" top:alt,sub
- '';
- };
- };
- in
- matrix "file" {
- checksum = [ false true ];
- deleteExcluded = [ false true ];
- filters = [ false true ];
- inputs = inputModes;
- } f
- ++ [
- # The deprecated exclude option, once, because it warns.
- (scenario (let
- args = f {
- checksum = false;
- deleteExcluded = true;
- exclude = true;
- filters = false;
- inputs = "none";
- } null;
- in args // {
- name = "file-local-exclude";
- targetKind = "local";
- phases = [ (builtins.head args.phases) ];
- }))
- ]
- # A source whose .rsync-filter rsync 3.5.0 and later can't read, because
- # it's reached through a symlink that neither root nor the user deploying
- # owns. (Running as root, the scenarios can't use the other common
- # cause, a directory on the way that the user may enter but not read.)
- ++ concatMap (targetKind: map (insecureLinks: let
- affected = lib.versionAtLeast rsyncVersion "3.5.0";
- excluded = !affected || insecureLinks == true;
- in scenario {
- name = "file-${targetKind}-insecureLinks" +
- (if insecureLinks == null then "Null" else if insecureLinks then "True" else "False");
- inherit targetKind;
- prepare = ''
- mkdir -p "$T"/x
- echo old > "$T"/x/garbage
- '';
- phases = twice ({
- sources.x.file = { path = "/fixtures/filtered-link"; inherit insecureLinks; };
- } // (if affected && insecureLinks == null then {
- expect.fail = "can't read /fixtures/filtered-link/.rsync-filter, so it would sync /fixtures/filtered-link/ without it.";
- } else {
- output = optional affected (
- if insecureLinks
- then "can't read /fixtures/filtered-link/.rsync-filter; it reads it with --insecure-links anyway, because insecureLinks is true."
- else "can't read /fixtures/filtered-link/.rsync-filter, so it syncs /fixtures/filtered-link/ without it, because insecureLinks is false."
- );
- check = ''
- expect_eq keep "$(cat "$T"/x/keep)" keep
- ${if excluded then ''
- test ! -e "$T"/x/garbage || fail "garbage not excluded"
- '' else ''
- expect_eq garbage "$(cat "$T"/x/garbage)" garbage
- ''}
- '';
- }));
- }) [ null true false ]) targetKinds
- ++ concatMap (targetKind: let
- affected = lib.versionAtLeast rsyncVersion "3.5.0";
- unreadable = name: { path, file, dir ? "x", env ? "" }: scenario {
- name = "file-${targetKind}-insecureLinks${name}";
- inherit targetKind;
- prepare = ''
- mkdir -p "$T"/${dir}
- echo old > "$T"/${dir}/garbage
- '';
- phases = twice ({
- sources.x.file = { inherit path; };
- inherit env;
- } // (if affected then {
- expect.fail = "can't read ${file}, so it would sync ${path}/ without it.";
- } else {
- check = ''
- expect_eq keep "$(cat "$T"/${dir}/keep)" keep
- test ! -e "$T"/${dir}/garbage || fail "garbage not excluded"
- '';
- }));
- };
- in [
- # The filter file is in the directory above what gets synced, where it
- # applies as well.
- (unreadable "Parent" {
- path = "/fixtures/filtered-parent-link/sub";
- file = "/fixtures/filtered-parent-link/.rsync-filter";
- })
- # The filter file is in a directory whose name rsync prints escaped
- # in the C locale.
- (unreadable "Name" {
- path = "/fixtures/filtered-name-link";
- file = "/fixtures/filtered-name-link/bär/.rsync-filter";
- dir = "x/bär";
- env = "LC_ALL=C";
- })
- ]) targetKinds;
-
- flake = matrix "flake" {
- kind = [ "git" "path" "subdir" "remote" ];
- fetchOnTarget = [ false true ];
- inputs = inputModes;
- materialize = [ false true ];
- } (flags: ctx: let
- flake = {
- git = "/fixtures/top";
- path = "path:/fixtures/top";
- subdir = "git+file:/fixtures/mono?dir=sub";
- remote = "${url}/remote.tar.gz";
- }.${flags.kind};
- msg = {
- git = "top:${if flags.inputs != "none" then "alt" else "dep+inner"},sub";
- path = msg.git;
- remote = "remote:${if flags.inputs != "none" then "alt" else "dep+inner"}";
- };
- in
- # The flake in a subdirectory has no inputs to draw from sources.
- if flags.kind == "subdir" && flags.inputs != "none" then null else {
- isolate = true;
- # Materializing copies what got delivered, whichever way, so it depends
- # on how inputs get drawn and on subdirectories, not on how the flake got
- # there.
- exhaustive = flags.materialize && !lib.elem flags.kind [ "git" "subdir" ];
- phases = twice ({
- sources = {
- x.flake = {
- url = flake;
- inherit (flags) fetchOnTarget materialize;
- } // drawInputs flags.inputs { dep.follows = "alt"; };
- } // optionalAttrs (flags.inputs != "none") alt;
- } // (if copyingWithSudo flags ctx then refuseCopyingWithSudo else {
- check = if flags.kind == "subdir" then checkFlake {
- attr = "who"; expected = "sub"; subdir = true;
- materialized = flags.materialize;
- } else checkFlake {
- attr = "msg"; expected = msg.${flags.kind};
- generated = flags.inputs == "generated";
- materialized = flags.materialize;
- };
- }));
- });
-
- flake-input = concatMap
- (name: matrix "flake-input-${lib.replaceStrings ["/"] ["."] name}" ({
- fetchOnTarget = [ false true ];
- materialize = [ false true ];
- } // optionalAttrs (name == "dep") {
- inputs = inputModes;
- }) (flags: ctx: let
- inputs = flags.inputs or "none";
- in {
- isolate = true;
- # Materializing copies what got delivered, whichever input it is.
- exhaustive = flags.materialize && name != "dep";
- phases = twice ({
- sources = {
- x.flake-input = {
- url = "/fixtures/top";
- inherit name;
- inherit (flags) fetchOnTarget materialize;
- } // drawInputs inputs { inner.follows = "alt"; };
- } // optionalAttrs (inputs != "none") alt;
- } // (if copyingWithSudo flags ctx then refuseCopyingWithSudo else {
- check = {
- dep = checkFlake {
- attr = "greeting";
- expected = if inputs != "none" then "dep+alt" else "dep+inner";
- generated = inputs == "generated";
- materialized = flags.materialize;
- };
- "dep/inner" = checkFlake {
- attr = "who"; expected = "inner"; materialized = flags.materialize;
- };
- sub = checkFlake {
- attr = "who"; expected = "sub"; subdir = true;
- materialized = flags.materialize;
- };
- }.${name};
- }));
- }))
- [ "dep" "dep/inner" "sub" ];
-
- git = matrix "git" {
- shallow = [ false true ];
- fetchAlways = [ false true ];
- cleanExclude = [ false true ];
- inputs = inputModes;
- } (flags: _: let
- check = ''
- expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitHead}
- expect_eq commits "$(git -C "$T"/x rev-list --count HEAD)" ${
- if flags.shallow then "1" else "2"
- }
- test -e "$T"/x/two || fail "two missing"
- '' + optionalString (flags.inputs == "generated") ''
- grep -q '"git+file:' "$T"/.populate/flakes/x/flake.nix ||
- fail "the Git work tree isn't referred to as git+file:"
- '' + optionalString (flags.inputs != "none") ''
- gc
- expect_eq msg "$(nix_eval ${flakeOf flags.inputs}#msg)" git:alt
- '';
- sources = ref: {
- x.git = {
- url = gitUrl;
- inherit ref;
- inherit (flags) shallow fetchAlways;
- clean.exclude = optional flags.cleanExclude "keepme";
- } // drawInputs flags.inputs { dep.follows = "alt"; };
- } // optionalAttrs (flags.inputs != "none") alt;
- in {
- # Drawing inputs happens after Git, so it doesn't depend on Git's
- # options.
- exhaustive = flags.inputs != "none" &&
- (flags.shallow || flags.fetchAlways || flags.cleanExclude);
- phases = [
- # The older commit first, so deploying HEAD changes the work tree, and
- # fetches if fetchAlways is set or the clone is shallow.
- {
- sources = sources gitFirst;
- check = ''
- expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitFirst}
- expect_eq commits "$(git -C "$T"/x rev-list --count HEAD)" 1
- test ! -e "$T"/x/two || fail "two present"
- '';
- }
- {
- sources = sources gitHead;
- inherit check;
- mutate = ''
- echo junk > "$T"/x/junk
- echo keepme > "$T"/x/keepme
- '';
- }
- {
- sources = sources gitHead;
- check = check + ''
- test ! -e "$T"/x/junk || fail "junk not cleaned"
- ${if flags.cleanExclude then ''
- test -e "$T"/x/keepme || fail "keepme cleaned"
- '' else ''
- test ! -e "$T"/x/keepme || fail "keepme not cleaned"
- ''}
- '';
- }
- ];
- })
- ++ map (targetKind: scenario (let
- # The commit the first phase adds on top of gitHead, deterministic like
- # the ones fixtures.sh creates.
- gitThird = "c2e0f9d727337617a548284552909abe0f196d05";
- in {
- # A full clone fetches a commit that appears on the server afterwards,
- # also without fetchAlways.
- name = "git-${targetKind}-fetch";
- inherit targetKind;
- prepareDeployer = ''
- git -C /fixtures/git/gitsrc.git update-ref -d refs/heads/extra
- '';
- phases = [
- {
- sources.x.git = { url = gitUrl; ref = gitHead; };
- check = ''
- expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitHead}
- '';
- mutateDeployer = ''
- export GIT_AUTHOR_NAME=krops GIT_AUTHOR_EMAIL=spam@krebsco.de
- export GIT_COMMITTER_NAME=krops GIT_COMMITTER_EMAIL=spam@krebsco.de
- export GIT_AUTHOR_DATE='1000000000 +0000' GIT_COMMITTER_DATE='1000000000 +0000'
- export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
- cd /fixtures/gitsrc
- git checkout -q -B extra ${gitHead}
- echo three > three
- git add -A
- git commit -q -m three
- git push -q -f /fixtures/git/gitsrc.git extra
- git checkout -q master
- expect_eq "third commit" "$(git rev-parse extra)" ${gitThird}
- '';
- }
- {
- sources.x.git = { url = gitUrl; ref = gitThird; };
- check = ''
- expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitThird}
- test -e "$T"/x/three || fail "three missing"
- '';
- }
- ];
- })) targetKinds
- # Branches and tags follow the server: a branch that moves between two
- # deploys, and a tag, with full and shallow clones.
- ++ concatMap (targetKind: map (shallow: scenario {
- name = "git-${targetKind}-shallow${if shallow then "1" else "0"}-refs";
- inherit targetKind;
- prepareDeployer = ''
- git -C /fixtures/git/gitsrc.git update-ref refs/heads/moving ${gitFirst}
- git -C /fixtures/git/gitsrc.git update-ref refs/tags/first ${gitFirst}
- '';
- phases = [
- {
- sources.x.git = { url = gitUrl; ref = "moving"; inherit shallow; };
- check = ''
- expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitFirst}
- '';
- mutateDeployer = ''
- git -C /fixtures/git/gitsrc.git update-ref refs/heads/moving ${gitHead}
- '';
- }
- {
- sources.x.git = { url = gitUrl; ref = "moving"; inherit shallow; };
- check = ''
- expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitHead}
- '';
- }
- {
- sources.x.git = { url = gitUrl; ref = "first"; inherit shallow; };
- check = ''
- expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitFirst}
- '';
- }
- ];
- }) [ false true ]) targetKinds
-
- # A git source stops drawing inputs: its lock file, which Nix created and
- # staged, goes, so the work tree is as the commit has it.
- ++ map (targetKind: scenario {
- name = "git-${targetKind}-dropInputs";
- inherit targetKind;
- phases = [
- {
- sources = alt // {
- x.git = { url = gitUrl; ref = gitHead; inputs.dep.follows = "alt"; };
- };
- check = ''
- expect_eq msg "$(nix_eval_online git+file:"$T"/x#msg)" git:alt
- '';
- }
- {
- sources = alt // { x.git = { url = gitUrl; ref = gitHead; }; };
- check = ''
- expect_eq status "$(git -C "$T"/x status --porcelain)" ""
- '';
- }
- ];
- }) targetKinds;
-
- pass = matrix "pass" {
- store = [ "pass" "pass-git" ];
- } (flags: _: {
- phases = twice {
- sources.x.pass = { dir = "/fixtures/${flags.store}"; name = "x"; };
- check = ''
- expect_eq a "$(cat "$T"/x/a)" secret-a
- '';
- };
- });
-
- passage = matrix "passage" {
- age = [ "default" "path" ];
- dir = [ "store" "subdir" ];
- } (flags: _: {
- phases = twice {
- sources.x.passage = {
- dir = "/fixtures/passage/store" + optionalString (flags.dir == "subdir") "/x";
- identities_file = "/fixtures/passage/identities";
- } // optionalAttrs (flags.age == "path") {
- age = agePath;
- };
- check = ''
- expect_eq a "$(cat "$T"/x/${optionalString (flags.dir == "store") "x/"}a)" secret-a
- '';
- };
- });
-
- pipe = matrix "pipe" {} (_: _: {
- phases = twice {
- sources.x.pipe = "/fixtures/pipe";
- check = ''
- expect_eq x "$(cat "$T"/x)" piped
- '';
- };
- });
-
- symlink = matrix "symlink" {
- absolute = [ false true ];
- inputs = [ false true ];
- } (flags: ctx: let
- target = optionalString flags.absolute "${ctx.path}/" + "topcopy";
- in {
- phases = twice {
- sources = {
- x.symlink = {
- inherit target;
- inputs = optionalAttrs flags.inputs { dep.follows = "alt"; };
- };
- topcopy.file = "/fixtures/filesrc";
- } // optionalAttrs flags.inputs alt;
- check = ''
- expect_eq link "$(readlink "$T"/x)" ${target}
- test -e "$T"/x/flake.nix || fail "flake.nix missing"
- '' + optionalString flags.inputs ''
- expect_eq msg "$(nix_eval_online "$T"/.populate/flakes/x#msg)" top:alt,sub
- '';
- };
- });
-
# writeDeploy's arguments to nixos-rebuild, which a stand-in records.
# flake is what gets deployed: nothing, or a flake from a file, git, or
# flake source, or one whose inputs get drawn from a source, by rewriting
@@ -815,1689 +105,4 @@ assert !(krops.isLocalTarget (targets.remote "/"));
}];
}) targetKinds;
- # Features spanning several sources.
- features = concatMap (targetKind: let
- s = name: args: scenario ({
- name = "${name}-${targetKind}";
- inherit targetKind;
- isolate = true;
- } // args);
- copying = targetKind != "sudo";
- fetchOnTarget = !copying;
- # A scenario for each way of drawing inputs, which f gets as mode, along
- # with draw, which adds inputs drawn that way to a source's options, and
- # flake, which gives the flake to evaluate for a source.
- modes = name: f: map
- (mode: s "${name}-${mode}" (f {
- draw = drawInputs mode;
- flake = n:
- if mode == "generated" then ''"$T"/.populate/flakes/${n}'' else ''"$T"/${n}'';
- inherit mode;
- }))
- [ "rewrite" "generated" ];
- in lib.flatten [
- # A source draws an input from a source that draws inputs itself.
- (modes "chain" ({ draw, flake, ... }: {
- phases = twice {
- sources = alt // {
- a.flake = { url = "/fixtures/top"; inherit fetchOnTarget; } // draw { dep.follows = "b"; };
- b.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- } // draw { inner.follows = "alt"; };
- };
- check = ''
- gc
- expect_eq msg "$(nix_eval ${flake "a"}#msg)" top:dep+alt,sub
- '';
- };
- }))
- # An input of an input drawn from a source gets drawn from another
- # source.
- (modes "nested" ({ draw, flake, ... }: {
- phases = twice {
- sources = alt // {
- x.flake = {
- url = "/fixtures/top"; inherit fetchOnTarget;
- } // draw { dep = { follows = "b"; inputs.inner.follows = "alt"; }; };
- b.flake-input = { url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; };
- };
- check = ''
- gc
- expect_eq msg "$(nix_eval ${flake "x"}#msg)" top:dep+alt,sub
- '';
- };
- }))
- # The lock file gets rewritten or generated again when a source inputs
- # get drawn from changes.
- (modes "relock" ({ draw, flake, ... }: let
- x.file = { path = "/fixtures/filesrc"; } // draw { dep.follows = "a"; };
- in {
- phases = [
- {
- sources = { inherit x; a.file = "/fixtures/alt"; };
- check = ''
- expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" top:alt,sub
- '';
- }
- {
- sources = { inherit x; a.file = "/fixtures/alt2"; };
- check = ''
- expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" top:alt2,sub
- '';
- }
- ];
- }))
- # What drawing inputs needs gets removed once a source draws no inputs
- # anymore.
- (modes "removal" ({ draw, mode, ... }: let
- state = {
- rewrite = "derived/x/result";
- generated = "flakes/x/flake.lock";
- }.${mode};
- in {
- phases = [
- {
- sources = alt // {
- x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; } // draw { dep.follows = "alt"; };
- };
- check = ''
- test -e "$T"/.populate/${state} || fail "${state} missing"
- '';
- }
- {
- sources = alt // {
- x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; };
- };
- check = ''
- test ! -e "$T"/.populate/${dirOf state} || fail "${dirOf state} remains"
- '' + checkFlake { attr = "msg"; expected = "top:dep+inner,sub"; };
- }
- ];
- }))
- # Patches get applied in order to a copy, which becomes a store path and
- # gets made again only when the patches change.
- (s "patches" (let
- x = patches: {
- x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget patches;
- };
- };
- both = x [ "/fixtures/patches/hello.patch" "/fixtures/patches/hello2.patch" ];
- in {
- phases = [
- {
- sources = both;
- check = checkFlake { attr = "greeting"; expected = "hello2+inner"; };
- # The key changes whenever the copy gets made again, which
- # the store path doesn't show, being the same.
- mutate = ''
- readlink "$T"/x > "$T".first
- stat -c %y "$T"/.populate/derived/x/key > "$T".key
- '';
- }
- {
- sources = both;
- check = checkFlake { attr = "greeting"; expected = "hello2+inner"; } + ''
- expect_eq "store path" "$(readlink "$T"/x)" "$(cat "$T".first)"
- expect_eq "copy made at" "$(stat -c %y "$T"/.populate/derived/x/key)" "$(cat "$T".key)"
- '';
- }
- {
- sources = x [ "/fixtures/patches/hello.patch" ];
- check = checkFlake { attr = "greeting"; expected = "hello+inner"; } + ''
- test "$(readlink "$T"/x)" != "$(cat "$T".first)" || fail "store path unchanged"
- expect_eq patches "$(ls "$T"/.populate/patches/x)" 0001-hello.patch
- '';
- }
- {
- sources = x [];
- check = checkFlake { attr = "greeting"; expected = "dep+inner"; } + ''
- test ! -e "$T"/.populate/patches || fail "patches remain"
- test ! -e "$T"/.populate/derived || fail "derived remains"
- '';
- }
- ];
- }))
- (s "patch-fails" {
- phases = [{
- sources.x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- patches = [ "/fixtures/patches/bad.patch" ];
- };
- expect.fail = "error: patch bad.patch does not apply to source x";
- }];
- })
- # Patches apply in the subdirectory the target path shows.
- (s "patch-subdir" {
- phases = twice {
- sources.x.flake-input = {
- url = "/fixtures/top"; name = "sub"; inherit fetchOnTarget;
- patches = [ "/fixtures/patches/sub.patch" ];
- };
- check = checkFlake { attr = "who"; expected = "patched"; subdir = true; };
- };
- })
- # A copy with a rewritten lock file gets made again only when a source
- # it draws inputs from changes.
- (s "rewrite-cache" (let
- x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- inputs.inner.follows = "a";
- };
- in {
- phases = [
- {
- sources = { inherit x; a.file = "/fixtures/alt"; };
- check = checkFlake { attr = "greeting"; expected = "dep+alt"; };
- # The key changes whenever the copy gets made again, which
- # the store path doesn't show, being the same.
- mutate = ''
- readlink "$T"/x > "$T".first
- stat -c %y "$T"/.populate/derived/x/key > "$T".key
- '';
- }
- {
- sources = { inherit x; a.file = "/fixtures/alt"; };
- check = checkFlake { attr = "greeting"; expected = "dep+alt"; } + ''
- expect_eq "store path" "$(readlink "$T"/x)" "$(cat "$T".first)"
- expect_eq "copy made at" "$(stat -c %y "$T"/.populate/derived/x/key)" "$(cat "$T".key)"
- '';
- }
- {
- sources = { inherit x; a.file = "/fixtures/alt2"; };
- check = checkFlake { attr = "greeting"; expected = "dep+alt2"; } + ''
- test "$(readlink "$T"/x)" != "$(cat "$T".first)" || fail "store path unchanged"
- '';
- }
- ];
- }))
- # A materialized input is a writable copy, patched and with a rewritten
- # lock file, which gets made again only when what it derives from
- # changes, and becomes a link again when not materialized anymore.
- (s "materialize" (let
- x = patches: alt // {
- x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget patches;
- materialize = true;
- inputs.inner.follows = "alt";
- };
- };
- hello = [ "/fixtures/patches/hello.patch" ];
- in {
- phases = [
- {
- sources = x hello;
- check = ''
- test ! -L "$T"/x || fail "x is a link"
- test -w "$T"/x/flake.nix || fail "flake.nix isn't writable"
- gc
- expect_eq greeting "$(nix_eval "$T"/x#greeting)" hello+alt
- '';
- mutate = ''
- echo local > "$T"/x/local
- '';
- }
- {
- sources = x hello;
- check = ''
- expect_eq local "$(cat "$T"/x/local)" local
- '';
- }
- {
- sources = x (hello ++ [ "/fixtures/patches/hello2.patch" ]);
- check = ''
- test ! -e "$T"/x/local || fail "local remains"
- gc
- expect_eq greeting "$(nix_eval "$T"/x#greeting)" hello2+alt
- '';
- }
- {
- sources = alt // {
- x.flake-input = { url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; };
- };
- check = checkFlake { attr = "greeting"; expected = "dep+inner"; } + ''
- test ! -e "$T"/.populate/derived || fail "derived remains"
- '';
- }
- ];
- }))
- # Roots follow the delivered input.
- (s "retarget" {
- phases = [
- {
- sources.x.flake-input = { url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; };
- check = ''
- expect_eq roots "$(ls "$T"/.populate/flake-inputs/x | wc -l)" 1
- '';
- }
- {
- sources.x.flake-input = { url = "/fixtures/top"; name = "sub"; inherit fetchOnTarget; };
- check = ''
- test ! -e "$T"/.populate/flake-inputs/x || fail "roots remain"
- test ! -e "$T"/.populate/flake-inputs || fail "empty roots directory remains"
- '' + checkFlake { attr = "who"; expected = "sub"; subdir = true; };
- }
- ];
- })
- # Pruning removes everything sources don't deliver anymore, including
- # garbage collector roots, and keeps everything else.
- (s "prune" (let
- sources = {
- a.file = "/fixtures/alt";
- b.flake = { url = "/fixtures/top"; inherit fetchOnTarget; };
- c.flake-input = { url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; };
- d.symlink = "a";
- };
- in {
- prepare = ''
- mkdir -p "$T"
- echo other > "$T"/other
- '';
- phases = [
- {
- inherit sources;
- check = ''
- test -e "$T"/.populate/flake-inputs/b || fail "roots of b missing"
- test -e "$T"/.populate/flake-inputs/c || fail "roots of c missing"
- '';
- }
- {
- # Without pruning, nothing gets removed.
- sources = { inherit (sources) a; };
- check = ''
- for name in b c d other .populate/flake-inputs/b .populate/flake-inputs/c; do
- test -e "$T"/$name || fail "$name removed"
- done
- '';
- # An upload an interrupted run left behind.
- mutate = ''
- mkdir -p "$T"/.populate/flake-uploads/c/tree
- '';
- }
- {
- # b changes its type, so its roots go, while b stays.
- sources = { inherit (sources) a; b.symlink = "a"; };
- prune = true;
- output = map
- (name: "pruning '${targetName targetKind "/var/krops/prune-${targetKind}/${name}"}'...")
- [ "c" "d" "other" ];
- check = ''
- test -e "$T"/a/flake.nix || fail "a removed"
- expect_eq b "$(readlink "$T"/b)" a
- test -d "$T"/.populate || fail ".populate removed"
- for name in c d other .populate/flake-inputs .populate/flake-uploads; do
- test ! -e "$T"/$name || fail "$name not pruned"
- done
- if nix-store --gc --print-roots 2>/dev/null | grep -F "$T/"; then
- fail "garbage collector roots remain"
- fi
- '';
- }
- ];
- }))
- (s "cycle" {
- phases = [{
- sources = {
- a.file = { path = "/fixtures/alt"; inputs.foo.follows = "b"; };
- b.file = { path = "/fixtures/alt"; inputs.foo.follows = "a"; };
- };
- expect = "eval-error";
- control = {
- a.file = { path = "/fixtures/alt"; inputs.foo.follows = "b"; };
- b.file = "/fixtures/alt";
- };
- }];
- })
- (s "self" {
- phases = [{
- sources.a.file = { path = "/fixtures/alt"; inputs.foo.follows = "a"; };
- expect = "eval-error";
- control = alt // {
- a.file = { path = "/fixtures/alt"; inputs.foo.follows = "alt"; };
- };
- }];
- })
- (s "missing" {
- phases = [{
- sources.a.file = { path = "/fixtures/alt"; inputs.foo.follows = "nope"; };
- expect = "eval-error";
- control = {
- a.file = { path = "/fixtures/alt"; inputs.foo.follows = "nope"; };
- nope.file = "/fixtures/alt";
- };
- }];
- })
- (s "noflake" {
- phases = [{
- sources = alt // {
- x.file = { path = "/fixtures/noflake"; inputs.foo.follows = "alt"; };
- };
- expect.fail = "error: source x draws flake inputs from other sources, but contains no flake.nix";
- }];
- })
- (s "noinput" {
- phases = [{
- sources.x.flake-input = { url = "/fixtures/top"; name = "nope"; inherit fetchOnTarget; };
- expect.fail = "error: flake /fixtures/top has no input nope";
- }];
- })
- # rsync's log gives sizes in units of 1024, as %'''l asks for.
- (s "rsync-units" {
- prepareDeployer = ''
- rm -fR /tmp/rsync-units
- mkdir /tmp/rsync-units
- head -c 2048 /dev/zero > /tmp/rsync-units/f
- '';
- phases = [{
- sources.x.file = "/tmp/rsync-units";
- output = [ "/2.00K" ];
- }];
- })
- # A pipe command without #!, which a shell runs as a script.
- (s "pipe-script" {
- prepareDeployer = ''
- printf 'echo piped\n' > /tmp/pipe-script
- chmod +x /tmp/pipe-script
- '';
- phases = [{
- sources.x.pipe = "/tmp/pipe-script";
- check = ''
- expect_eq x "$(cat "$T"/x)" piped
- '';
- }];
- })
- # A pass source gets decrypted again only when the last commit of its
- # passwords changes, which .pass_info records.
- (s "pass-changes" (let
- sources.x.pass = { dir = "/tmp/pass-changes"; name = "x"; };
- in {
- prepareDeployer = ''
- rm -fR /tmp/pass-changes
- cp -R /fixtures/pass-git /tmp/pass-changes
- '';
- phases = [
- {
- inherit sources;
- check = ''
- expect_eq a "$(cat "$T"/x/a)" secret-a
- '';
- mutate = ''
- echo local-change > "$T"/x/a
- '';
- }
- {
- inherit sources;
- check = ''
- expect_eq a "$(cat "$T"/x/a)" local-change
- '';
- mutateDeployer = ''
- export GIT_AUTHOR_NAME=krops GIT_AUTHOR_EMAIL=spam@krebsco.de
- export GIT_COMMITTER_NAME=krops GIT_COMMITTER_EMAIL=spam@krebsco.de
- printf 'secret-b\n' |
- PASSWORD_STORE_DIR=/tmp/pass-changes pass insert -m -f x/a >/dev/null
- '';
- }
- {
- inherit sources;
- check = ''
- expect_eq a "$(cat "$T"/x/a)" secret-b
- '';
- }
- ];
- }))
- # An input locked to a path the target doesn't have.
- (s "gone" {
- phases = [{
- sources.x.flake = { url = "/fixtures/usegone"; fetchOnTarget = true; };
- expect.fail = "error: flake /fixtures/usegone: input gone refers to /fixtures/gone, which does not exist on the target";
- }];
- })
- ]) targetKinds;
-
- # Edge cases of the flake source types and of generated flakes: unusual lock
- # files, references, and changes between deploys.
- edge-cases = concatMap (targetKind: let
- s = name: args: scenario ({
- name = "${name}-${targetKind}";
- inherit targetKind;
- isolate = true;
- } // args);
- copying = targetKind != "sudo";
- fetchOnTarget = !copying;
- roots = n: ''
- expect_eq roots "$(ls "$T"/.populate/flake-inputs/x 2>/dev/null | wc -l)" ${toString n}
- '';
- # A scenario for each way of drawing inputs, which f gets as mode, along
- # with draw, which adds inputs drawn that way to a source's options, and
- # flake, which gives the flake to evaluate for a source.
- modes = name: f: map
- (mode: s "${name}-${mode}" (f {
- draw = drawInputs mode;
- flake = n:
- if mode == "generated" then ''"$T"/.populate/flakes/${n}'' else ''"$T"/${n}'';
- inherit mode;
- }))
- [ "rewrite" "generated" ];
- in lib.flatten [
- # The registry entry kropsremote, which fixtures.sh adds on the deploying
- # machine, points at ${url}/remote.tar.gz. The deploying machine has no
- # global registry, which it couldn't download.
- (s "indirect" {
- phases = twice {
- sources.x.flake = { url = "kropsremote"; inherit fetchOnTarget; };
- check = checkFlake { attr = "msg"; expected = "remote:dep+inner"; };
- };
- })
- (s "remote-subdir" {
- phases = twice {
- sources.x.flake = { url = "${url}/mono.tar.gz?dir=sub"; inherit fetchOnTarget; };
- check = checkFlake { attr = "who"; expected = "sub"; subdir = true; };
- };
- })
- (modes "subdir-inputs" ({ draw, mode, ... }: {
- phases = twice {
- sources = alt // {
- x.flake = {
- url = "git+file:/fixtures/monoin?dir=sub"; inherit fetchOnTarget;
- } // draw { dep.follows = "alt"; };
- };
- check = checkFlake {
- attr = "msg"; expected = "monoin:alt"; subdir = true;
- generated = mode == "generated";
- };
- };
- }))
- (s "dirty" {
- phases = twice {
- sources.x.flake = { url = "/fixtures/dirty"; inherit fetchOnTarget; };
- check = checkFlake { attr = "msg"; expected = "dirty:dep+inner,sub"; } + ''
- test ! -e "$T"/x/untracked || fail "untracked file delivered"
- '';
- };
- })
- (s "unlocked" {
- phases = [{
- sources.x.flake = { url = "/fixtures/unlocked"; inherit fetchOnTarget; };
- expect.fail = "requires lock file changes";
- }];
- })
- (s "notdir" {
- phases = [{
- sources = {
- x.file = { path = "/fixtures/filesrc"; inputs.dep.follows = "p"; };
- p.pipe = "/fixtures/pipe";
- };
- expect.fail = "error: source p is not a directory";
- # Locking the flake with an empty URL would lock the current
- # directory, the login directory on a remote target.
- unexpected = [ "while fetching the input" ];
- }];
- })
- (s "noinput-nested" {
- phases = [{
- sources.x.flake-input = { url = "/fixtures/top"; name = "dep/nope"; inherit fetchOnTarget; };
- expect.fail = "error: flake /fixtures/top has no input dep/nope";
- }];
- })
- (s "shared" {
- phases = twice {
- sources = {
- x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; };
- dep.flake-input = { url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget; };
- sub.flake-input = { url = "/fixtures/top"; name = "sub"; fetchOnTarget = true; };
- };
- check = checkFlake { attr = "msg"; expected = "top:dep+inner,sub"; } + ''
- expect_eq dep "$(nix_eval "$T"/dep#greeting)" dep+inner
- expect_eq sub "$(nix_eval "$T"/sub#who)" sub
- '';
- };
- })
- (s "follows-empty" {
- phases = twice {
- sources.x.flake = { url = "/fixtures/nofollow"; fetchOnTarget = true; };
- check = checkFlake { attr = "msg"; expected = "nofollow"; };
- };
- })
- (s "follows-cycle" {
- phases = twice {
- sources.x.flake = { url = "/fixtures/cyc"; fetchOnTarget = true; };
- check = checkFlake { attr = "msg"; expected = "cyc:cyca,cycb"; };
- };
- })
- # Names resolve through the parent's lock file, so dep/inner is the
- # parent's inner, which dep's inner follows.
- (s "follows-name" {
- phases = twice {
- sources.x.flake-input = { url = "/fixtures/follow"; name = "dep/inner"; inherit fetchOnTarget; };
- check = checkFlake { attr = "who"; expected = "inner2"; };
- };
- })
- # The delivered input comes with the inputs its own lock file pins, which
- # evaluating it needs, not with the parent's inner, which it follows there.
- (s "follows-closure" {
- phases = twice {
- sources.x.flake-input = { url = "/fixtures/follow"; name = "dep"; inherit fetchOnTarget; };
- check = checkFlake { attr = "greeting"; expected = "dep+inner"; } + roots 1;
- };
- })
- (s "relative-input" {
- phases = twice {
- sources.x.flake = { url = "/fixtures/rel"; fetchOnTarget = true; };
- check = checkFlake { attr = "msg"; expected = "rel:lib"; };
- };
- })
- # The target can't fetch an input of the flake. The deploying machine
- # fetched it when locking, so its fetcher cache has to go.
- (s "fetch-failure" {
- prepareDeployer = ''
- rm -fR /root/.cache/nix
- '';
- phases = [{
- sources.x.flake = { url = "/fixtures/broken"; fetchOnTarget = true; };
- expect.fail = "vanishing.tar.gz";
- }];
- })
- # The delivered input itself is locked to a path the target lacks.
- (s "gone-input" {
- phases = [{
- sources.x.flake-input = { url = "/fixtures/usegone"; name = "gone"; fetchOnTarget = true; };
- expect.fail = "input gone refers to /fixtures/gone, which does not exist on the target";
- }];
- })
- # An input that gets drawn from a source isn't needed on the target, with
- # all it pins.
- (modes "overridden-roots" ({ draw, mode, ... }: {
- # What gets delivered doesn't depend on how inputs get drawn.
- exhaustive = mode == "generated";
- phases = twice {
- sources = alt // {
- x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- } // draw { inner.follows = "alt"; };
- };
- check = checkFlake {
- attr = "greeting"; expected = "dep+alt";
- generated = mode == "generated";
- } + roots 0;
- };
- }))
- # The input's own lock file pins something unreachable, which gets drawn
- # from a source anyway.
- (modes "own-lock-unreachable" ({ draw, mode, ... }: {
- # What gets delivered doesn't depend on how inputs get drawn.
- exhaustive = mode == "generated";
- prepareDeployer = ''
- rm -fR /root/.cache/nix
- '';
- phases = twice {
- sources = alt // {
- x.flake-input = {
- url = "/fixtures/usepinned"; name = "dep"; inherit fetchOnTarget;
- } // draw { inner.follows = "alt"; };
- };
- check = checkFlake {
- attr = "greeting"; expected = "pinned+alt";
- generated = mode == "generated";
- };
- };
- }))
- # The input's own lock file pins something unreachable, or lacks an input
- # its flake.nix has. That fails, unless the input isn't required to come
- # with its inputs.
- (s "own-lock-broken" (let
- x = url: requireInputs: {
- x.flake-input = { inherit url; name = "dep"; inherit fetchOnTarget requireInputs; };
- };
- delivered = ''
- test -e "$T"/x/flake.nix || fail "input not delivered"
- '';
- in {
- prepareDeployer = ''
- rm -fR /root/.cache/nix
- '';
- phases = [
- {
- sources = x "/fixtures/usepinned" true;
- expect.fail = "error: source x: the inputs its own lock file pins can't be delivered. Set its requireInputs to false to deliver it anyway.";
- }
- {
- sources = x "/fixtures/usepinned" false;
- check = delivered + roots 0;
- }
- {
- sources = x "/fixtures/usestale" true;
- expect.fail = "error: source x: the inputs its own lock file pins can't be delivered. Set its requireInputs to false to deliver it anyway.";
- }
- {
- sources = x "/fixtures/usestale" false;
- check = delivered + roots 2;
- }
- {
- sources = x "/fixtures/usestale" false;
- check = delivered + roots 2;
- }
- ];
- }))
- # Redeploying an unchanged input while where it comes from is
- # unreachable and the fetcher caches are gone: the store has it.
- (s "offline-redeploy" (let
- sources.x.flake-input = { url = "/fixtures/offline"; name = "gap"; inherit fetchOnTarget; };
- check = checkFlake { attr = "who"; expected = "gap"; };
- in {
- prepareDeployer = ''
- if test -e /fixtures/www/gap.tar.gz.off; then
- mv /fixtures/www/gap.tar.gz.off /fixtures/www/gap.tar.gz
- fi
- '';
- phases = [
- {
- inherit sources check;
- mutate = ''
- rm -fR /root/.cache/nix
- '';
- mutateDeployer = ''
- rm -fR /root/.cache/nix
- mv /fixtures/www/gap.tar.gz /fixtures/www/gap.tar.gz.off
- '';
- }
- {
- inherit sources check;
- mutateDeployer = ''
- mv /fixtures/www/gap.tar.gz.off /fixtures/www/gap.tar.gz
- '';
- }
- ];
- }))
- (s "flake-to-file" {
- phases = [
- {
- sources.x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; };
- check = roots 3;
- mutate = ''
- readlink "$T"/x > "$T"/old
- '';
- }
- {
- sources.x.file = "/fixtures/alt";
- check = ''
- test ! -L "$T"/x || fail "x is still a link to $(readlink "$T"/x)"
- test -e "$T"/x/flake.nix || fail "flake.nix missing"
- nix-store --verify-path "$(cat "$T"/old)" || fail "store path changed"
- '' + roots 0;
- }
- ];
- })
- (s "symlink-to-flake" {
- phases = [
- {
- sources = alt // { x.symlink = "alt"; };
- check = ''
- expect_eq link "$(readlink "$T"/x)" alt
- '';
- }
- {
- sources = alt // { x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; }; };
- check = checkFlake { attr = "msg"; expected = "top:dep+inner,sub"; } + ''
- test -e "$T"/alt/flake.nix || fail "alt changed"
- '';
- }
- ];
- })
- # The ssh master stops, after failing as well as after succeeding.
- (s "ssh-master" (let
- checkDeployer = ''
- for i in 1 2 3 4 5 6 7 8 9 10; do
- if ! pgrep -f 'ssh: .*/populate\..*\[mux\]' >/dev/null; then
- exit 0
- fi
- sleep 0.5
- done
- fail "ssh master remains"
- '';
- in {
- phases = [
- {
- sources.x.flake-input = { url = "/fixtures/top"; name = "nope"; inherit fetchOnTarget; };
- expect.fail = "error: flake /fixtures/top has no input nope";
- inherit checkDeployer;
- }
- {
- sources = alt;
- inherit checkDeployer;
- }
- ];
- }))
- # Pruning entries whose names a shell's read would strip or split.
- (s "prune-names" {
- prepare = ''
- mkdir -p "$T/ lead" "$T/trail " "$T/$(printf 'new\nline')" "$T/axb" "$T/[b]"
- '';
- phases = [{
- sources = alt;
- prune = true;
- check = ''
- # In the C locale, ls sorts by bytes, so . comes first.
- expect_eq entries "$(LC_ALL=C ls -A "$T" | tr '\n' ' ')" '.populate alt '
- '';
- }];
- })
- # Misspelt input names draw nothing, so they fail before anything
- # changes: of the source's flake, and below an input drawn from a source.
- (modes "inputs-typo" ({ draw, ... }: {
- phases = [
- {
- sources = alt // {
- x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- } // draw { innr.follows = "alt"; };
- };
- expect.fail = "error: source x draws input innr from source alt, but its flake has no input innr";
- }
- {
- sources = {
- x.flake = {
- url = "/fixtures/top"; inherit fetchOnTarget;
- } // draw { dep = { follows = "b"; inputs.qq.follows = "c2"; }; };
- b.file = "/fixtures/two";
- c2.file = "/fixtures/c2";
- };
- expect.fail = "error: source x draws input dep/qq from source c2, but the flake of source b has no input qq";
- }
- ];
- }))
- # A signal ending populate leaves neither the ssh master nor the
- # temporary directory.
- (s "interrupt" {
- prepareDeployer = ''
- find "''${TMPDIR:-/tmp}" -maxdepth 1 -name 'populate.*' -exec rm -fR {} +
- '';
- phases = [{
- sources.x.pipe = "/fixtures/pipe-slow";
- interrupt = 3;
- checkDeployer = ''
- for i in 1 2 3 4 5 6 7 8 9 10; do
- if ! pgrep -f 'ssh: .*/populate\..*\[mux\]' >/dev/null; then
- break
- fi
- sleep 0.5
- done
- ! pgrep -f 'ssh: .*/populate\..*\[mux\]' >/dev/null ||
- fail "ssh master remains"
- expect_eq "temporary directories" \
- "$(find "''${TMPDIR:-/tmp}" -maxdepth 1 -name 'populate.*')" ""
- '';
- }];
- })
- # An input of an input drawn from a source that draws inputs itself
- # gets drawn from another source.
- (modes "override" ({ draw, flake, ... }: {
- # Drawing inputs happens on the target alike for every kind of target.
- exhaustive = targetKind != "local";
- phases = twice {
- sources = {
- x.flake = {
- url = "/fixtures/top"; inherit fetchOnTarget;
- } // draw { dep = { follows = "b"; inputs.q.follows = "c2"; }; };
- b.file = { path = "/fixtures/two"; } // draw { p.follows = "c1"; };
- c1.file = "/fixtures/c1";
- c2.file = "/fixtures/c2";
- };
- check = ''
- expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" top:two:c1+c2,sub
- '';
- };
- }))
- # The same one level deeper: the input gets drawn from a source below
- # an input that the source in between draws from a source itself.
- (modes "override-deep" ({ draw, flake, ... }: {
- # Drawing inputs happens on the target alike for every kind of target.
- exhaustive = targetKind != "local";
- phases = twice {
- sources = {
- x.flake = {
- url = "/fixtures/top"; inherit fetchOnTarget;
- } // draw { dep = { follows = "b"; inputs.p.inputs.z.follows = "c2"; }; };
- b.file = { path = "/fixtures/two"; } // draw { p.follows = "c3"; };
- c2.file = "/fixtures/c2";
- c3.file = "/fixtures/c3";
- };
- check = ''
- expect_eq msg "$(nix_eval_online ${flake "x"}#msg)" 'top:two:c3(c2)+inner,sub'
- '';
- };
- }))
- # A source with a rewritten lock file draws an input from a source with
- # a generated flake, and an input below it from another source.
- (s "override-mixed" {
- # Drawing inputs happens on the target alike for every kind of target.
- exhaustive = targetKind != "local";
- phases = twice {
- sources = {
- x.flake = {
- url = "/fixtures/top"; inherit fetchOnTarget;
- inputs.dep = { follows = "b"; inputs.q.follows = "c2"; };
- };
- b.file = {
- path = "/fixtures/two";
- rewriteLock = false;
- inputs.p.follows = "c1";
- };
- c1.file = "/fixtures/c1";
- c2.file = "/fixtures/c2";
- };
- check = ''
- expect_eq msg "$(nix_eval_online "$T"/x#msg)" top:two:c1+c2,sub
- '';
- };
- })
- (modes "nonflake-input" ({ draw, flake, ... }: {
- # Drawing inputs happens on the target alike for every kind of target.
- exhaustive = targetKind != "local";
- phases = twice {
- sources = {
- x.file = { path = "/fixtures/usedata"; } // draw { data.follows = "data"; };
- data.file = "/fixtures/data";
- };
- check = ''
- expect_eq msg "$(nix_eval ${flake "x"}#msg)" data:hello
- '';
- };
- }))
- # The target directory in a Git work tree.
- (modes "gitdir" ({ draw, flake, ... }: {
- prepare = ''
- mkdir -p "$T"
- git -C "$T" init -q
- '';
- phases = twice {
- sources = alt // {
- x.file = { path = "/fixtures/filesrc"; } // draw { dep.follows = "alt"; };
- };
- check = ''
- # Not just the path, which Nix takes for the enclosing work tree.
- expect_eq msg "$(nix_eval_online path:${flake "x"}#msg)" top:alt,sub
- '';
- };
- }))
- # Output of the same length, which rsync's quick check doesn't notice.
- (s "pipe-change" {
- prepareDeployer = ''
- echo piped > /fixtures/pipe2-data
- '';
- phases = [
- {
- sources.x.pipe = "/fixtures/pipe2";
- check = ''
- expect_eq x "$(cat "$T"/x)" piped
- '';
- mutateDeployer = ''
- echo PIPED > /fixtures/pipe2-data
- '';
- }
- {
- sources.x.pipe = "/fixtures/pipe2";
- check = ''
- expect_eq x "$(cat "$T"/x)" PIPED
- '';
- mutateDeployer = ''
- echo piped > /fixtures/pipe2-data
- '';
- }
- ];
- })
- # Source names that aren't input names, or that Nix has another use
- # for, both of sources inputs get drawn from and of sources drawing them.
- (modes "source-name" ({ draw, flake, ... }: let
- x = from: { path = "/fixtures/filesrc"; } // draw { dep.follows = from; };
- in {
- # Drawing inputs happens on the target alike for every kind of target.
- exhaustive = targetKind != "local";
- phases = twice {
- sources = {
- "alt-1.0".file = "/fixtures/alt";
- self.file = "/fixtures/alt";
- x.file = x "alt-1.0";
- y.file = x "self";
- "a.b_c-d".file = x "self";
- };
- check = ''
- expect_eq x "$(nix_eval_online ${flake "x"}#msg)" top:alt,sub
- expect_eq y "$(nix_eval_online ${flake "y"}#msg)" top:alt,sub
- expect_eq a.b_c-d "$(nix_eval_online ${flake "a.b_c-d"}#msg)" top:alt,sub
- '';
- };
- }))
- # An upload an interrupted run left behind.
- (s "upload-leftover" {
- phases = [
- {
- sources.x.flake = { url = "/fixtures/top"; fetchOnTarget = true; };
- check = checkFlake { attr = "msg"; expected = "top:dep+inner,sub"; };
- mutate = ''
- mkdir -p "$T"/.populate/flake-uploads/x/tree
- echo half > "$T"/.populate/flake-uploads/x/tree/half
- '';
- }
- {
- sources.x.flake = { url = "/fixtures/top"; fetchOnTarget = true; };
- check = checkFlake { attr = "msg"; expected = "top:dep+inner,sub"; };
- }
- ];
- })
- ] ++ lib.optional (targetKind == "local")
- # The target has the path the input is locked to.
- (s "uselocal" {
- phases = twice {
- sources.x.flake = { url = "/fixtures/uselocal"; fetchOnTarget = true; };
- check = checkFlake { attr = "msg"; expected = "uselocal:alt"; };
- };
- })
- ) targetKinds;
-
-
- # Populate replaces and removes things in the target directory. These
- # scenarios check that it doesn't anywhere else: names and paths that lead
- # out of the target directory don't evaluate, and symlinks in it get
- # replaced or refused instead of followed. The canaries, which every phase
- # of every scenario checks, tell if something outside changed; these
- # scenarios point links at them.
- safety = concatMap (targetKind: let
- s = name: args: scenario ({
- name = "${name}-${targetKind}";
- inherit targetKind;
- } // args);
- copying = targetKind != "sudo";
- fetchOnTarget = !copying;
- outside = "/var/krops-outside";
- hello = [ "/fixtures/patches/hello.patch" ];
- # Sources that use every kind of state populate keeps: x a copy with
- # patches, y garbage collector roots and, where the flake gets uploaded,
- # an upload, and z a generated flake.
- stateful = alt // {
- x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- patches = hello;
- inputs.inner.follows = "alt";
- };
- y.flake = { url = "/fixtures/top"; inherit fetchOnTarget; };
- z.file = {
- path = "/fixtures/filesrc";
- rewriteLock = false;
- inputs.dep.follows = "alt";
- };
- };
- # A source deployed where a symlink to something outside is. The link
- # gets replaced, never followed.
- link = type: { sources, to, check, says ? true }: let
- phase = {
- inherit sources;
- check = ''
- case $(readlink "$T"/x || :) in
- ${outside}/*) fail "x still links outside" ;;
- esac
- '' + check;
- };
- in s "link-${type}" {
- isolate = true;
- prepare = ''
- mkdir -p "$T"
- ln -s ${outside}/${to} "$T"/x
- '';
- phases = [
- (phase // {
- output = optional says
- "replacing symlink '${targetName targetKind "/var/krops/link-${type}-${targetKind}/x"}', which points to '${outside}/${to}'...";
- })
- phase
- ];
- };
- # Something of populate's state replaced by a symlink to outside between
- # two deploys. The second one refuses to use it.
- state = name: {
- entry, backup ? false,
- message ? "/${entry} is a symlink, but populate needs a directory of its own there",
- }: s "state-${name}" {
- isolate = true;
- phases = [
- {
- sources = stateful;
- inherit backup;
- mutate = ''
- rm -fR "$T"/${entry}
- mkdir -p "$(dirname "$T"/${entry})"
- ln -s ${outside}/dir "$T"/${entry}
- '';
- }
- {
- sources = stateful;
- inherit backup;
- expect.fail = message;
- }
- ];
- };
- in lib.flatten [
- (link "file" {
- sources.x.file = "/fixtures/alt";
- to = "dir";
- check = ''
- test -e "$T"/x/flake.nix || fail "flake.nix missing"
- '';
- })
- # What rsync logs about each item: what changed, and its size.
- (s "rsync-log" {
- phases = [{
- sources.x.file = "/fixtures/alt";
- output = [
- "f+++++++++ flake.nix " "/53"
- # Named like pruning names it.
- "populating '${targetName targetKind "/var/krops/rsync-log-${targetKind}/x"}'..."
- ];
- }];
- })
- # The link points at a Git work tree, which resetting and cleaning would
- # change.
- (link "git" {
- sources.x.git = { url = gitUrl; ref = gitHead; };
- to = "repo";
- check = ''
- expect_eq HEAD "$(git -C "$T"/x rev-parse HEAD)" ${gitHead}
- '';
- })
- (link "pass" {
- sources.x.pass = { dir = "/fixtures/pass"; name = "x"; };
- to = "dir";
- check = ''
- expect_eq a "$(cat "$T"/x/a)" secret-a
- '';
- })
- (link "passage" {
- sources.x.passage = {
- dir = "/fixtures/passage/store/x";
- identities_file = "/fixtures/passage/identities";
- };
- to = "dir";
- check = ''
- expect_eq a "$(cat "$T"/x/a)" secret-a
- '';
- })
- (link "pipe" {
- sources.x.pipe = "/fixtures/pipe";
- to = "file";
- check = ''
- expect_eq x "$(cat "$T"/x)" piped
- '';
- })
- (link "derivation" {
- sources.x.derivation = ''
- derivation {
- name = "krops-test";
- system = builtins.currentSystem;
- builder = "/bin/sh";
- args = [ "-c" "echo built > $out" ];
- }
- '';
- to = "file";
- check = ''
- expect_eq x "$(cat "$T"/x)" built
- '';
- })
- (link "symlink" {
- sources = alt // { x.symlink = "alt"; };
- to = "dir";
- says = false;
- check = ''
- expect_eq link "$(readlink "$T"/x)" alt
- '';
- })
- (link "flake" {
- sources.x.flake = { url = "/fixtures/top"; inherit fetchOnTarget; };
- to = "dir";
- check = checkFlake { attr = "msg"; expected = "top:dep+inner,sub"; };
- })
- (link "flake-materialized" {
- sources.x.flake = {
- url = "/fixtures/top"; inherit fetchOnTarget;
- materialize = true;
- };
- to = "dir";
- check = checkFlake {
- attr = "msg"; expected = "top:dep+inner,sub"; materialized = true;
- };
- })
- (link "flake-input-patched" {
- sources.x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- patches = hello;
- };
- to = "dir";
- check = checkFlake { attr = "greeting"; expected = "hello+inner"; };
- })
- # Symlinks to outside in what a source replaces: rsync and Git remove
- # them, and so does replacing a directory by a link.
- (s "inner-links" (let
- prepare = ''
- for name in a b c; do
- mkdir -p "$T"/$name
- ln -s ${outside}/dir "$T"/$name/keep
- ln -s ${outside}/dir "$T"/$name/stale
- ln -s ${outside}/repo/.git "$T"/$name/.git
- done
- # A work tree whose repository is elsewhere.
- mkdir -p "$T"/d
- echo "gitdir: ${outside}/repo/.git" > "$T"/d/.git
- '';
- in {
- isolate = true;
- inherit prepare;
- phases = twice {
- sources = {
- a.file = "/fixtures/filesrc";
- b.git = { url = gitUrl; ref = gitHead; };
- c.flake = { url = "/fixtures/top"; inherit fetchOnTarget; };
- d.git = { url = gitUrl; ref = gitHead; };
- };
- check = ''
- expect_eq keep "$(cat "$T"/a/keep)" keep
- test ! -L "$T"/a/stale || fail "a/stale remains"
- expect_eq HEAD "$(git -C "$T"/b rev-parse HEAD)" ${gitHead}
- test ! -L "$T"/b/.git || fail "b/.git remains a link"
- test ! -L "$T"/b/stale || fail "b/stale remains"
- expect_eq HEAD "$(git -C "$T"/d rev-parse HEAD)" ${gitHead}
- test -d "$T"/d/.git || fail "d/.git isn't a directory"
- expect_eq "store directory" "$(dirname "$(readlink "$T"/c)")" /nix/store
- '';
- };
- }))
- (state "sentinel" {
- entry = ".populate";
- message = "error: sentinel file is a symlink";
- })
- (state "backup" { entry = ".populate/backup"; backup = true; })
- (map
- (entry: state (lib.replaceStrings ["/"] ["-"] entry) {
- entry = ".populate/${entry}";
- })
- [
- "derived" "flake-inputs" "flake-uploads" "flakes" "patches"
- "derived/x" "patches/x" "flake-inputs/y" "flake-uploads/y" "flakes/z"
- ])
- # Symlinks to outside among what populate removes: leftovers of
- # interrupted runs, state of sources that are gone, and, when pruning,
- # entries of the target directory. They get removed as links. So do
- # hard links to a read-only file outside, whose mode stays.
- (s "leftovers" {
- isolate = true;
- prepare = ''
- mkdir -p "$T"/hard
- ln -s ${outside}/dir "$T"/other
- ln -s ${outside}/repo "$T"/repo
- ln ${outside}/dir/sub/file "$T"/hard/link
- ln ${outside}/dir/sub/file "$T"/hardlink
- '';
- phases = [
- {
- sources = stateful;
- mutate = ''
- ln -s ${outside}/dir "$T"/.populate/derived/x/work
- ln -s ${outside}/dir "$T"/.populate/derived/gone
- ln -s ${outside}/dir "$T"/.populate/flakes/gone
- ln -s ${outside}/dir "$T"/.populate/patches/gone
- ln -s ${outside}/dir "$T"/.populate/flake-inputs/gone
- mkdir -p "$T"/.populate/flake-uploads
- ln -s ${outside}/dir "$T"/.populate/flake-uploads/gone
- '';
- }
- {
- sources = stateful;
- check = ''
- for name in derived/x/work derived/gone flakes/gone patches/gone; do
- test ! -L "$T"/.populate/$name || fail "$name remains"
- done
- for name in other repo .populate/flake-inputs/gone .populate/flake-uploads/gone; do
- test -L "$T"/$name || fail "$name removed without pruning"
- done
- '';
- }
- {
- sources = stateful;
- prune = true;
- check = ''
- for name in other repo hard hardlink .populate/flake-inputs/gone .populate/flake-uploads; do
- test ! -L "$T"/$name && test ! -e "$T"/$name || fail "$name not pruned"
- done
- '';
- }
- ];
- })
- # A target path with characters that mean something to shells and in
- # URLs.
- (s "odd-target" (let
- targetPath = "/var/krops/odd-target-${targetKind}/a b*c?d#e%f";
- in {
- isolate = true;
- phases = twice {
- sources = stateful // {
- g.git = { url = gitUrl; ref = gitHead; };
- p.pipe = "/fixtures/pipe";
- };
- inherit targetPath;
- prune = true;
- check = ''
- D=${lib.escapeShellArg targetPath}
- test -e "$D"/alt/flake.nix || fail "alt missing"
- expect_eq HEAD "$(git -C "$D"/g rev-parse HEAD)" ${gitHead}
- expect_eq p "$(cat "$D"/p)" piped
- expect_eq entries "$(ls "$T")" 'a b*c?d#e%f'
- gc
- expect_eq greeting "$(nix_eval "$(readlink -f "$D"/x)"#greeting)" hello+alt
- expect_eq msg "$(nix_eval "$(readlink -f "$D"/y)"#msg)" top:dep+inner,sub
- '';
- };
- }))
- # The target directory itself may be a symlink: what it points to is the
- # target directory then.
- (s "linked-target" {
- isolate = true;
- prepare = ''
- rm -fR "$T"-real
- mkdir "$T"-real
- ln -s "$(basename "$T")"-real "$T"
- '';
- phases = twice {
- sources = stateful;
- check = ''
- test -L "$T" || fail "the target directory isn't a link anymore"
- test -e "$T"-real/alt/flake.nix || fail "alt missing"
- gc
- expect_eq greeting "$(nix_eval "$(readlink -f "$T"/x)"#greeting)" hello+alt
- expect_eq msg "$(nix_eval "$(readlink -f "$T"/y)"#msg)" top:dep+inner,sub
- expect_eq msg "$(nix_eval_online "path:$(readlink -f "$T"/.populate/flakes/z)#msg")" top:alt,sub
- '';
- };
- })
- # Git work trees whose .git directory names places elsewhere, or a
- # command: a work tree elsewhere, a configuration file elsewhere, another
- # repository, and a command run when Git checks for changes. Populate
- # refuses them, telling why, and replaces them by fresh clones with
- # reclone. A hook doesn't run.
- (s "git-foreign" (let
- elsewhere = "/var/krops-elsewhere";
- git = name: reclone: {
- ${name}.git = { url = gitUrl; ref = gitHead; inherit reclone; };
- };
- untouched = ''
- cmp -s ${elsewhere}/config ${elsewhere}/config.orig ||
- fail "what b/.git/config links to changed"
- test ! -e ${elsewhere}/ran || fail "a command in a .git directory ran"
- '';
- refused = name: message: {
- sources = git name false;
- expect.fail = "error: populate doesn't use the Git work tree ${targetName targetKind "/var/krops/git-foreign-${targetKind}/${name}"}, because ${message}. Fix that, remove the work tree, or set the source's reclone to true to replace it by a fresh clone.";
- };
- in {
- isolate = true;
- prepare = ''
- rm -fR ${elsewhere}
- mkdir -p ${elsewhere} "$T"
- for name in a b c d e; do
- git clone -q ${gitUrl} "$T"/$name
- git -C "$T"/$name checkout -q ${gitFirst}
- done
- git -C "$T"/a config core.worktree ${outside}/dir
- mv "$T"/b/.git/config ${elsewhere}/config
- ln -s ${elsewhere}/config "$T"/b/.git/config
- cp -p ${elsewhere}/config ${elsewhere}/config.orig
- printf '#! /bin/sh\ntouch ${elsewhere}/ran\n' > "$T"/c/.git/hooks/reference-transaction
- chmod +x "$T"/c/.git/hooks/reference-transaction
- echo ${outside}/repo/.git > "$T"/d/.git/commondir
- git -C "$T"/e config core.fsmonitor "touch ${elsewhere}/ran"
- '';
- phases = [
- (refused "a" "/var/krops/git-foreign-${targetKind}/a/.git/config sets core.worktree to ${outside}/dir")
- (refused "b" "/var/krops/git-foreign-${targetKind}/b/.git contains symlinks: /var/krops/git-foreign-${targetKind}/b/.git/config")
- {
- sources = git "c" false;
- check = untouched + ''
- expect_eq "c HEAD" "$(git -C "$T"/c rev-parse HEAD)" ${gitHead}
- '';
- }
- (refused "d" "/var/krops/git-foreign-${targetKind}/d/.git/commondir names another repository")
- (refused "e" "/var/krops/git-foreign-${targetKind}/e/.git/config sets core.fsmonitor, which neither git clone nor populate does")
- {
- sources = git "a" true // git "b" true // git "c" true // git "d" true // git "e" true;
- output = map
- (name: "replacing the Git work tree '${targetName targetKind "/var/krops/git-foreign-${targetKind}/${name}"}', because")
- [ "a" "b" "d" "e" ];
- check = untouched + ''
- for name in a b c d e; do
- expect_eq "$name HEAD" "$(git -C "$T"/$name rev-parse HEAD)" ${gitHead}
- test -e "$T"/$name/two || fail "$name/two missing"
- done
- '';
- }
- ];
- }))
- # A Git work tree whose submodule's .git, which Git uses as it finds it,
- # isn't the gitfile git clone made, naming the repository in
- # .git/modules: a repository of its own, whose configuration names a
- # command, and a gitfile naming a repository elsewhere.
- (s "git-submodule" (let
- elsewhere = "/var/krops-elsewhere";
- url = dirOf gitUrl + "/withsub.git";
- git = reclone: { x.git = { inherit url reclone; ref = "master"; }; };
- untouched = ''
- test ! -e ${elsewhere}/ran || fail "a command in a .git directory ran"
- expect_eq "HEAD elsewhere" "$(git -C ${elsewhere}/repo rev-parse HEAD)" ${gitFirst}
- '';
- in {
- isolate = true;
- prepare = ''
- rm -fR ${elsewhere}
- git clone -q ${gitUrl} ${elsewhere}/repo
- git -C ${elsewhere}/repo checkout -q ${gitFirst}
- '';
- phases = [
- {
- sources = git false;
- check = untouched + ''
- expect_eq "sub HEAD" "$(git -C "$T"/x/sub rev-parse HEAD)" ${gitHead}
- '';
- mutate = ''
- rm "$T"/x/sub/.git
- cp -R "$T"/x/.git/modules/sub "$T"/x/sub/.git
- git config --file "$T"/x/sub/.git/config --unset core.worktree
- git config --file "$T"/x/sub/.git/config \
- filter.x.smudge "touch ${elsewhere}/ran; cat"
- echo '* filter=x' > "$T"/x/sub/.git/info/attributes
- git -C "$T"/x/sub update-ref --no-deref HEAD ${gitFirst}
- git -C "$T"/x/sub read-tree ${gitFirst}
- rm "$T"/x/sub/two
- '';
- }
- {
- sources = git false;
- expect.fail = "error: populate doesn't use the Git work tree";
- check = untouched;
- mutate = ''
- rm -fR "$T"/x/sub
- mkdir "$T"/x/sub
- echo gitdir: ${elsewhere}/repo/.git > "$T"/x/sub/.git
- '';
- }
- {
- sources = git false;
- expect.fail = "error: populate doesn't use the Git work tree";
- check = untouched;
- }
- {
- sources = git true;
- check = untouched + ''
- expect_eq "sub HEAD" "$(git -C "$T"/x/sub rev-parse HEAD)" ${gitHead}
- '';
- }
- ];
- }))
- # A ref that git fetch would take for an option: --upload-pack runs a
- # command where the repository is local.
- (s "git-ref-option" (let
- elsewhere = "/var/krops-elsewhere";
- in {
- isolate = true;
- prepare = ''
- rm -fR ${elsewhere}
- git clone -q --bare ${gitUrl} ${elsewhere}/repo.git
- '';
- phases = [
- {
- sources.x.git = {
- url = "${elsewhere}/repo.git";
- ref = "--upload-pack=touch ${elsewhere}/ran; git-upload-pack";
- };
- expect.fail = "invalid refspec";
- check = ''
- test ! -e ${elsewhere}/ran || fail "the ref ran a command"
- '';
- }
- ];
- }))
- # populate exits with the status of the command that failed, here git
- # fetch's.
- (s "git-status" {
- phases = [{
- sources.x.git = { url = gitUrl; ref = "nonexistent"; };
- expect = { fail = "couldn't find remote ref"; status = 128; };
- }];
- })
- # A .git that a materialized flake got, and one that a file source
- # keeps, which Nix uses for the git+file: URL of the flake there: writing
- # its lock file, Nix runs Git, with that repository's configuration.
- (s "lock-foreign-git" (let
- elsewhere = "/var/krops-elsewhere";
- sources = alt // {
- x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- materialize = true;
- inputs.inner.follows = "alt";
- };
- y.file = {
- path = "/fixtures/filesrc";
- deleteExcluded = false;
- filters = [ { type = "exclude"; pattern = "/.git"; } ];
- inputs.dep.follows = "alt";
- };
- };
- in {
- isolate = true;
- prepare = ''
- rm -fR ${elsewhere}
- mkdir -p ${elsewhere}
- '';
- phases = [
- {
- inherit sources;
- # Repositories tracking only flake.nix, so the lock file changes.
- mutate = ''
- for name in x y; do
- git -C "$T"/$name init -q
- git -C "$T"/$name add flake.nix
- git -C "$T"/$name -c user.name=x -c user.email=x@x commit -q -m x
- git -C "$T"/$name config core.fsmonitor "touch ${elsewhere}/ran; false"
- done
- '';
- }
- {
- inherit sources;
- check = ''
- test ! -e ${elsewhere}/ran || fail "a command in a .git directory ran"
- '';
- }
- ];
- }))
- # A .git that a file source keeps, with configuration a git source would
- # use, but a hook, which Nix's Git runs when it adds the lock file it
- # writes: populate's Git runs none, so git sources keep hooks, but
- # drawing inputs refers to the flake as path:.
- (s "lock-hooks" (let
- elsewhere = "/var/krops-elsewhere";
- sources = alt // {
- y.file = {
- path = "/fixtures/filesrc";
- deleteExcluded = false;
- filters = [ { type = "exclude"; pattern = "/.git"; } ];
- inputs.dep.follows = "alt";
- };
- };
- in {
- isolate = true;
- prepare = ''
- rm -fR ${elsewhere}
- mkdir -p ${elsewhere}
- '';
- phases = [
- {
- inherit sources;
- # A repository tracking only flake.nix, so Nix adds the lock file,
- # which runs post-index-change.
- mutate = ''
- git -C "$T"/y init -q
- git -C "$T"/y add flake.nix
- git -C "$T"/y -c user.name=x -c user.email=x@x commit -q -m x
- printf '#! /bin/sh\ntouch ${elsewhere}/ran\n' > "$T"/y/.git/hooks/post-index-change
- chmod +x "$T"/y/.git/hooks/post-index-change
- '';
- }
- {
- inherit sources;
- check = ''
- test ! -e ${elsewhere}/ran || fail "a hook in a .git directory ran"
- test -e "$T"/y/flake.lock || fail "y/flake.lock missing"
- '';
- }
- ];
- }))
- # A file in populate's state replaced by a symlink to a file outside.
- (s "state-key" (let
- sources.x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- patches = hello;
- };
- in {
- isolate = true;
- phases = [
- {
- inherit sources;
- mutate = ''
- rm "$T"/.populate/derived/x/key
- ln -s ${outside}/file "$T"/.populate/derived/x/key
- '';
- }
- {
- inherit sources;
- check = checkFlake { attr = "greeting"; expected = "hello+inner"; };
- }
- ];
- }))
- # Stale state that contains a hard link to a read-only file outside,
- # whose mode stays.
- (s "state-hard-link" (let
- sources.x.flake-input = {
- url = "/fixtures/top"; name = "dep"; inherit fetchOnTarget;
- patches = hello;
- };
- in {
- isolate = true;
- phases = [
- {
- inherit sources;
- mutate = ''
- mkdir -p "$T"/.populate/derived/gone "$T"/.populate/derived/x/work
- ln ${outside}/dir/sub/file "$T"/.populate/derived/gone/file
- ln ${outside}/dir/sub/file "$T"/.populate/derived/x/work/file
- '';
- }
- {
- inherit sources;
- check = ''
- test ! -e "$T"/.populate/derived/gone || fail "gone remains"
- '';
- }
- ];
- }))
- # The source contains flake.lock.populate and a file like the temporary
- # copy of the lock file, symlinks to a file outside.
- (s "lock-temporary-link" {
- isolate = true;
- prepareDeployer = ''
- rm -fR /var/krops-src/lock-temporary-link
- mkdir -p /var/krops-src
- cp -R /fixtures/filesrc /var/krops-src/lock-temporary-link
- ln -s ${outside}/file /var/krops-src/lock-temporary-link/flake.lock.populate
- ln -s ${outside}/file /var/krops-src/lock-temporary-link/.flake.lock.XXXXXX
- '';
- phases = twice {
- sources = alt // {
- x.file = {
- path = "/var/krops-src/lock-temporary-link";
- inputs.dep.follows = "alt";
- };
- };
- check = ''
- expect_eq msg "$(nix_eval_online path:"$T"/x#msg)" top:alt,sub
- '';
- };
- })
- # A file in the target directory is a hard link to a read-only file
- # outside with the same contents, which rsync takes for unchanged, and
- # would fix the mode and time of in place.
- (s "rsync-hard-link" {
- isolate = true;
- prepareDeployer = ''
- rm -fR /var/krops-src/rsync-hard-link
- mkdir -p /var/krops-src/rsync-hard-link
- echo outside > /var/krops-src/rsync-hard-link/file
- '';
- prepare = ''
- mkdir -p "$T"/x
- ln ${outside}/dir/sub/file "$T"/x/file
- '';
- phases = twice {
- sources.x.file = {
- path = "/var/krops-src/rsync-hard-link";
- useChecksum = true;
- };
- check = ''
- expect_eq file "$(cat "$T"/x/file)" outside
- expect_eq links "$(stat -c %h "$T"/x/file)" 1
- '';
- };
- })
- (lib.optionals (targetKind == "local") [
- # Rewriting a lock file changes no other hard link to it. The lock
- # file is like the source's, with its modification time, so rsync
- # leaves the link in place. (The source is on the deploying machine,
- # so this only runs there.)
- (s "lock-hard-link" {
- prepare = ''
- mkdir -p "$T"
- cp -a /fixtures/filesrc "$T"/x
- cp -p "$T"/x/flake.lock "$T".lock
- ln -f "$T".lock "$T"/x/flake.lock
- '';
- phases = twice {
- sources = alt // {
- x.file = { path = "/fixtures/filesrc"; inputs.dep.follows = "alt"; };
- };
- check = ''
- cmp -s "$T".lock /fixtures/filesrc/flake.lock ||
- fail "the other hard link changed"
- expect_eq msg "$(nix_eval_online "$T"/x#msg)" top:alt,sub
- '';
- };
- })
- # Names that lead out of the target directory, to the directory itself,
- # or to populate's state.
- (s "names" {
- phases = map
- (name: {
- sources.${name}.file = "/fixtures/alt";
- expect = "eval-error";
- control = { ok.file = "/fixtures/alt"; };
- })
- [ "" "." ".." ".populate" "a/b" "../x" "/etc" "a\nb" "a b" "a*b" "a?b" "a+b" "a=b" "-a" ];
- })
- # Target paths that aren't absolute, that are the root directory, or
- # that resolve to another directory than they name.
- (s "paths" {
- phases = map
- (targetPath: {
- sources = alt;
- inherit targetPath;
- expect = "eval-error";
- controlTargetPath = "/var/krops/paths-local";
- })
- [ "" "/" "//" "/." "relative" "/var/krops/.." "/var/krops/../.." "/var/krops/a\nb" ];
- })
- ])
- ]) targetKinds;
-
}
diff --git a/tests/vm.nix b/tests/vm.nix
deleted file mode 100644
index 8a08819..0000000
--- a/tests/vm.nix
+++ /dev/null
@@ -1,204 +0,0 @@
-# Runs scenarios in a NixOS VM test with two machines: deployer, which runs
-# populate, and target, which it deploys to via ssh. Local scenarios deploy
-# to deployer itself.
-{ pkgs, name, scenarios, gitHead, rsync ? pkgs.rsync }:
-
-let
- inherit (pkgs) lib;
-
- sshKeys = import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs;
-
- script = name: text: if text == null then null else pkgs.writeText name text;
-
- # The scenarios as JSON, with scripts as files.
- plan = pkgs.writeText "krops-test-${name}.json" (builtins.toJSON (map
- (scenario: scenario // {
- prepare = pkgs.writeText "prepare" scenario.prepare;
- canarySetup = pkgs.writeText "canary-setup" scenario.canarySetup;
- canaryCheck = pkgs.writeText "canary-check" scenario.canaryCheck;
- prepareDeployer = script "prepare-deployer" scenario.prepareDeployer;
- phases = map
- (phase: phase // {
- check = pkgs.writeText "check" phase.check;
- mutate = script "mutate" phase.mutate;
- checkDeployer = script "check-deployer" phase.checkDeployer;
- mutateDeployer = script "mutate-deployer" phase.mutateDeployer;
- })
- scenario.phases;
- })
- scenarios));
-
- common = { pkgs, ... }: {
- # The plan refers to all scripts the scenarios run. Being part of the
- # system, they survive the scenarios collecting garbage.
- system.extraDependencies = [ plan "${./fixtures.sh}" ];
- nix.settings.substituters = lib.mkForce [];
- environment.systemPackages = [
- pkgs.git
- rsync
- (pkgs.writeScriptBin "nixos-rebuild" (builtins.readFile ./nixos-rebuild.sh))
- ];
- # The stand-in replaces nixos-rebuild.
- system.disableInstallerTools = true;
- # Only warnings and errors on the console, which ends up in the log.
- services.journald.extraConfig = lib.mkAfter "MaxLevelConsole=warning";
- boot.initrd.systemd.contents."/etc/systemd/journald.conf".text =
- lib.mkAfter "MaxLevelConsole=warning";
- boot.consoleLogLevel = lib.mkForce 4;
- # Neither probing BIOS disk information, nor iPXE option ROMs, which
- # network boot would need, nor systemd-oomd, which warns about missing
- # swap.
- boot.kernelParams = lib.optional pkgs.stdenv.hostPlatform.isx86 "edd=off";
- virtualisation.qemu.options = [ "-global virtio-net-pci.romfile=" ];
- systemd.oomd.enable = false;
- virtualisation.memorySize = 2048;
- virtualisation.diskSize = 4096;
- };
-
-in pkgs.testers.runNixOSTest {
- name = "krops-${name}";
-
- globalTimeout = 4 * 60 * 60;
-
- nodes.deployer = { pkgs, ... }: {
- imports = [ common ];
- environment.systemPackages = [
- pkgs.age pkgs.gnupg pkgs.pass pkgs.passage
- ];
- networking.firewall.allowedTCPPorts = [ 8000 9418 ];
- # Resolving indirect references fails if the global registry can't be
- # downloaded, which it can't in the VM. Disabling it needs flakes, and a
- # deploying machine using indirect references has them anyway. The
- # target doesn't, so its checks for flake support still mean something.
- nix.settings.experimental-features = [ "nix-command" "flakes" ];
- nix.settings.flake-registry = "";
- programs.ssh.extraConfig = ''
- StrictHostKeyChecking no
- UserKnownHostsFile /dev/null
- LogLevel ERROR
- '';
- systemd.services.fixtures-http = {
- wantedBy = [ "multi-user.target" ];
- preStart = "mkdir -p /fixtures/www";
- serviceConfig.ExecStart =
- "${pkgs.python3}/bin/python3 -m http.server 8000 --directory /fixtures/www";
- };
- systemd.services.fixtures-git = {
- wantedBy = [ "multi-user.target" ];
- preStart = "mkdir -p /fixtures/git";
- serviceConfig.ExecStart =
- "${pkgs.git}/bin/git daemon --reuseaddr --export-all --base-path=/fixtures/git";
- };
- };
-
- nodes.target = { ... }: {
- imports = [ common ];
- services.openssh.enable = true;
- users.users.root.openssh.authorizedKeys.keys = [ sshKeys.snakeOilPublicKey ];
- users.users.deploy = {
- isNormalUser = true;
- extraGroups = [ "wheel" ];
- openssh.authorizedKeys.keys = [ sshKeys.snakeOilPublicKey ];
- };
- security.sudo.wheelNeedsPassword = false;
- };
-
- testScript = /* python */ ''
- import json
-
- plan = json.load(open("${plan}"))
- nodes = {"deployer": deployer, "target": target}
-
-
- # Runs a command, logging its output only if it fails.
- def run(node, command):
- status, out = node.execute(f"{command} 2>&1")
- if status != 0:
- raise Exception(f"{command} fails:\n{out}")
-
-
- start_all()
- deployer.wait_for_unit("fixtures-http.service")
- deployer.wait_for_unit("fixtures-git.service")
- deployer.wait_for_open_port(8000)
- deployer.wait_for_open_port(9418)
- target.wait_for_unit("sshd.service")
- target.wait_for_open_port(22)
-
- with subtest("setup"):
- run(deployer, "install -Dm600 ${sshKeys.snakeOilPrivateKey} /root/.ssh/id_ecdsa")
- run(deployer, "ssh root@target true")
- run(deployer, "ssh deploy@target sudo true")
- run(deployer, "HOME=/root sh ${./fixtures.sh} http://deployer:8000 ${gitHead}")
-
- failed = []
- # The canaries get created at the start, after isolating, which removes
- # some, and after a failure, which may have changed some.
- canaries = False
- for scenario in plan:
- node = nodes[scenario["node"]]
- try:
- with subtest(scenario["name"]):
- if scenario["isolate"]:
- for machine in nodes.values():
- run(machine, "rm -fR /var/krops && nix-store --gc >/dev/null")
- if scenario["isolate"] or not canaries:
- for machine in nodes.values():
- run(machine, f"sh {scenario['canarySetup']}")
- canaries = True
- if scenario["prepareDeployer"] is not None:
- run(deployer, f"sh {scenario['prepareDeployer']}")
- run(node, f"sh {scenario['prepare']}")
- for i, phase in enumerate(scenario["phases"]):
- # A hang fails the scenario, not the whole test. The
- # input is the console the test driver talks through,
- # a terminal, which ssh -t would take over.
- limit = phase["interrupt"] or 600
- status, out = deployer.execute(
- f"env HOME=/root {phase['env']} timeout {limit} {phase['populate']} </dev/null 2>&1",
- timeout=900,
- )
- for unexpected in phase["unexpected"]:
- if unexpected in out:
- raise Exception(
- f"populate {i} prints {unexpected!r}:\n{out}"
- )
- if phase["interrupt"] is not None:
- if status != 124:
- raise Exception(f"populate {i} ends on its own:\n{out}")
- elif phase["expect"] is None:
- if status != 0:
- raise Exception(f"populate {i} fails:\n{out}")
- for expected in phase["output"]:
- if expected not in out:
- raise Exception(
- f"populate {i} doesn't print {expected!r}:\n{out}"
- )
- elif status == 0 or phase["expect"] not in out:
- raise Exception(
- f"populate {i} doesn't fail with {phase['expect']!r}:\n{out}"
- )
- elif phase["status"] is not None and status != phase["status"]:
- raise Exception(
- f"populate {i} exits with {status} instead of {phase['status']}:\n{out}"
- )
- run(node, f"KROPS_TEST_GC=1 sh {phase['check']}")
- if phase["checkDeployer"] is not None:
- run(deployer, f"sh {phase['checkDeployer']}")
- if phase["mutate"] is not None:
- run(node, f"sh {phase['mutate']}")
- if phase["mutateDeployer"] is not None:
- run(deployer, f"sh {phase['mutateDeployer']}")
- # Each phase's check covers the canaries of its machine.
- for machine in nodes.values():
- if machine is not node:
- run(machine, f"sh {scenario['canaryCheck']}")
- except Exception as e:
- print(f"scenario {scenario['name']} fails: {e}")
- failed.append(scenario["name"])
- canaries = False
-
- if failed:
- raise Exception(f"{len(failed)} scenarios fail: {' '.join(failed)}")
- '';
-}