1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import shutil
import pytest
import re
import dataclasses

from testlib.fixtures.command import CommandResult
from testlib.fixtures.nix import Nix
from testlib.fixtures.env import ManagedEnv


def build(nix: Nix, *args) -> CommandResult:
    expr = """
        derivation {
            name = "test";
            system = builtins.currentSystem;
            builder = "/bin/sh";
            args = [ "-c" "echo > $out" ];
            outputHashMode = "flat";
            outputHash = "sha256-AbpHGcgLb+kRsJGnwFEktk7uzpZOCcBY74+YBdrKVGs=";
        }
    """
    return nix.nix_build(["-E", expr, "--no-link", "--no-require-sigs", *args]).run()


@dataclasses.dataclass
class Caches:
    good: str
    bad: str


@pytest.fixture
def caches(nix: Nix, env: ManagedEnv) -> Caches:
    good_path, bad_path = env.dirs.home / "good", env.dirs.home / "bad"
    good_uri, bad_uri = f"file://{good_path}", f"file://{bad_path}"

    # build the derivation
    output = build(nix).ok().stdout_s.strip()
    # copy it to the good cache
    nix.nix(["copy", output, "--to", good_uri, "--no-require-sigs"], flake=True).run().ok()
    # create the bad cache by simulating read failures
    shutil.copytree(good_path, bad_path)
    for info in bad_path.glob("*.narinfo"):
        info.chmod(0o200)

    nix.nix(["store", "delete", output], flake=True).run().ok()

    return Caches(good=good_uri, bad=bad_uri)


def test_substitution_fallback_good_first(nix: Nix, caches: Caches):
    build(nix, "--substituters", f"{caches.good} {caches.bad}").ok()


def test_substitution_fallback_bad_first(nix: Nix, caches: Caches):
    # we expect three warnings for the single nar: two from querying, one from the substitution itself
    result = build(nix, "--substituters", f"{caches.bad} {caches.good}").ok()
    assert len(re.findall(r"warning.*narinfo", result.stderr_s)) == 3


def test_substitution_fallback_may_build(nix: Nix, caches: Caches):
    # we expect two errors for the single nar: one from querying, one from the substitution itself
    result = build(nix, "--substituters", f"{caches.bad}", "--fallback").ok()
    assert len(re.findall(r"error.*narinfo", result.stderr_s)) == 2


def test_substitution_fallback_no_build(nix: Nix, caches: Caches):
    # we expect one error, and it's fatal
    result = build(nix, "--substituters", f"{caches.bad}").expect(1)
    assert len(re.findall(r"error.*narinfo", result.stderr_s)) == 1