From e76c6afd8d276c21e8b65d5dc7841a1cef7cec4a Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Sun, 22 Feb 2026 23:57:47 +0100 Subject: [PATCH] testing: migrate flakes/follow-paths.sh Change-Id: I825445a51f496dbe2efde684a0045e6fb56d9c0a --- tests/functional/flakes/follow-paths.sh | 364 ---------------- tests/functional/meson.build | 1 - tests/functional2/flakes/test_follow_paths.py | 387 ++++++++++++++++++ 3 files changed, 387 insertions(+), 365 deletions(-) delete mode 100644 tests/functional/flakes/follow-paths.sh create mode 100644 tests/functional2/flakes/test_follow_paths.py diff --git a/tests/functional/flakes/follow-paths.sh b/tests/functional/flakes/follow-paths.sh deleted file mode 100644 index 1f824793b..000000000 --- a/tests/functional/flakes/follow-paths.sh +++ /dev/null @@ -1,364 +0,0 @@ -source ./common.sh - -requireGit - -flakeFollowsA=$TEST_ROOT/follows/flakeA -flakeFollowsB=$TEST_ROOT/follows/flakeA/flakeB -flakeFollowsC=$TEST_ROOT/follows/flakeA/flakeB/flakeC -flakeFollowsD=$TEST_ROOT/follows/flakeA/flakeD -flakeFollowsE=$TEST_ROOT/follows/flakeA/flakeE - -# Test following path flakerefs. -createGitRepo $flakeFollowsA -mkdir -p $flakeFollowsB -mkdir -p $flakeFollowsC -mkdir -p $flakeFollowsD -mkdir -p $flakeFollowsE - -cat > $flakeFollowsA/flake.nix < $flakeFollowsB/flake.nix < $flakeFollowsC/flake.nix < $flakeFollowsD/flake.nix < $flakeFollowsE/flake.nix < $flakeFollowsA/flake.nix < $flakeFollowsA/flake.nix <&1 | grep 'points outside' - -# Non-existant follows should print a warning. -cat >$flakeFollowsA/flake.nix <&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid'" -nix flake lock "$flakeFollowsA" 2>&1 | grep "warning: input 'B' has an override for a non-existent input 'invalid2'" - -# Now test follow path overloading -# This tests a lockfile checking regression https://github.com/NixOS/nix/pull/8819 -# -# We construct the following graph, where p->q means p has input q. -# A double edge means that the edge gets overridden using `follows`. -# -# A -# / \ -# / \ -# v v -# B ==> C --- follows declared in A -# \\ / -# \\/ --- follows declared in B -# v -# D -# -# The message was -# error: input 'B/D' follows a non-existent input 'B/C/D' -# -# Note that for `B` to resolve its follow for `D`, it needs `C/D`, for which it needs to resolve the follow on `C` first. -flakeFollowsOverloadA="$TEST_ROOT/follows/overload/flakeA" -flakeFollowsOverloadB="$TEST_ROOT/follows/overload/flakeA/flakeB" -flakeFollowsOverloadC="$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC" -flakeFollowsOverloadD="$TEST_ROOT/follows/overload/flakeA/flakeB/flakeC/flakeD" - -# Test following path flakerefs. -createGitRepo "$flakeFollowsOverloadA" -mkdir -p "$flakeFollowsOverloadB" -mkdir -p "$flakeFollowsOverloadC" -mkdir -p "$flakeFollowsOverloadD" - -cat > "$flakeFollowsOverloadD/flake.nix" < "$flakeFollowsOverloadC/flake.nix" < "$flakeFollowsOverloadB/flake.nix" < "$flakeFollowsOverloadA/flake.nix" < $flakeFollowsD/flake.nix -{ outputs = _: {}; } -EOF -cat < $flakeFollowsC/flake.nix -{ - inputs.D.url = "path:nosuchflake"; - outputs = _: {}; -} -EOF -cat < $flakeFollowsB/flake.nix -{ - inputs.C.url = "path:$flakeFollowsC"; - outputs = _: {}; -} -EOF -cat < $flakeFollowsA/flake.nix -{ - inputs.B.url = "path:$flakeFollowsB"; - inputs.D.url = "path:$flakeFollowsD"; - inputs.B.inputs.C.inputs.D.follows = "D"; - outputs = _: {}; -} -EOF - -nix flake lock $flakeFollowsA - -[[ $(jq -c .nodes.C.inputs.D $flakeFollowsA/flake.lock) = '["D"]' ]] - -# Test overlapping flake follows: B has D follow C/D, while A has B/C follow C - -cat < $flakeFollowsC/flake.nix -{ - inputs.D.url = "path:$flakeFollowsD"; - outputs = _: {}; -} -EOF -cat < $flakeFollowsB/flake.nix -{ - inputs.C.url = "path:nosuchflake"; - inputs.D.url = "path:nosuchflake"; - inputs.D.follows = "C/D"; - outputs = _: {}; -} -EOF -cat < $flakeFollowsA/flake.nix -{ - inputs.B.url = "path:$flakeFollowsB"; - inputs.C.url = "path:$flakeFollowsC"; - inputs.B.inputs.C.follows = "C"; - outputs = _: {}; -} -EOF - -# bug was not triggered without recreating the lockfile -nix flake update --flake $flakeFollowsA - -[[ $(jq -c .nodes.B.inputs.D $flakeFollowsA/flake.lock) = '["B","C","D"]' ]] - -cat < $flakeFollowsA/flake.nix -{ - inputs.B.url = "path:$flakeFollowsB"; - inputs.C.url = "path:$flakeFollowsC"; - inputs.B.inputs.C.inputs.E.follows = "C"; - outputs = _: {}; -} -EOF -cat < $flakeFollowsB/flake.nix -{ - inputs.C.url = "path:$flakeFollowsC"; - outputs = _: {}; -} -EOF - -nix flake update --flake $flakeFollowsA 2>&1 | grepQuiet "warning: input 'B/C' has an override for a non-existent input 'E'" - -# Test for Nested follows cause flake interactions to update the nested input #460 -for letter in {A..E}; do - path="flakeFollows${letter}" - rm -f "${!path}"/flake.lock -done - -cat < $flakeFollowsA/flake.nix -{ - inputs = { - B.url = "path:$flakeFollowsB"; - C = { - url = "path:$flakeFollowsC"; - inputs.D.inputs.E.follows = "B"; - }; - }; - outputs = _: {}; -} -EOF - -cat < $flakeFollowsB/flake.nix -{ - outputs = _: {}; -} -EOF - -cat < $flakeFollowsC/flake.nix -{ - inputs = { - D.url = "path:$flakeFollowsD"; - }; - outputs = _: {}; -} -EOF - -cat < $flakeFollowsD/flake.nix -{ - inputs = { - E.url = "path:$flakeFollowsE"; - }; - outputs = _: {}; -} -EOF - -# Lockfiles are cleared, initially the dependency needs to be fetched. -out="$(nix --verbose flake show path:$flakeFollowsA 2>&1)" -echo "$out" -[[ "$out" = *$'\n'"fetching path input 'path:"*"/flakeD'"$'\n'* ]] - -# But on another flake command it doesn't. -out="$(nix --verbose flake show path:$flakeFollowsA 2>&1)" -[[ "$out" != *$'\n'"fetching path input 'path:"*"/flakeD'"$'\n'* ]] - -# Make sure the nested override is actually correct in this testcase. -[[ "$(nix flake metadata path:$flakeFollowsA --json | jq '.locks.nodes.D.inputs.E|.[]' -r)" = "B" ]] diff --git a/tests/functional/meson.build b/tests/functional/meson.build index f50e3b86e..42ab13dd2 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -33,7 +33,6 @@ functional_tests_scripts = [ 'init.sh', 'test-infra.sh', 'flakes/flakes.sh', - 'flakes/follow-paths.sh', 'gc.sh', 'nix-collect-garbage-d.sh', 'nix-collect-garbage-dry-run.sh', diff --git a/tests/functional2/flakes/test_follow_paths.py b/tests/functional2/flakes/test_follow_paths.py new file mode 100644 index 000000000..5e67c8f7f --- /dev/null +++ b/tests/functional2/flakes/test_follow_paths.py @@ -0,0 +1,387 @@ +from testlib.fixtures.nix import Nix +from testlib.fixtures.git import Git +from testlib.fixtures.file_helper import with_files, File, EnvTemplate +from testlib.utils import get_global_asset_pack +from pathlib import Path +import pytest +import fnmatch +import json + + +@pytest.fixture(autouse=True) +def add_xp_features(nix: Nix): + nix.settings.add_xp_feature("nix-command", "flakes") + + +@with_files( + { + "flakeA": get_global_asset_pack(".git") + | { + "flake.nix": EnvTemplate("""{ + inputs = { + B = { + url = "path:./flakeB"; + inputs.foobar.follows = "foobar"; + }; + foobar.url = "path:@HOME@/flakeA/flakeE"; + }; + outputs = { ... }: {}; + }"""), + "flakeB": { + "flake.nix": EnvTemplate("""{ + inputs = { + foobar.url = "path:@HOME@/flakeA/flakeE"; + goodoo.follows = "C/goodoo"; + C = { + url = "path:./flakeC"; + inputs.foobar.follows = "foobar"; + }; + }; + outputs = { ... }: {}; + }"""), + "flakeC": { + "flake.nix": EnvTemplate("""{ + inputs = { + foobar.url = "path:@HOME@/flakeA/flakeE"; + goodoo.follows = "foobar"; + }; + outputs = { ... }: {}; + }""") + }, + }, + "flakeD": {"flake.nix": File("{ outputs = { ... }: {}; }")}, + "flakeE": {"flake.nix": File("{ outputs = { ... }: {}; }")}, + } + } +) +class TestPathFollows: + @pytest.fixture(autouse=True) + def init(self, git: Git, files: Path): + git(files / "flakeA", "add", ".") + + def test_metadata(self, nix: Nix, files: Path): + out = nix.nix(["flake", "metadata", files / "flakeA"]).run().ok() + assert "B: path" in out.stdout_s + assert "C: path" in out.stdout_s + assert "foobar: path" in out.stdout_s + assert "goodoo follows" in out.stdout_s + + def test_update(self, nix: Nix, files: Path): + out = nix.nix(["flake", "update", "--flake", files / "flakeA"]).run().ok() + assert "Added input 'B" in out.stderr_s + assert "Added input 'B/C" in out.stderr_s + assert "Added input 'B/C/foobar" in out.stderr_s + assert "Added input 'B/C/goodoo" in out.stderr_s + assert "Added input 'B/foobar" in out.stderr_s + assert "Added input 'B/goodoo" in out.stderr_s + + def test_lock(self, nix: Nix, files: Path): + nix.nix(["flake", "lock", files / "flakeA"]).run().ok() + lockfile = json.loads((files / "flakeA/flake.lock").read_text()) + + assert lockfile["nodes"]["B"]["inputs"]["C"] == "C" + assert lockfile["nodes"]["B"]["inputs"]["foobar"] == ["foobar"] + assert lockfile["nodes"]["C"]["inputs"]["foobar"] == ["B", "foobar"] + + # Ensure that locking twice doesn't change anything + nix.nix(["flake", "lock", files / "flakeA"]).run().ok() + lockfile2 = json.loads((files / "flakeA/flake.lock").read_text()) + assert lockfile == lockfile2 + + def test_removing_follows_updates_lockfile(self, nix: Nix, files: Path, git: Git): + git(files / "flakeA", "add", ".") + + nix.nix(["flake", "lock", files / "flakeA"]).run().ok() + (files / "flakeA/flake.nix").write_text("""{ + inputs = { + B.url = "path:./flakeB"; + D.url = "path:./flakeD"; + }; + outputs = { ... }: {}; + }""") + + nix.nix(["flake", "lock", files / "flakeA"]).run().ok() + + lockfile = json.loads((files / "flakeA/flake.lock").read_text()) + assert lockfile["nodes"]["B"]["inputs"]["foobar"] == "foobar" + assert "foobar" in lockfile["nodes"] + + +@with_files( + { + "flakeA": get_global_asset_pack(".git") + | { + "flake.nix": File("""{ + inputs.B.url = "path:../flakeB"; + outputs = { ... }: {}; + }""") + } + } +) +def test_relative_path_cant_leave_store_path(nix: Nix, files: Path, git: Git): + git(files / "flakeA", "add", ".") + + out = nix.nix(["flake", "lock", files / "flakeA"]).run().expect(1) + assert "points outside" in out.stderr_s + + +@with_files( + { + "flakeA": get_global_asset_pack(".git") + | { + "flake.nix": File("""{ + inputs.B = { + url = "path:./flakeB"; + inputs.invalid.follows = "D"; + inputs.invalid2.url = "path:./flakeD"; + }; + inputs.D.url = "path:./flakeD"; + outputs = { ... }: {}; + }"""), + "flakeB": { + "flake.nix": EnvTemplate("""{ + inputs = { + foobar.url = "path:@HOME@/flakeA/flakeE"; + goodoo.follows = "C/goodoo"; + C = { + url = "path:./flakeC"; + inputs.foobar.follows = "foobar"; + }; + }; + outputs = { ... }: {}; + }"""), + "flakeC": { + "flake.nix": EnvTemplate("""{ + inputs = { + foobar.url = "path:@HOME@/flakeA/flakeE"; + goodoo.follows = "foobar"; + }; + outputs = { ... }: {}; + }""") + }, + }, + "flakeD": {"flake.nix": File("{ outputs = { ... }: {}; }")}, + "flakeE": {"flake.nix": File("{ outputs = { ... }: {}; }")}, + } + } +) +def test_nonexistent_follows_print_warnings(nix: Nix, files: Path, git: Git): + git(files / "flakeA", "add", ".") + + out = nix.nix(["flake", "lock", files / "flakeA"]).run().ok() + assert "warning: input 'B' has an override for a non-existent input 'invalid'" in out.stderr_s + assert "warning: input 'B' has an override for a non-existent input 'invalid2'" in out.stderr_s + + +@with_files( + { + "flakeA": get_global_asset_pack(".git") + | { + # input B/D should be able to be found... + "flake.nix": File("""{ + inputs = { + B = { + url = "path:./flakeB"; + inputs.C.follows = "C"; + }; + C.url = "path:./flakeB/flakeC"; + }; + outputs = { ... }: {}; + }"""), + "flakeB": { + "flake.nix": File("""{ + inputs = { + C.url = "path:./flakeC"; + D.follows = "C/D"; + }; + outputs = { ... }: {}; + }"""), + "flakeC": { + "flake.nix": File("""{ + inputs.D.url = "path:./flakeD"; + outputs = { ... }: {}; + }"""), + "flakeD": {"flake.nix": File("{ outputs = { ... }: {}; }")}, + }, + }, + } + } +) +def test_follow_path_overloading(nix: Nix, files: Path, git: Git): + r""" + This tests a lockfile checking regression https://github.com/NixOS/nix/pull/8819 + + We construct the following graph, where p->q means p has input q. + A double edge means that the edge gets overridden using `follows`. + + A + / \ + / \ + v v + B ==> C --- follows declared in A + \\ / + \\/ --- follows declared in B + v + D + + The message was + error: input 'B/D' follows a non-existent input 'B/C/D' + + Note that for `B` to resolve its follow for `D`, it needs `C/D`, + for which it needs to resolve the follow on `C` first. + """ + + git(files / "flakeA", "add", ".") + + nix.nix(["flake", "metadata", f"{files}/flakeA"]).run().ok() + nix.nix(["flake", "update", "--flake", f"{files}/flakeA"]).run().ok() + nix.nix(["flake", "lock", f"{files}/flakeA"]).run().ok() + + lockfile = json.loads((files / "flakeA/flake.lock").read_text()) + assert lockfile["nodes"]["B"]["inputs"] == {"C": ["C"], "D": ["B", "C", "D"]} + assert lockfile["nodes"]["C"]["inputs"] == {"D": "D"} + assert lockfile["nodes"]["root"]["inputs"] == {"B": "B", "C": "C"} + + +@with_files( + { + "flakeA": { + "flake.nix": EnvTemplate("""{ + inputs.B.url = "path:@HOME@/flakeA/flakeB"; + inputs.D.url = "path:@HOME@/flakeA/flakeD"; + inputs.B.inputs.C.inputs.D.follows = "D"; + outputs = _: {}; + }"""), + "flakeB": { + "flake.nix": EnvTemplate("""{ + inputs.C.url = "path:@HOME@/flakeA/flakeB/flakeC"; + outputs = _: {}; + }"""), + "flakeC": { + "flake.nix": EnvTemplate("""{ + inputs.D.url = "path:nosuchflake"; + outputs = _: {}; + }""") + }, + }, + "flakeD": {"flake.nix": File("{ outputs = _: {}; }")}, + } + } +) +def test_nested_overrides(nix: Nix, files: Path): + nix.nix(["flake", "lock", f"{files}/flakeA"]).run().ok() + lockfile = json.loads((files / "flakeA/flake.lock").read_text()) + assert lockfile["nodes"]["C"]["inputs"]["D"] == ["D"] + + +@with_files( + { + "flakeA": { + "flake.nix": EnvTemplate("""{ + inputs.B.url = "path:@HOME@/flakeA/flakeB"; + inputs.C.url = "path:@HOME@/flakeA/flakeB/flakeC"; + inputs.B.inputs.C.follows = "C"; + outputs = _: {}; + }"""), + "flakeB": { + "flake.nix": EnvTemplate("""{ + inputs.C.url = "path:nosuchflake"; + inputs.D.url = "path:nosuchflake"; + inputs.D.follows = "C/D"; + outputs = _: {}; + }"""), + "flakeC": { + "flake.nix": EnvTemplate("""{ + inputs.D.url = "path:@HOME@/flakeA/flakeD"; + outputs = _: {}; + }""") + }, + }, + "flakeD": {"flake.nix": File("{ outputs = _: {}; }")}, + } + } +) +def test_overlapping_flake_follows(nix: Nix, files: Path): + # bug was not triggered without recreating the lockfile + nix.nix(["flake", "update", "--flake", f"{files}/flakeA"]).run().ok() + lockfile = json.loads((files / "flakeA/flake.lock").read_text()) + assert lockfile["nodes"]["B"]["inputs"]["D"] == ["B", "C", "D"] + + +@with_files( + { + "flakeA": { + "flake.nix": EnvTemplate("""{ + inputs.B.url = "path:@HOME@/flakeA/flakeB"; + inputs.C.url = "path:@HOME@/flakeA/flakeB/flakeC"; + inputs.B.inputs.C.inputs.E.follows = "C"; + outputs = _: {}; + }"""), + "flakeB": { + "flake.nix": EnvTemplate("""{ + inputs.C.url = "path:@HOME@/flakeA/flakeB/flakeC"; + outputs = _: {}; + }"""), + "flakeC": { + "flake.nix": EnvTemplate("""{ + inputs.D.url = "path:@HOME@/flakeA/flakeD"; + outputs = _: {}; + }""") + }, + }, + "flakeD": {"flake.nix": File("{ outputs = _: {}; }")}, + } + } +) +def test_override_nonexistant_input(nix: Nix, files: Path): + """Test overlapping flake follows: B has D follow C/D, while A has B/C follow C""" + out = nix.nix(["flake", "update", "--flake", f"{files}/flakeA"]).run().ok() + assert "warning: input 'B/C' has an override for a non-existent input 'E'" in out.stderr_s + + +@with_files( + { + "flakeA": { + "flake.nix": EnvTemplate("""{ + inputs = { + B.url = "path:@HOME@/flakeA/flakeB"; + C = { + url = "path:@HOME@/flakeA/flakeB/flakeC"; + inputs.D.inputs.E.follows = "B"; + }; + }; + outputs = _: {}; + }"""), + "flakeB": { + "flake.nix": File("{ outputs = _: {}; }"), + "flakeC": { + "flake.nix": EnvTemplate("""{ + inputs.D.url = "path:@HOME@/flakeA/flakeD"; + outputs = _: {}; + }""") + }, + }, + "flakeD": { + "flake.nix": File("""{ + inputs.E.url = "path:@HOME@/flakeA/flakeE"; + outputs = _: {}; + }""") + }, + "flakeE": {"flake.nix": File("{ outputs = { ... }: {}; }")}, + } + } +) +def test_dep_fetch(nix: Nix, files: Path): + """Test for Nested follows cause flake interactions to update the nested input #460""" + + # Lockfiles are cleared, initially the dependency needs to be fetched. + out = nix.nix(["--verbose", "flake", "show", f"path:{files}/flakeA"]).run().ok() + assert fnmatch.fnmatch(out.stderr_s, "*\nfetching path input 'path:*/flakeD'\n*") + + # But on another flake command it doesn't. + out = nix.nix(["--verbose", "flake", "show", f"path:{files}/flakeA"]).run().ok() + assert not fnmatch.fnmatch(out.stderr_s, "*\nfetching path input 'path:*/flakeD'\n*") + + # Make sure the nested override is actually correct in this testcase. + metadata = nix.nix(["flake", "metadata", f"path:{files}/flakeA", "--json"]).run().json() + assert metadata["locks"]["nodes"]["D"]["inputs"]["E"] == ["B"]