aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/populate/tests/builder.nix
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/populate/tests/builder.nix')
-rw-r--r--pkgs/populate/tests/builder.nix249
1 files changed, 249 insertions, 0 deletions
diff --git a/pkgs/populate/tests/builder.nix b/pkgs/populate/tests/builder.nix
new file mode 100644
index 0000000..26a360b
--- /dev/null
+++ b/pkgs/populate/tests/builder.nix
@@ -0,0 +1,249 @@
+# The scenario builder of populate's tests, which krops' tests use as well.
+# A scenario prepares the target directory, then runs its phases. Each phase
+# runs populate, or something running it, expecting it to succeed, to fail
+# with a message, or to refuse its arguments, and checks the result on the
+# target. Refusals checked during evaluation get checked here and don't run.
+{
+ # nixpkgs' lib.
+ lib,
+ # evalSource of populate's Nix library.
+ evalSource,
+ # Function from a phase's arguments, { source, backup, force, prune,
+ # deploy, targetKind, targetPath }, to the command running the phase.
+ # source is what evalSource returns; deploy is null, or arguments for
+ # running populate in a deployment.
+ run,
+ # Whether the command checks populate's arguments during evaluation, so
+ # phases expecting a refusal expect evaluation to fail, instead of
+ # populate refusing when it runs.
+ checksDuringEvaluation,
+ # 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);
+
+ 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}";
+ vars = "T=${path}\n";
+ # A phase expecting evaluation to fail, if checksDuringEvaluation, or else
+ # populate to refuse with the message refused, 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", refused ? null, control ? null, controlTargetPath ? null,
+ check ? "", mutate ? null, checkDeployer ? null, mutateDeployer ? null,
+ }: let
+ command = sources: targetPath: run {
+ source = evalSource [ sources ];
+ inherit backup prune deploy targetKind targetPath;
+ force = true;
+ };
+ evaluates = sources: targetPath:
+ (builtins.tryEval
+ (builtins.seq (toString (command sources targetPath)) true)).success;
+ script = text: if text == null then null else prelude + vars + text;
+ # Without checks during evaluation, populate refuses when it runs.
+ expect' =
+ if expect == "eval-error" && !checksDuringEvaluation then
+ if refused == null then
+ throw "scenario ${name}: expecting a refusal needs refused, what populate says"
+ else { fail = refused; }
+ else expect;
+ 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;
+ # Whether a phase expects a refusal, which krops' tests check during
+ # evaluation.
+ refusals = lib.any (phase: phase.expect or null == "eval-error") phases;
+ 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);
+ };
+
+ # 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";
+ refused = "copying flakes to targets with sudo is not supported";
+ control = withFetchOnTarget;
+ };
+
+in {
+ inherit lib url gitUrl remoteHost agePath gitHead gitFirst rsyncVersion;
+ inherit (lib)
+ attrNames concatMap concatStringsSep foldl' isBool mapAttrsToList
+ optional optionalAttrs optionals optionalString;
+ inherit
+ prelude cartesian flagsName alt targetName
+ inputModes drawInputs flakeOf scenario twice
+ targetKinds matrix checkFlake copyingWithSudo withFetchOnTarget
+ refuseCopyingWithSudo;
+}