okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
74cf623
main
1"""How the nix shim rewrites a command line in a jj workspace without .git, 2and what a cached dev shell must carry. Each rewrite case costs a real nix 3evaluation end to end, and the mistakes are quiet: a lock file written over 4the workspace's (`--inputs-from .` once wrote nixpkgs' empty lock into it), 5a flake inserted into a command that takes none, or a lock written into the 6main checkout the ref names. The carry cases run a fake nix whose 7print-dev-env writes a script shaped like nix's own: a cached shell dropped 8every variable whose value the capture's own environment already held 9(mkShell's env.* among them), so `nix develop -c env` showed no CGO_ENABLED 10and a cgo link failed.""" 11 12import os 13from pathlib import Path 14import shutil 15import sys 16import tempfile 17import unittest 18from unittest import mock 19 20sys .path .insert (0 ,str (Path (__file__ ).resolve ().parents [2 ]/ "priv" / "python" )) 21import albedo_nix # noqa: E402 22 23 24@ unittest . skipUnless ( shutil . which ( "nix" ), "needs nix for its flag table" ) 25class RewriteTest (unittest .TestCase ): 26def setUp (self ): 27scratch = tempfile .TemporaryDirectory () 28self .addCleanup (scratch .cleanup ) 29self .workspace = Path (scratch .name ).resolve ()/ "ws" 30 (self .workspace / ".jj" ).mkdir (parents = True ) 31 (self .workspace / "flake.nix" ).write_text ("{ outputs = _: {}; }\n" ) 32os .environ ["ALBEDO_HOME" ]= scratch .name 33nix = shutil .which ("nix" ) 34assert nix is not None 35self .table = albedo_nix .flag_table (nix ) 36patch = mock .patch .object (albedo_nix ,"flake_ref" ,return_value = "REF" ) 37patch .start () 38self .addCleanup (patch .stop ) 39 40def read (self ,line :str )-> albedo_nix .Command : 41return albedo_nix .read (line .split (),self .table ) 42 43def rewrite (self ,line :str )-> str | None : 44command = self .read (line ) 45result = albedo_nix .rewritten (command ,self .workspace ) 46if result is None : 47return None 48lock = str (self .workspace / "flake.lock" ) 49return " " .join (result [0 ]).replace (lock ,"LOCK" ) 50 51def test_the_workspace_lock_is_written_only_for_the_workspace_flake (self ): 52cases = { 53"build" :"build --output-lock-file LOCK REF" , 54"-L build .#x" :"-L build --output-lock-file LOCK REF#x" , 55"run .#x ./arg" :"run --output-lock-file LOCK REF#x ./arg" , 56"flake update nixpkgs" :"flake update --output-lock-file LOCK --flake REF nixpkgs" , 57"profile install .#x" :"profile install --output-lock-file LOCK REF#x" , 58# another flake is locked too: the workspace's lock is left alone 59"build .#a nixpkgs#b" :"build --no-write-lock-file REF#a nixpkgs#b" , 60"shell --inputs-from . nixpkgs#hello -c hello" : ( 61"shell --no-write-lock-file --inputs-from REF nixpkgs#hello -c hello" 62 ), 63"why-depends .#a nixpkgs#b" :"why-depends --no-write-lock-file REF#a nixpkgs#b" , 64 } 65for line ,expected in cases .items (): 66with self .subTest (line ): 67self .assertEqual (self .rewrite (line ),expected ) 68 69def test_commands_that_name_no_local_flake_are_left_alone (self ): 70for line in [ 71"build nixpkgs#hello" , 72"eval --expr 1" , 73"eval" , 74"build -f . hello" , 75"store gc" , 76"path-info --all" , 77"flake show github:owner/repo" , 78 ]: 79with self .subTest (line ): 80self .assertIsNone (self .rewrite (line )) 81 82def test_the_formatter_runs_from_the_commit_at_the_flake_root (self ): 83command = self .read ("fmt --impure src" ) 84result = albedo_nix .rewritten (command ,self .workspace ) 85assert result is not None 86words ,environment = result 87self .assertEqual ( 88words , 89 [ 90"run" , 91"--output-lock-file" , 92str (self .workspace / "flake.lock" ), 93f"REF#formatter. { albedo_nix . system () } " , 94"--impure" , 95"--" , 96"src" , 97 ], 98 ) 99self .assertEqual (environment , {"PRJ_ROOT" :str (self .workspace )}) 100 101def test_committing_a_lock_into_the_main_checkout_is_refused (self ): 102with self .assertRaises (albedo_nix .NixError ): 103self .rewrite ("flake lock --commit-lock-file" ) 104 105def test_unknown_commands_and_flags_go_to_nix_to_refuse (self ): 106for line in ["frobnicate ." ,"build --frobnicate" ]: 107with self .subTest (line ),self .assertRaises (albedo_nix .Unreadable ): 108self .read (line ) 109 110 111class ExportTest (unittest .TestCase ): 112def test_exports_are_the_scripts_own (self ): 113script = ( 114'nix_saved_PATH="$PATH"\n' 115"PATH=''\n" 116"export PATH\n" 117"CC='clang'\n" 118"export CC\n" 119"HOME='/homeless-shelter'\n" # assigned, not exported: the caller keeps theirs 120"declare -a outputs=('out')\n" 121"f ()\n" 122"{\n" 123' export SOURCE_DATE_EPOCH="$time";\n' # a function body: does not run 124"}\n" 125'export NIX_LDFLAGS${role_post}+=" -L$1/lib";\n' 126'export NIX_BUILD_TOP="$(mktemp -d)"\n' 127 ) 128self .assertEqual (albedo_nix .exported (script ), {"CC" ,"NIX_BUILD_TOP" }) 129 130 131class DeltaTest (unittest .TestCase ): 132def test_a_matching_value_is_kept_only_when_the_shell_provides_it (self ): 133change = albedo_nix .delta ( 134 {"CGO_ENABLED" :"1" ,"TERM" :"xterm" }, 135 {"CGO_ENABLED" :"1" ,"TERM" :"xterm" }, 136frozenset ({"CGO_ENABLED" }), 137 ) 138self .assertEqual (change ["set" ], {"CGO_ENABLED" :"1" }) 139 140 141class CaptureTest (unittest .TestCase ): 142SCRIPT = """\ 143nix_saved_PATH="$PATH" 144nix_saved_XDG_DATA_DIRS="$XDG_DATA_DIRS" 145PATH='/shell/bin' 146export PATH 147CGO_ENABLED='1' 148export CGO_ENABLED 149export CGO_LDFLAGS='-L/shell/lib' 150shellHook='export HOOKED=from-hook' 151export shellHook 152PATH="$PATH${nix_saved_PATH:+:$nix_saved_PATH}" 153XDG_DATA_DIRS="$XDG_DATA_DIRS${nix_saved_XDG_DATA_DIRS:+:$nix_saved_XDG_DATA_DIRS}" 154export NIX_BUILD_TOP="$(mktemp -d -t nix-shell.XXXXXX)" 155export TMP="$NIX_BUILD_TOP" 156export TMPDIR="$NIX_BUILD_TOP" 157export TEMP="$NIX_BUILD_TOP" 158export TEMPDIR="$NIX_BUILD_TOP" 159eval "${shellHook:-}" 160""" 161 162FAKE = """#!/bin/sh 163while [ $# -gt 0 ]; do 164if [ "$1" = "--profile" ]; then profile=$2; shift; fi 165shift 166done 167cat > "$profile" <<'JSON' 168{"variables": {}} 169JSON 170cat <<'RC' 171""" 172 173def test_a_capture_inside_the_shell_still_carries_its_exports (self ): 174with tempfile .TemporaryDirectory ()as scratch : 175fake = Path (scratch ,"nix" ) 176fake .write_text (self .FAKE + self .SCRIPT + "RC\n" ) 177fake .chmod (0o755 ) 178with mock .patch .dict (os .environ , {"CGO_ENABLED" :"1" }): 179change = albedo_nix .capture ( 180str (fake ), ["installable" ],Path (scratch ,"profile" ),Path (scratch ) 181 ) 182self .assertEqual (change ["set" ]["CGO_ENABLED" ],"1" ) 183self .assertEqual (change ["set" ]["CGO_LDFLAGS" ],"-L/shell/lib" ) 184self .assertEqual (change ["set" ]["HOOKED" ],"from-hook" ) 185self .assertNotIn ("HOME" ,change ["set" ]) 186self .assertEqual (change ["prepend" ]["PATH" ],"/shell/bin" ) 187 188 189if __name__ == "__main__" : 190unittest .main ()