okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.petfix: guard closed child mail and remove inherited shim generationsff92766

main
7.9 KiB203 linesraw
1"""The nix and cargo shims through the real kernel: in a jj workspace without
2.git, `nix develop -c` runs a program in a dev shell built from the
3workspace's commit (with its packages and shellHook) and cached, and nix reads
4the flake from that commit instead of copying the directory; cargo goes
5through mbx, with the environment mbx needs to share builds across checkouts."""
6
7import json
8import os
9import shutil
10import subprocess
11import sys
12import unittest
13from pathlib import Path
14
15ROOT = Path(__file__).resolve().parents[2]
16sys.path.insert(0, str(ROOT / "priv/python"))
17import albedo_shims  # noqa: E402
18
19from harness import Albedo, Provider, exclusive, python, text  # noqa: E402
20
21
22def script_for(test):
23    def script(request):
24        if request["messages"][-1].get("role") == "user":
25            return python(test.code)
26        return text("done")
27
28    return script
29
30
31def cell_results(app, session):
32    return [
33        json.loads(part["value"])
34        for entry in app.history(session)["items"]
35        if entry["kind"] == "tool_result" and entry["tool"]["name"] == "python"
36        for part in entry["content"]
37        if part["kind"] == "json" and part["field"] == "result"
38    ]
39
40
41FAKE_MBX = """#!/bin/sh
42echo "mbx $* sdk=${SDKROOT-unset} cc=${CC-unset} mode=$MBX_CARGO_SHIM_MODE flags=$HOST_CFLAGS"
43"""
44
45
46class CargoShimTests(unittest.TestCase):
47    def setUp(self):
48        self.provider = Provider(script_for(self))
49        self.addCleanup(self.provider.close)
50        self.app = Albedo(self.provider)
51        self.app.__enter__()
52        self.addCleanup(self.app.__exit__, None, None, None)
53
54    def test_cargo_runs_through_mbx_without_what_keeps_builds_apart(self):
55        bin = self.app.workspace / "bin"
56        bin.mkdir()
57        for name, script in (
58            ("mbx", FAKE_MBX),
59            ("cc", "#!/bin/sh\n"),
60            ("gcc", "#!/bin/sh\n"),
61        ):
62            (bin / name).write_text(script)
63            (bin / name).chmod(0o755)
64        # the shell's compiler under another name, as clang is cc in a nix shell
65        (bin / "clang").symlink_to("cc")
66        session = self.app.session()
67        self.code = (
68            f"path = {str(bin)!r} + ':' + os.environ['PATH']\n"
69            "for cc, extra in (('clang', {}), ('clang', {'ALBEDO_NIX_ENV': 'abc'}),\n"
70            "                  ('gcc', {'ALBEDO_NIX_ENV': 'abc'})):\n"
71            "    env = {'PATH': path, 'SDKROOT': '/nix/store/x-sdk', 'CC': cc, **extra}\n"
72            "    job = run('cargo', 'build', env=env)\n"
73            "    await job\n"
74            "    print(job.tail().strip())\n"
75        )
76        self.app.prompt(session, "build").close()
77        self.app.idle(session)
78        lines = cell_results(self.app, session)[-1]["output"].splitlines()
79        # outside a nix shell a chosen compiler stays
80        self.assertEqual(lines[0], "mbx build sdk=unset cc=clang mode=1 flags=")
81        # inside a nix shell the shell's own cc goes to mbx, and the wrapper is keyed
82        self.assertEqual(
83            lines[1], "mbx build sdk=unset cc=unset mode=1 flags= -DALBEDO_NIX_ENV_abc"
84        )
85        # a shell that picks another compiler keeps it
86        self.assertEqual(
87            lines[2], "mbx build sdk=unset cc=gcc mode=1 flags= -DALBEDO_NIX_ENV_abc"
88        )
89
90
91def nixpkgs_lock():
92    """albedo's own locked nixpkgs: in the store wherever its dev shell is, so
93    the test flake needs no network."""
94    lock = json.loads((ROOT / "flake.lock").read_text())
95    return lock["nodes"][lock["nodes"]["root"]["inputs"]["nixpkgs"]]
96
97
98FLAKE = """{
99  inputs.nixpkgs.url = "github:%(owner)s/%(repo)s/%(ref)s";
100  outputs = { self, nixpkgs }: {
101    devShells = nixpkgs.lib.genAttrs
102      [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ]
103      (system: let pkgs = nixpkgs.legacyPackages.${system}; in {
104        default = pkgs.mkShell {
105          packages = [ (pkgs.writeShellScriptBin "albedo-nix-probe" "echo probe:$ALBEDO_NIX_MARK") ];
106          shellHook = "export ALBEDO_NIX_MARK=from-hook";
107        };
108      });
109    marker = "read-from-the-commit";
110  };
111}
112"""
113
114
115@exclusive
116@unittest.skipUnless(shutil.which("nix") and shutil.which("jj"), "needs nix and jj")
117class NixShellTests(unittest.TestCase):
118    def setUp(self):
119        def prepare(app):
120            # Keep two inherited shim generations to cover nested daemon launches.
121            source = albedo_shims.NIX
122            inherited = []
123            for name in ("outer-a", "outer-b"):
124                directory = app.workspace / name / "shims" / "nix"
125                directory.mkdir(parents=True)
126                shim = directory / "nix"
127                shim.write_text(source)
128                shim.chmod(0o755)
129                inherited.append(str(directory))
130            app.env["PATH"] = os.pathsep.join([*inherited, app.env["PATH"]])
131            app.env["NIX_CONFIG"] = "experimental-features = nix-command flakes"
132            # a first dev shell evaluates nixpkgs; let the cell finish in place
133            app.env["ALBEDO_CELL_BACKGROUND_SECONDS"] = "900"
134
135        self.provider = Provider(script_for(self))
136        self.addCleanup(self.provider.close)
137        self.app = Albedo(self.provider, prepare=prepare)
138        self.app.__enter__()
139        self.addCleanup(self.app.__exit__, None, None, None)
140
141    def jj(self, *args, cwd):
142        subprocess.run(
143            ["jj", *args],
144            cwd=cwd,
145            env=self.app.env,
146            check=True,
147            capture_output=True,
148            timeout=120,
149        )
150
151    def test_a_jj_workspace_is_read_from_its_commit_and_its_shell_is_cached(self):
152        node = nixpkgs_lock()
153        main = self.app.workspace / "main"
154        main.mkdir()
155        (main / "flake.nix").write_text(FLAKE % node["original"])
156        lock = {
157            "nodes": {"nixpkgs": node, "root": {"inputs": {"nixpkgs": "nixpkgs"}}},
158            "root": "root",
159            "version": 7,
160        }
161        (main / "flake.lock").write_text(json.dumps(lock))
162        (main / ".gitignore").write_text("/build\n")
163        self.jj("git", "init", cwd=main)
164        self.jj("commit", "-m", "flake", cwd=main)
165        workspace = self.app.workspace / "ws"
166        self.jj("workspace", "add", "--name", "ws", str(workspace), cwd=main)
167        self.assertFalse((workspace / ".git").exists())
168        (workspace / "build").mkdir()
169        (workspace / "build" / "output").write_bytes(b"\0" * 1_000_000)
170        session = self.app.session(workspace)
171        self.code = (
172            "for name, argv in (('first', ['nix', 'develop', '-c', 'albedo-nix-probe']),\n"
173            "                   ('again', ['nix', 'develop', '-c', 'albedo-nix-probe']),\n"
174            "                   ('eval', ['nix', 'eval', '--raw', '.#marker'])):\n"
175            "    job = run(*argv, timeout=900)\n"
176            "    await job\n"
177            "    print(name, job.exit_code, json.dumps(job.tail()))\n"
178        )
179        self.app.prompt(session, "use the dev shell").close()
180        self.app.idle(session)
181        result = cell_results(self.app, session)[-1]
182        self.assertEqual(result["status"], "ok", result)
183        runs = {
184            name: (int(code), json.loads(output))
185            for name, code, output in (
186                line.split(" ", 2) for line in result["output"].splitlines()
187            )
188        }
189        self.assertEqual(runs["first"][0], 0, runs["first"])
190        self.assertIn("albedo: building the dev shell", runs["first"][1])
191        self.assertTrue(runs["first"][1].endswith("probe:from-hook\n"))
192        # the cached shell runs the program and says nothing else
193        self.assertEqual(runs["again"], (0, "probe:from-hook\n"))
194        code, output = runs["eval"]
195        self.assertEqual(code, 0, output)
196        self.assertIn(f"git+file://{main.resolve()}?rev=", output)
197        self.assertTrue(output.endswith("read-from-the-commit"))
198        # nothing nix locked went into the main checkout
199        self.assertEqual(json.loads((main / "flake.lock").read_text()), lock)
200
201
202if __name__ == "__main__":
203    unittest.main()