diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | pkgs/populate/default.nix | 67 | ||||
| -rw-r--r-- | pkgs/populate/flake-jq.nix | 70 | ||||
| -rw-r--r-- | tests/default.nix | 13 | ||||
| -rwxr-xr-x | tests/jq.sh | 122 |
5 files changed, 206 insertions, 67 deletions
@@ -592,6 +592,7 @@ The tests require KVM and nixpkgs with `pkgs.testers.runNixOSTest` ```sh nix-build tests # all tests nix-build tests -A flake # the test for one source type +nix-build tests -A jq # populate's jq programs for flakes, without VMs ``` diff --git a/pkgs/populate/default.nix b/pkgs/populate/default.nix index bd7e5e4..f3cf925 100644 --- a/pkgs/populate/default.nix +++ b/pkgs/populate/default.nix @@ -259,6 +259,7 @@ let pop.flake = target: source: pop.flake-input target (source // { name = null; }); pop.flake-input = target: source: let + inherit (import ./flake-jq.nix) selected selfIsRemote selfLocked; nix' = "${nix}/bin/nix --extra-experimental-features ${quote "nix-command flakes"}"; inputPath = if source.name == null then [] else splitString "/" source.name; fetchOnTarget = source.fetchOnTarget or false; @@ -291,72 +292,6 @@ let ${quote source.flake} ${quote (toString source.name)} exit 1 ''; - # jq definitions for subdirectories containing flakes, which flake - # references and lock entries name either as attribute dir or as query - # parameter dir of their URL, depending on the version of Nix. fetchTree - # supports neither. - subdirs = /* jq */ '' - def subdir: - .dir // ((.url // "") | capture("[?&]dir=(?<dir>[^&]*)").dir) // ""; - def withoutSubdir: - del(.__final, .dir) - | if (.url | type) == "string" then - .url |= (sub("(?<sep>[?&])dir=[^&]*&?"; "\(.sep)") | sub("[?&]$"; "")) - else . end; - def subdirOf($node): - [$node.locked, $node.original] - | map(select(. != null) | subdir | select(. != "")) - | first // ""; - ''; - # jq filter selecting the flake or input in nix flake metadata's output, - # by the path of input names $input, as what to fetch and the - # subdirectory containing it: {self, locked, subdir}. Names follow the - # flake's lock file, follows included; an empty follows path removes an - # input. The flake itself, and inputs given by paths relative to it, have - # self set instead of fetchTree arguments in locked. - selected = /* jq */ '' - ${subdirs} - . as $m - | .locks as $l - | def ref($path; $depth): - if $depth > 64 then error("follows nest too deep") else - reduce $path[] as $name ($l.root; - (($l.nodes[.].inputs // {})[$name]) as $next - | if $next == null or $next == [] then error("no input \($name)") - elif ($next | type) == "array" then ref($next; $depth + 1) - else $next end) - end; - def join($a; $b): - if $a == "" then $b elif $b == "" then $a else "\($a)/\($b)" end; - def resolve($key; $depth): - $l.nodes[$key] as $node - | if $depth > 64 then error("parents nest too deep") - elif $key == $l.root then - { self: true, locked: null, subdir: subdirOf($m) } - elif $node.parent != null then - resolve(ref($node.parent; 0); $depth + 1) as $base - | $base + { - subdir: join(join($base.subdir; - $node.locked.path | ltrimstr("./") | rtrimstr("/") | if . == "." then "" else . end); - subdirOf($node)) - } - else - { self: false, locked: ($node.locked | withoutSubdir), subdir: subdirOf($node) } - end; - resolve(ref($input; 0); 0) - ''; - # jq filter telling whether the target can fetch the flake itself, i.e. - # whether its locked reference doesn't refer to the deploying machine. - selfIsRemote = /* jq */ '' - .locked | (.type != "path") and ((.url // "") | startswith("file:") | not) - ''; - # jq filter replacing self in the selection $selected with the flake's - # locked reference. - selfLocked = /* jq */ '' - ${subdirs} - .locked as $self - | $selected + { self: false, locked: ($self | withoutSubdir) } - ''; # Shell function printing the store path $1, followed by the inputs its # flake in the subdirectory $2 pins in its own lock file, using the nix # command nix. Without a lock file, locking would change the flake, so diff --git a/pkgs/populate/flake-jq.nix b/pkgs/populate/flake-jq.nix new file mode 100644 index 0000000..41edcc5 --- /dev/null +++ b/pkgs/populate/flake-jq.nix @@ -0,0 +1,70 @@ +# jq programs populate uses for flake and flake-input sources, in a file of +# their own, so tests can run them. +rec { + # jq definitions for subdirectories containing flakes, which flake + # references and lock entries name either as attribute dir or as query + # parameter dir of their URL, depending on the version of Nix. fetchTree + # supports neither. + subdirs = /* jq */ '' + def subdir: + .dir // ((.url // "") | capture("[?&]dir=(?<dir>[^&]*)").dir) // ""; + def withoutSubdir: + del(.__final, .dir) + | if (.url | type) == "string" then + .url |= (sub("(?<sep>[?&])dir=[^&]*&?"; "\(.sep)") | sub("[?&]$"; "")) + else . end; + def subdirOf($node): + [$node.locked, $node.original] + | map(select(. != null) | subdir | select(. != "")) + | first // ""; + ''; + # jq filter selecting the flake or input in nix flake metadata's output, + # by the path of input names $input, as what to fetch and the + # subdirectory containing it: {self, locked, subdir}. Names follow the + # flake's lock file, follows included; an empty follows path removes an + # input. The flake itself, and inputs given by paths relative to it, have + # self set instead of fetchTree arguments in locked. + selected = /* jq */ '' + ${subdirs} + . as $m + | .locks as $l + | def ref($path; $depth): + if $depth > 64 then error("follows nest too deep") else + reduce $path[] as $name ($l.root; + (($l.nodes[.].inputs // {})[$name]) as $next + | if $next == null or $next == [] then error("no input \($name)") + elif ($next | type) == "array" then ref($next; $depth + 1) + else $next end) + end; + def join($a; $b): + if $a == "" then $b elif $b == "" then $a else "\($a)/\($b)" end; + def resolve($key; $depth): + $l.nodes[$key] as $node + | if $depth > 64 then error("parents nest too deep") + elif $key == $l.root then + { self: true, locked: null, subdir: subdirOf($m) } + elif $node.parent != null then + resolve(ref($node.parent; 0); $depth + 1) as $base + | $base + { + subdir: join(join($base.subdir; + $node.locked.path | ltrimstr("./") | rtrimstr("/") | if . == "." then "" else . end); + subdirOf($node)) + } + else + { self: false, locked: ($node.locked | withoutSubdir), subdir: subdirOf($node) } + end; + resolve(ref($input; 0); 0) + ''; + # jq filter telling whether the target can fetch the flake itself, i.e. + # whether its locked reference doesn't refer to the deploying machine. + selfIsRemote = /* jq */ '' + .locked | (.type != "path") and ((.url // "") | startswith("file:") | not) + ''; + # jq filter replacing self in the selection $selected with the flake's + # locked reference. + selfLocked = /* jq */ '' + ${subdirs} + .locked as $self + | $selected + { self: false, locked: ($self | withoutSubdir) } + ''; +} diff --git a/tests/default.nix b/tests/default.nix index ca409b3..a6b9356 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -1,7 +1,8 @@ # NixOS VM tests for populate: one test per source type, one for features # spanning sources, one for edge cases of flakes, 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. +# e.g., nix-build tests -A flake. jq runs populate's jq programs for flakes +# without VMs. { pkgs ? import ../pkgs {} }: let @@ -21,7 +22,17 @@ let agePath = "${pkgs.age}/bin/age"; inherit gitHead gitFirst; }; + flakeJq = import ../pkgs/populate/flake-jq.nix; in builtins.mapAttrs (name: scenarios: import ./vm.nix { inherit pkgs name scenarios gitHead; }) scenarios + // { + # Lock files in the shapes different versions of Nix write. + jq = pkgs.runCommand "krops-jq" { nativeBuildInputs = [ pkgs.jq ]; } '' + sh ${./jq.sh} ${pkgs.linkFarm "krops-flake-jq" (map + (name: { name = "${name}.jq"; path = pkgs.writeText "${name}.jq" flakeJq.${name}; }) + [ "selected" "selfIsRemote" "selfLocked" ])} + touch $out + ''; + } diff --git a/tests/jq.sh b/tests/jq.sh new file mode 100755 index 0000000..a62d6cb --- /dev/null +++ b/tests/jq.sh @@ -0,0 +1,122 @@ +#! /bin/sh +# Runs populate's jq programs for flakes over lock files in the shapes +# different versions of Nix write, without VMs. +# +# usage: jq.sh DIR +# +# DIR contains the programs as selected.jq, selfIsRemote.jq, and +# selfLocked.jq. +set -efu + +programs=$1 +failed=0 + +# usage: expect WHAT ACTUAL EXPECTED +expect() { + if test "$2" != "$3"; then + printf 'FAIL: %s\n expected: %s\n got: %s\n' "$1" "$3" "$2" >&2 + failed=1 + fi +} + +# usage: expect_error WHAT ACTUAL MESSAGE +expect_error() { + case $2 in + *"$3"*) ;; + *) + printf 'FAIL: %s\n expected an error: %s\n got: %s\n' "$1" "$3" "$2" >&2 + failed=1 + ;; + esac +} + +# usage: selection METADATA INPUT +selection() { + printf '%s\n' "$1" | + timeout 10 jq -c --argjson input "$2" --from-file "$programs"/selected.jq 2>&1 || + : +} + +# A flake with an input dep, which has an input inner following the flake's +# input inner, and an input sub, a flake in a subdirectory, written as +# attribute dir or as query parameter dir of the URL. +metadata() { + cat <<JSON +{ + "locked": { "type": "path", "path": "/src/top", "narHash": "sha256-top" }, + "path": "/nix/store/top-source", + "locks": { + "root": "root", + "version": 7, + "nodes": { + "root": { "inputs": { "dep": "dep", "inner": "inner", "sub": "sub" } }, + "dep": { + "inputs": { "inner": [ "inner" ] }, + "locked": { "type": "tarball", "url": "http://x/dep.tar.gz", "narHash": "sha256-dep" } + }, + "inner": { + "locked": { "type": "tarball", "url": "http://x/inner.tar.gz", "narHash": "sha256-inner" } + }, + "sub": { + "locked": $1, + "original": $2 + } + } + } +} +JSON +} +attribute=$(metadata \ + '{ "type": "tarball", "url": "http://x/mono.tar.gz", "dir": "sub", "narHash": "sha256-mono" }' \ + '{ "type": "tarball", "url": "http://x/mono.tar.gz", "dir": "sub" }') +url=$(metadata \ + '{ "type": "tarball", "url": "http://x/mono.tar.gz?dir=sub", "narHash": "sha256-mono" }' \ + '{ "type": "tarball", "url": "http://x/mono.tar.gz?dir=sub" }') + +mono='{"self":false,"locked":{"type":"tarball","url":"http://x/mono.tar.gz","narHash":"sha256-mono"},"subdir":"sub"}' +expect "subdirectory as attribute" "$(selection "$attribute" '["sub"]')" "$mono" +expect "subdirectory in the URL" "$(selection "$url" '["sub"]')" "$mono" +expect "the flake itself" "$(selection "$attribute" '[]')" \ + '{"self":true,"locked":null,"subdir":""}' +expect "name through follows" "$(selection "$attribute" '["dep","inner"]')" \ + '{"self":false,"locked":{"type":"tarball","url":"http://x/inner.tar.gz","narHash":"sha256-inner"},"subdir":""}' +expect_error "missing input" "$(selection "$attribute" '["nope"]')" "no input nope" + +# An empty follows path removes an input. +removed=$(printf '%s\n' "$attribute" | jq -c '.locks.nodes.dep.inputs.inner = []') +expect_error "removed input" "$(selection "$removed" '["dep","inner"]')" "no input inner" + +# Inputs following each other. +cyclic=$(printf '%s\n' "$attribute" | jq -c ' + .locks.nodes.dep.inputs.inner = ["inner", "x"] + | .locks.nodes.inner.inputs = { x: ["dep", "inner"] } +') +expect_error "cyclic follows" "$(selection "$cyclic" '["dep","inner"]')" "follows nest too deep" + +# An input given by a path relative to the flake, which is in subdirectory +# sub itself. +relative=$(printf '%s\n' "$attribute" | jq -c ' + .locked.dir = "sub" + | .locks.nodes.root.inputs.lib = "lib" + | .locks.nodes.lib = { locked: { type: "path", path: "./lib" }, parent: [] } +') +expect "relative path" "$(selection "$relative" '["lib"]')" \ + '{"self":true,"locked":null,"subdir":"sub/lib"}' + +# usage: remote LOCKED +remote() { + printf '{ "locked": %s }\n' "$1" | + jq --from-file "$programs"/selfIsRemote.jq +} +expect "path" "$(remote '{ "type": "path", "path": "/src" }')" false +expect "local Git" "$(remote '{ "type": "git", "url": "file:///src" }')" false +expect "remote Git" "$(remote '{ "type": "git", "url": "https://x/y" }')" true +expect "GitHub" "$(remote '{ "type": "github", "owner": "o", "repo": "r" }')" true + +expect "locked reference of the flake itself" "$( + printf '%s\n' '{ "locked": { "__final": true, "type": "tarball", "url": "http://x/mono.tar.gz?dir=sub", "narHash": "sha256-mono" } }' | + jq -c --argjson selected '{"self":true,"locked":null,"subdir":"sub"}' \ + --from-file "$programs"/selfLocked.jq +)" "$mono" + +exit "$failed" |
