diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/default.nix | 182 | ||||
| -rwxr-xr-x | tests/fixtures.sh | 498 | ||||
| -rw-r--r-- | tests/lib.sh | 118 | ||||
| -rw-r--r-- | tests/rsync.nix | 50 | ||||
| -rw-r--r-- | tests/scenarios.nix | 2407 | ||||
| -rw-r--r-- | tests/vm.nix | 204 |
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)}") - ''; -} |
