Files
lix/tests/functional2/flakes/test_follow_paths.py
T
piegamesandeldritch horrors 567348a90c testlib: default nix fixture to also test all daemon protocols (2/3)
This commit enables the parametrization for "legacy-combined" protocol.
All failing tests with that have been either fixed or disabled. Notably:

- A couple of tests (e.g. involving builders) could be made to work with
a non-local store, but this would require some refactoring to testlib in
order to make certain configuration settings generic over local and
remote operations. We've disabled those for now, in order to make
progress
- The store tests all run locally only because of their nature
- The flakes tests also all run locally only because my energy for
fixing them is limited (and it's a *lot* of test failures, with probably
little overall benefit in terms of test coverage)

Change-Id: I56fa249a64f7c17c952f688ec89e9687f2a13f12
2026-06-28 18:17:57 +02:00

390 lines
14 KiB
Python

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
pytestmark = pytest.mark.no_daemon
@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"]