Files
home-manager/tests/lib/generators/mkImpureConfigMerger.nix
Austin Horstman 199871bf7e generators: document TOML date limitation in mkImpureConfigMerger
jaq 3.1.1 reads TOML through toml-span, which does not deserialize
date or time values. An existing TOML file containing one makes the
merge refuse, so activation fails on every switch until the value is
removed. The file itself is left untouched.

Document the limitation and test that the refused merge leaves the
file byte-identical. If a later jaq parses these values, they would
pass through JSON as strings, so the test is expected to fail then and
prompt a review of the documented behavior.
2026-10-02 09:41:18 -05:00

209 lines
6.9 KiB
Nix

# Tests for the activation snippet generated by mkImpureConfigMerger. It is
# run the way home activation runs it: as a script with the home-manager bash
# helpers available, against files in a temporary home.
{
lib,
pkgs,
...
}:
let
mkMerger =
format: static:
lib.hm.generators.mkImpureConfigMerger {
inherit pkgs format;
empty = "{}";
# The trailing comment must not break the filter the operation is
# embedded in.
jqOperation = "$dynamic * $static # recursive merge";
path = "/@TMPDIR@/hm-user/.config/testmerger/settings.${format}";
staticSettings = builtins.toFile "static-settings.${format}" static;
};
# Mirror the activation script environment: set -euo pipefail is active and
# home-manager.sh provides errorEcho.
mkScript =
name: snippet:
pkgs.writeScript name ''
set -euo pipefail
errorEcho() { echo "$*" >&2; }
${snippet}
'';
mergeJson = mkScript "merge-json" (mkMerger "json" (builtins.toJSON { nixManaged = true; }));
mergeToml = mkScript "merge-toml" (mkMerger "toml" "nixManaged = true\n");
# Messages must print the path literally instead of evaluating it.
mergeOddPath = mkScript "merge-odd-path" (
lib.hm.generators.mkImpureConfigMerger {
inherit pkgs;
format = "json";
empty = "{}";
jqOperation = "$dynamic * $static";
path = "/@TMPDIR@/hm-user/odd $(touch marker)/settings.json";
staticSettings = builtins.toFile "static-settings.json" "{}";
}
);
largeStatic = builtins.toJSON { nixManaged = lib.genList (i: i) 40000; };
largeStaticFile = builtins.toFile "static-settings.json" largeStatic;
mergeLargeStatic = mkScript "merge-large-static" (mkMerger "json" largeStatic);
# jaq prints the first object of this stream before failing on the
# truncated second one, so checking only the reader output would accept a
# partial read.
truncatedJson = builtins.toFile "truncated.json" ''
{"keep":true}
{"lost":
'';
userToml = builtins.toFile "user.toml" ''
userSetting = 5
'';
expectedJson = builtins.toFile "expected.json" ''
{
"nixManaged": true
}
'';
expectedToml = builtins.toFile "expected.toml" ''
nixManaged = true
'';
expectedUserToml = builtins.toFile "expected-user.toml" ''
userSetting = 5
nixManaged = true
'';
mergeYaml = mkScript "merge-yaml" (
mkMerger "yaml" ''
managed:
key: nix
''
);
userYaml = builtins.toFile "user.yaml" ''
user:
theme: dark
recent:
- /a
- /b
'';
expectedYaml = builtins.toFile "expected.yaml" ''
---
user:
theme: dark
recent:
- /a
- /b
managed:
key: nix
...
'';
userDateToml = builtins.toFile "user-date.toml" ''
# Keep this file intact when jaq cannot parse its date value.
date = 1979-05-27T07:32:00Z
userSetting = 5
'';
in
{
nmt.script = ''
substitute ${mergeJson} $TMPDIR/merge-json --subst-var TMPDIR
substitute ${mergeToml} $TMPDIR/merge-toml --subst-var TMPDIR
substitute ${mergeLargeStatic} $TMPDIR/merge-large-static --subst-var TMPDIR
chmod +x $TMPDIR/merge-json $TMPDIR/merge-toml $TMPDIR/merge-large-static
mkdir -p $TMPDIR/hm-user/.config/testmerger
jsonSettings=$TMPDIR/hm-user/.config/testmerger/settings.json
tomlSettings=$TMPDIR/hm-user/.config/testmerger/settings.toml
# A reader that emits JSON before failing must fail activation and keep
# the original file instead of merging the partial output.
cat ${truncatedJson} > $jsonSettings
if $TMPDIR/merge-json > /dev/null 2>&1; then
fail "Merging a truncated JSON stream must fail activation"
fi
assertFileContent $jsonSettings ${truncatedJson}
printf '%s\n' '{"a":1}' '{"b":2}' > $jsonSettings
cp $jsonSettings $TMPDIR/original.json
if $TMPDIR/merge-json > /dev/null 2> $TMPDIR/merge-error; then
fail "Merging multiple JSON values must fail activation"
fi
assertFileContent $jsonSettings $TMPDIR/original.json
grep -qF "$jsonSettings" $TMPDIR/merge-error \
|| fail "A failed merge must name the config file"
${lib.getExe pkgs.jaq} -cn '{userSetting: [range(0; 40000)]}' > $jsonSettings
if [ "$(wc -c < $jsonSettings)" -le 131072 ]; then
fail "The existing JSON file must exceed 128 KiB"
fi
$TMPDIR/merge-json
${lib.getExe pkgs.jaq} -e '.userSetting == [range(0; 40000)] and .nixManaged == true' $jsonSettings > /dev/null
if [ "$(wc -c < ${largeStaticFile})" -le 131072 ]; then
fail "The Nix-declared JSON settings must exceed 128 KiB"
fi
echo '{"userSetting":true}' > $jsonSettings
$TMPDIR/merge-large-static
${lib.getExe pkgs.jaq} -e '.userSetting == true and .nixManaged == [range(0; 40000)]' $jsonSettings > /dev/null
substitute ${mergeOddPath} $TMPDIR/merge-odd-path --subst-var TMPDIR
chmod +x $TMPDIR/merge-odd-path
oddSettings='/'"$TMPDIR"'/hm-user/odd $(touch marker)/settings.json'
DRY_RUN=1 $TMPDIR/merge-odd-path > $TMPDIR/odd-output
mkdir -p "$(dirname "$oddSettings")"
echo '{' > "$oddSettings"
if $TMPDIR/merge-odd-path 2>> $TMPDIR/odd-output; then
fail "Merging a malformed file must fail activation"
fi
[[ ! -e marker ]] || fail "Messages must not run commands in the path"
grep -qF 'odd $(touch marker)/settings.json' $TMPDIR/odd-output \
|| fail "Messages must contain the literal path"
# A missing JSON target installs the generated settings.
rm -f $jsonSettings
$TMPDIR/merge-json
assertFileContent $jsonSettings ${expectedJson}
# A missing TOML target installs the generated settings instead of
# failing on a file seeded with the JSON `empty` value.
$TMPDIR/merge-toml
assertFileContent $tomlSettings ${expectedToml}
# Existing user settings survive the merge.
cat ${userToml} > $tomlSettings
$TMPDIR/merge-toml
assertFileContent $tomlSettings ${expectedUserToml}
# A zero-byte file is treated as `empty`.
: > $tomlSettings
$TMPDIR/merge-toml
assertFileContent $tomlSettings ${expectedToml}
# jaq 3.1.1 cannot parse TOML dates. If a newer jaq can, they would pass
# through JSON as strings and come back as TOML strings, so this failing
# means the documented limitation needs revisiting.
cat ${userDateToml} > $tomlSettings
if $TMPDIR/merge-toml > /dev/null 2>&1; then
fail "Merging TOML with a date value must fail activation"
fi
assertFileContent $tomlSettings ${userDateToml}
substitute ${mergeYaml} $TMPDIR/merge-yaml --subst-var TMPDIR
chmod +x $TMPDIR/merge-yaml
yamlSettings=$TMPDIR/hm-user/.config/testmerger/settings.yaml
# YAML is written in block style, and merging the written file again
# leaves it unchanged.
cat ${userYaml} > $yamlSettings
$TMPDIR/merge-yaml
assertFileContent $yamlSettings ${expectedYaml}
$TMPDIR/merge-yaml
assertFileContent $yamlSettings ${expectedYaml}
'';
}