aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-12 02:36:21 +0000
committertv <tv@krebsco.de>2026-11-12 02:36:21 +0000
commit74fb1d515e6879922bc65fab4c7099dde9dd92e5 (patch)
tree650a07d3a31923e3c724f9e709ece39d05a2a506
parentb0469337bd8eb3d8e325e82640ac855731975000 (diff)
tests: run populate's jq programs over lock files of both shapes
Depending on the version of Nix, lock files name subdirectories as attribute dir or as query parameter dir of the URL. The VM tests run one version, so only one shape gets covered there. The jq programs for flakes move to pkgs/populate/flake-jq.nix, and a new test, jq, runs them without VMs over lock files of both shapes, with follows, removed and cyclic follows, and inputs given by relative paths.
-rw-r--r--README.md1
-rw-r--r--pkgs/populate/default.nix67
-rw-r--r--pkgs/populate/flake-jq.nix70
-rw-r--r--tests/default.nix13
-rwxr-xr-xtests/jq.sh122
5 files changed, 206 insertions, 67 deletions
diff --git a/README.md b/README.md
index 713bfad..e70da72 100644
--- a/README.md
+++ b/README.md
@@ -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"