From 32f139ca26dc31a91981b4c076dd483ef9a5af37 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 8 Aug 2026 10:35:30 -0700 Subject: [PATCH 1/4] wip --- .github/workflows/checks.yml | 30 +++++++++++++++++++ flake.lock | 21 ++++++++++++++ flake.nix | 40 +++++++++++++++++++------- tests/default.nix | 9 ++++++ tests/install-script.nix | 56 ++++++++++++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/checks.yml create mode 100644 tests/default.nix create mode 100644 tests/install-script.nix diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..599653f --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,30 @@ +name: checks + +on: + push: + branches: [master] + pull_request: + +jobs: + install-script: + # The VM test needs an x86_64-linux host with KVM; GitHub's Linux runners + # expose /dev/kvm. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Give the current user access to /dev/kvm (present on the runner, but not + # group-accessible by default). + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ + | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - uses: DeterminateSystems/nix-installer-action@main + + # Runs the VM-test driver directly (not a sandboxed build), so it has the + # network apt-get needs. Builds the Ubuntu image + driver first, then boots. + - name: Run install.sh VM test + run: nix run .#install-script-test diff --git a/flake.lock b/flake.lock index 9ee1bb8..5e1099e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,25 @@ { "nodes": { + "nix-vm-test": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1783265394, + "narHash": "sha256-cq4YfNFGYzp0NItZP8tEC7xUI8OSgY4fj75AU/NSaPM=", + "owner": "numtide", + "repo": "nix-vm-test", + "rev": "1a587212d2ac8b669c6c32499015f996506b6ba5", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "nix-vm-test", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1785967620, @@ -18,6 +38,7 @@ }, "root": { "inputs": { + "nix-vm-test": "nix-vm-test", "nixpkgs": "nixpkgs", "treefmt": "treefmt" } diff --git a/flake.nix b/flake.nix index bd30081..d492704 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,10 @@ url = "github:numtide/treefmt-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; + nix-vm-test = { + url = "github:numtide/nix-vm-test"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = { self, nixpkgs, ... }@inputs: @@ -47,13 +51,27 @@ default = pkgs.git-kitten; }); - apps = eachSystem (pkgs: { - default = { - type = "app"; - program = "${pkgs.git-kitten}/bin/git-kitten"; - meta.description = "An opt-in kitty-diff git plugin."; - }; - }); + apps = eachSystem (pkgs: + { + default = { + type = "app"; + program = "${pkgs.git-kitten}/bin/git-kitten"; + meta.description = "An opt-in kitty-diff git plugin."; + }; + } + # The install.sh VM test is a runnable driver (needs an Ubuntu image + + # network + KVM), so it is an app you `nix run`, not a hermetic check. + # nix-vm-test only supports x86_64-linux, so gate it there. + // nixpkgs.lib.optionalAttrs (pkgs.stdenv.buildPlatform.system == "x86_64-linux") { + install-script-test = { + type = "app"; + program = "${import ./tests/install-script.nix { + system = pkgs.stdenv.buildPlatform.system; + nix-vm-test = inputs.nix-vm-test; + }}/bin/test-driver"; + meta.description = "Run install.sh on an Ubuntu VM (needs KVM)."; + }; + }); checks = eachSystem (pkgs: { treefmt-check = @@ -64,10 +82,10 @@ devShells = eachSystem (pkgs: { default = pkgs.mkShell { - inputsFrom = - [ - pkgs.git-kitten - ] ++ builtins.attrValues self.checks.${pkgs.stdenv.buildPlatform.system}; + inputsFrom = [ + pkgs.git-kitten + self.checks.${pkgs.stdenv.buildPlatform.system}.treefmt-check + ]; packages = with pkgs; [ nil ]; }; }); diff --git a/tests/default.nix b/tests/default.nix new file mode 100644 index 0000000..54bdec6 --- /dev/null +++ b/tests/default.nix @@ -0,0 +1,9 @@ +let + # TODO: Add nix-vm-test and nixpkgs +in +{ + install-script-test = import ./tests/install-script.nix { + system = pkgs.stdenv.buildPlatform.system; + nix-vm-test = inputs.nix-vm-test; + }; +} diff --git a/tests/install-script.nix b/tests/install-script.nix new file mode 100644 index 0000000..3f084f9 --- /dev/null +++ b/tests/install-script.nix @@ -0,0 +1,56 @@ +# VM test for install.sh, run on a real Ubuntu cloud image via nix-vm-test. +# +# Returns the `.driver`: an executable (`bin/test-driver`) you *run* (via +# `nix run`), rather than `.sandboxed` which runs the VM as a hermetic nix +# build. The driver runs in the normal environment, so it has the network the +# `apt-get` steps below need and /dev/kvm — no sandbox to poke holes in. It +# boots Ubuntu (whose /bin/sh is dash and whose git is the distro package) and +# asserts the installer end-to-end. x86_64-linux + KVM only (gated in flake.nix). +{ nix-vm-test, system }: +(nix-vm-test.lib.${system}.ubuntu."24_04" { + # Mount the project source (install.sh + src/) read-only. Referenced by a + # relative path rather than the flake's `self` so this stays a plain function + # that does not depend on flake evaluation. + sharedDirs.gitKitten = { + source = "${../.}"; + target = "/mnt/git-kitten"; + }; + + testScript = '' + vm.wait_for_unit("multi-user.target") + vm.succeed("apt-get update") + vm.succeed("apt-get install -y git man-db") + + # Stub `kitten` so the wrapper's `command -v kitten` passes and we can prove + # dispatch actually reaches it (real kitty would pull GL/X deps + need a tty). + # It only needs to print a marker we can grep for. + vm.succeed("printf '#!/bin/sh\\necho KITTEN-RAN\\n' > /usr/local/bin/kitten") + vm.succeed("chmod +x /usr/local/bin/kitten") + + # Run the installer into /usr/local (on the default PATH and MANPATH). + vm.succeed("PREFIX=/usr/local/bin sh /mnt/git-kitten/install.sh") + + # Installer results: binary + man page landed where expected. + vm.succeed("test -x /usr/local/bin/git-kitten") + vm.succeed("test -f /usr/local/share/man/man1/git-kitten.1") + + # git discovers the `kitten` subcommand; short help is our usage(). + vm.succeed("git kitten -h | grep -q 'usage: git kitten diff'") + + # `git kitten --help` -> `man git-kitten` finds the installed page. + vm.succeed("MANPAGER=cat git kitten --help | col -b | grep -q GIT-KITTEN") + + # Real end-to-end on Ubuntu's dash + git: dispatch reaches our kitten stub. + vm.succeed( + "git init /tmp/r && cd /tmp/r && " + "git config user.email t@t && git config user.name t && " + "printf 'a\n' > f && git add . && git commit -m c1" + ) + vm.succeed("cd /tmp/r && printf 'b\n' > f && git kitten diff HEAD | grep -q KITTEN-RAN") + + # Installing to a directory that is not on PATH warns on stderr. + vm.succeed( + "PREFIX=/opt/xyz/bin sh /mnt/git-kitten/install.sh 2>&1 | grep -qi 'not on your PATH'" + ) + ''; +}).driver From 75dc6cd779c3c58b0264782b83a08a9badad8c2b Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 8 Aug 2026 15:29:41 -0700 Subject: [PATCH 2/4] get tests working with stub kitten --- flake.lock | 44 +------------ flake.nix | 56 ++++++----------- nix/tamal/.editorconfig | 8 +++ nix/tamal/.ignore | 2 + nix/tamal/default.nix | 116 ++++++++++++++++++++++++++++++++++ nix/tamal/lock.json | 7 +++ nix/tamal/manifest.kdl | 25 ++++++++ tests/default.nix | 132 +++++++++++++++++++++++++++++++++++++-- tests/install-script.nix | 56 ----------------- 9 files changed, 305 insertions(+), 141 deletions(-) create mode 100644 nix/tamal/.editorconfig create mode 100644 nix/tamal/.ignore create mode 100644 nix/tamal/default.nix create mode 100644 nix/tamal/lock.json create mode 100644 nix/tamal/manifest.kdl delete mode 100644 tests/install-script.nix diff --git a/flake.lock b/flake.lock index 5e1099e..0578d48 100644 --- a/flake.lock +++ b/flake.lock @@ -1,25 +1,5 @@ { "nodes": { - "nix-vm-test": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1783265394, - "narHash": "sha256-cq4YfNFGYzp0NItZP8tEC7xUI8OSgY4fj75AU/NSaPM=", - "owner": "numtide", - "repo": "nix-vm-test", - "rev": "1a587212d2ac8b669c6c32499015f996506b6ba5", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "nix-vm-test", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1785967620, @@ -38,29 +18,7 @@ }, "root": { "inputs": { - "nix-vm-test": "nix-vm-test", - "nixpkgs": "nixpkgs", - "treefmt": "treefmt" - } - }, - "treefmt": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1785945821, - "narHash": "sha256-NLSyTCW4K4ofhNBllt3omPasm6QpralXH1DBZOc91Dw=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "ae7910970dddc408fe6ab1c8e4b277bb21d72dc0", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index d492704..c770909 100644 --- a/flake.nix +++ b/flake.nix @@ -1,18 +1,9 @@ { - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - treefmt = { - url = "github:numtide/treefmt-nix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - nix-vm-test = { - url = "github:numtide/nix-vm-test"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - }; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - outputs = { self, nixpkgs, ... }@inputs: + outputs = { self, nixpkgs }: let + inputs = import ./nix/tamal { bootstrap-nixpkgs = nixpkgs; }; overlays.default = import ./overlay.nix; eachSystem = f: nixpkgs.lib.genAttrs [ @@ -31,16 +22,19 @@ overlays = [ overlays.default ]; })); + treefmt = import inputs.treefmt; fmtOpts = { projectRootFile = "flake.lock"; programs = { nixpkgs-fmt.enable = true; shfmt.enable = true; + kdlfmt.enable = true; mdformat.enable = true; }; - # The shfmt module only exposes indent_size/simplify, so append -ci - # (indent switch-case bodies, i.e. the `*)` patterns) to its args. - settings.formatter.shfmt.options = [ "-ci" ]; + settings.formatter = { + nixpkgs-fmt.excludes = [ "nix/tamal/*.nix" ]; + shfmt.options = [ "-ci" ]; + }; }; in { @@ -51,34 +45,20 @@ default = pkgs.git-kitten; }); - apps = eachSystem (pkgs: - { - default = { - type = "app"; - program = "${pkgs.git-kitten}/bin/git-kitten"; - meta.description = "An opt-in kitty-diff git plugin."; - }; - } - # The install.sh VM test is a runnable driver (needs an Ubuntu image + - # network + KVM), so it is an app you `nix run`, not a hermetic check. - # nix-vm-test only supports x86_64-linux, so gate it there. - // nixpkgs.lib.optionalAttrs (pkgs.stdenv.buildPlatform.system == "x86_64-linux") { - install-script-test = { - type = "app"; - program = "${import ./tests/install-script.nix { - system = pkgs.stdenv.buildPlatform.system; - nix-vm-test = inputs.nix-vm-test; - }}/bin/test-driver"; - meta.description = "Run install.sh on an Ubuntu VM (needs KVM)."; - }; - }); + apps = eachSystem (pkgs: { + default = { + type = "app"; + program = "${pkgs.git-kitten}/bin/git-kitten"; + meta.description = "An opt-in kitty-diff git plugin."; + }; + }); checks = eachSystem (pkgs: { treefmt-check = - ((import inputs.treefmt).evalModule pkgs fmtOpts).config.build.check ./.; + (treefmt.evalModule pkgs fmtOpts).config.build.check ./.; }); - formatter = eachSystem (pkgs: (inputs.treefmt.lib).mkWrapper pkgs fmtOpts); + formatter = eachSystem (pkgs: treefmt.mkWrapper pkgs fmtOpts); devShells = eachSystem (pkgs: { default = pkgs.mkShell { diff --git a/nix/tamal/.editorconfig b/nix/tamal/.editorconfig new file mode 100644 index 0000000..d029f52 --- /dev/null +++ b/nix/tamal/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/nix/tamal/.ignore b/nix/tamal/.ignore new file mode 100644 index 0000000..d3dc30a --- /dev/null +++ b/nix/tamal/.ignore @@ -0,0 +1,2 @@ +darcs_context +.silo diff --git a/nix/tamal/default.nix b/nix/tamal/default.nix new file mode 100644 index 0000000..ac9d9e9 --- /dev/null +++ b/nix/tamal/default.nix @@ -0,0 +1,116 @@ +/* +SPDX-FileCopyrightText: 2025–2026 toastal +SPDX-FileCopyrightText: 2026 Nixtamal contributors +SPDX-License-Identifier: ISC + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice & this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED “AS IS” & ISC DISCLAIMS ALL WARRANTIES WITH REGARD +TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY & +FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, +OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +OF THIS SOFTWARE. + +────────────────────────────────────────────────────────────────────────────── +┏┓╻+╻ ╱┏┳┓┏┓┏┳┓┏┓╻ +┃┃┃┃┗━┓╹┃╹┣┫┃┃┃┣┫┃ This file was generated by Nixtamal. +╹┗┛╹╱ ╹ ╹ ╹╹╹ ╹╹╹┗┛ Do not edit as it will be overwritten. +────────────────────────────────────────────────────────────────────────────── +*/ +{ + system ? builtins.currentSystem, + bootstrap-nixpkgs ? null, + bootstrap-nixpkgs-lock-name ? null, +}: + +let lock = builtins.fromJSON (builtins.readFile ./lock.json); in +assert (lock.v == "1.0.0"); +let + hash-token = { + "0" = "sha256"; + "1" = "sha512"; + "2" = "blake3"; + }; + + try-fetch = name: fetcher: + let + try-fetch' = failed-urls: url: urls: + let result = builtins.tryEval (fetcher url); in + if result.success then + result.value + else + let failed-urls' = [ url ] ++ failed-urls; in + if builtins.length urls <= 0 then + let fus = builtins.concatStringsSep " " failed-urls'; in + throw "Input 「${name}」fetchable @ [ ${fus} ]" + else + try-fetch' failed-urls' (builtins.head urls) (builtins.tail urls); + in + try-fetch' [ ]; + + builtin-fetch-tarball = {name, kind, hash}: + try-fetch name (url: + builtins.fetchTarball { + inherit url; + ${hash-token.${builtins.toString hash.al}} = hash.vl; + } + ) kind.ur kind.ms; + + builtin-to-input = name: input: + let k = builtins.head input.kd; in + if k == 1 then + builtin-fetch-tarball { + inherit name; + kind = builtins.elemAt input.kd 1; + hash = input.ha; + } + else + throw "Unsupported input kind “${builtins.toString k}”."; + + nixpkgs' = + if builtins.isNull bootstrap-nixpkgs then + builtin-to-input "nixpkgs-for-nixtamal" ( + if builtins.isString bootstrap-nixpkgs-lock-name then + lock.i.${bootstrap-nixpkgs-lock-name} + else + lock.i.nixpkgs-nixtamal or lock.i.nixpkgs + ) + else + bootstrap-nixpkgs; + + pkgs = import nixpkgs' {inherit system;}; + + inherit (pkgs) lib; + + fetch-zip = {name, kind, hash}: pkgs.fetchzip { + inherit name; + url = kind.ur; + hash = hash.vl; + } // lib.optionalAttrs (builtins.length kind.ms > 0) { urls = kind.ms; }; + + to-input = name: input: + let + k = builtins.head input.kd; + raw-input = + if k == 1 then + let + kind = builtins.elemAt input.kd 1; + fetch_time = kind.ft; + hash = input.ha; + in + if fetch_time == 0 then + fetch-zip {inherit name kind hash;} + else if fetch_time == 1 then + builtin-fetch-tarball {inherit name kind hash;} + else + throw "Unsupported fetch time ${fetch_time}." + else + throw "Unsupported input kind “${builtins.toString}”."; + in + raw-input; +in +builtins.mapAttrs to-input lock.i diff --git a/nix/tamal/lock.json b/nix/tamal/lock.json new file mode 100644 index 0000000..c60f8d4 --- /dev/null +++ b/nix/tamal/lock.json @@ -0,0 +1,7 @@ +{"v":"1.0.0" +,"i":{ +"nix-vm-test":{"kd":[1,{"ft":1,"ur":"https://github.com/numtide/nix-vm-test/archive/1a587212d2ac8b669c6c32499015f996506b6ba5.tar.gz","ms":[]}],"ha":{"al":0,"vl":"sha256-cq4YfNFGYzp0NItZP8tEC7xUI8OSgY4fj75AU/NSaPM="},"fv":"1a587212d2ac8b669c6c32499015f996506b6ba5","ps":[]} +,"treefmt":{"kd":[1,{"ft":1,"ur":"https://github.com/numtide/treefmt-nix/archive/ae7910970dddc408fe6ab1c8e4b277bb21d72dc0.tar.gz","ms":[]}],"ha":{"al":0,"vl":"sha256-NLSyTCW4K4ofhNBllt3omPasm6QpralXH1DBZOc91Dw="},"fv":"ae7910970dddc408fe6ab1c8e4b277bb21d72dc0","ps":[]} +} +,"p":{} +} diff --git a/nix/tamal/manifest.kdl b/nix/tamal/manifest.kdl new file mode 100644 index 0000000..4fdbd37 --- /dev/null +++ b/nix/tamal/manifest.kdl @@ -0,0 +1,25 @@ +// ┏┓╻+╻ ╱┏┳┓┏┓┏┳┓┏┓╻ +// ┃┃┃┃┗━┓╹┃╹┣┫┃┃┃┣┫┃ Read the manpage: +// ╹┗┛╹╱ ╹ ╹ ╹╹╹ ╹╹╹┗┛ $ man nixtamal-manifest +version "1.0.0" +default-fetch-time eval +inputs { + treefmt { + archive { + url "https://github.com/numtide/treefmt-nix/archive/{{fresh_value}}.tar.gz" + } + fresh-cmd { + $ git ls-remote --heads "https://github.com/numtide/treefmt-nix.git" --refs "refs/heads/main" + | cut -f1 + } + } + nix-vm-test { + archive { + url "https://github.com/numtide/nix-vm-test/archive/{{fresh_value}}.tar.gz" + } + fresh-cmd { + $ git ls-remote --heads "https://github.com/numtide/nix-vm-test.git" --refs "refs/heads/main" + | cut -f1 + } + } +} diff --git a/tests/default.nix b/tests/default.nix index 54bdec6..a8f3723 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -1,9 +1,133 @@ let - # TODO: Add nix-vm-test and nixpkgs + /** + Get a node from the flake lock file. + + Type: getFlake' :: String -> Attrset + + :::example getFlake' "nixpkgs" + */ + getFlake' = node: + let source = (builtins.fromJSON (builtins.readFile ../flake.lock)).nodes.${node}.locked; in + { + inherit (source) rev; + outPath = fetchTarball + (let inherit (source) owner repo rev narHash; in { + url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; + sha256 = narHash; + }); + }; +in +{ nixpkgs ? getFlake' "nixpkgs" }: +let + inputs = import ../nix/tamal { bootstrap-nixpkgs = nixpkgs; }; + pkgs = import nixpkgs { + overlays = [ (import "${inputs.nix-vm-test}/overlay.nix") ]; + }; in { - install-script-test = import ./tests/install-script.nix { - system = pkgs.stdenv.buildPlatform.system; - nix-vm-test = inputs.nix-vm-test; + install-script-test = pkgs.testers.nonNixOSDistros.ubuntu."24_04" { + # Mount the project source (install.sh + src/) read-only. Referenced by a + # relative path rather than the flake's `self` so this stays a plain function + # that does not depend on flake evaluation. + sharedDirs.gitKitten = { + source = "${../.}"; + target = "/mnt/git-kitten"; + }; + + testScript = '' + vm.wait_for_unit("multi-user.target") + + # git and man are already present on the base image; nothing to apt-get. + # Run the installer into /usr/local (on the default PATH and MANPATH). + vm.succeed("PREFIX=/usr/local/bin sh /mnt/git-kitten/install.sh") + + # Installer results: binary + man page landed where expected. + vm.succeed("test -x /usr/local/bin/git-kitten") + vm.succeed("test -f /usr/local/share/man/man1/git-kitten.1") + + # Without `kitten` (kitty) on PATH, git-kitten fails fast with exit 127 + # -- the `command -v kitten` guard runs before any argument parsing, so + # this must be checked before the stub below is created. 127 is the + # meaningful signal here (a missing dependency, distinct from the usage + # errors below which all use exit 2), so check the status explicitly + # via `execute` rather than the plain "did it fail" of `vm.fail`. + status, out = vm.execute("git kitten -h 2>&1") + assert status == 127, f"expected exit 127, got {status}:\n{out}" + assert "'kitten' not found" in out, f"expected 'kitten' not found message:\n{out}" + + # Stub `kitten` (real kitty needs a GPU/terminal we don't have here) that + # records the two dir-diff directories git difftool hands it, so we can + # later prove git-kitten materializes the *correct* diff content, not + # just that dispatch reaches some binary. + vm.succeed( + "printf '#!/bin/sh\\n" + "echo KITTEN-RAN\\n" + "test $1 = diff && cp $2/f /tmp/kitten-local-f 2>/dev/null\\n" + "test $1 = diff && cp $3/f /tmp/kitten-remote-f 2>/dev/null\\n" + "exit 0\\n" + "' > /usr/local/bin/kitten" + ) + vm.succeed("chmod +x /usr/local/bin/kitten") + + # Help: short usage (bare and -h), long help via the installed man page, + # and `diff -h` showing our own usage rather than forwarding to `git + # difftool`'s help. + out = vm.succeed("git kitten -h") + assert "usage: git kitten diff" in out, f"expected usage text:\n{out}" + + out = vm.succeed("git kitten") + assert "usage: git kitten diff" in out, f"expected usage text:\n{out}" + + out = vm.succeed("git kitten diff -h") + assert "usage: git kitten diff" in out, f"expected usage text:\n{out}" + + out = vm.succeed("MANPAGER=cat git kitten --help | col -b") + assert "GIT-KITTEN" in out, f"expected man page title:\n{out}" + assert "SYNOPSIS" in out, f"expected man page SYNOPSIS section:\n{out}" + assert "EXAMPLES" in out, f"expected man page EXAMPLES section:\n{out}" + + # Real end-to-end on Ubuntu's dash + git. + vm.succeed( + "git init /tmp/r && cd /tmp/r && " + "git config user.email t@t && git config user.name t && " + "printf 'a\n' > f && git add . && git commit -m c1" + ) + + # Comparing a commit to itself is a no-op, not an error. + out = vm.succeed("cd /tmp/r && git kitten diff HEAD HEAD 2>&1") + assert "no differences between the given inputs" in out, f"expected no-differences message:\n{out}" + + # An actual change: dispatch reaches `kitten diff ` with + # the real old/new contents, proving the --dir-diff wiring works end to + # end, not just that dispatch reaches the binary. + vm.succeed("cd /tmp/r && printf 'b\n' > f") + + out = vm.succeed("cd /tmp/r && git kitten diff HEAD") + assert "KITTEN-RAN" in out, f"expected dispatch to reach the kitten stub:\n{out}" + + local_content = vm.succeed("cat /tmp/kitten-local-f").strip() + assert local_content == "a", f"expected LOCAL dir-diff content 'a', got {local_content!r}" + + remote_content = vm.succeed("cat /tmp/kitten-remote-f").strip() + assert remote_content == "b", f"expected REMOTE dir-diff content 'b', got {remote_content!r}" + + # Failure cases. All three are usage errors (exit 2); the message is + # what distinguishes them, so `vm.fail` (the counterpart to `succeed`) + # is enough -- no need to pin the exact status like the 127 case above. + out = vm.fail("cd / && git kitten diff 2>&1") + assert "not inside a git repository" in out, f"expected not-a-repo message:\n{out}" + + out = vm.fail("git kitten frobnicate 2>&1") + assert "unknown subcommand 'frobnicate'" in out, f"expected unknown-subcommand message:\n{out}" + + vm.succeed("cd /tmp/r && touch x y") + out = vm.fail("cd /tmp/r && git kitten diff x y 2>&1") + assert "look like files, not git revisions" in out, f"expected two-plain-files message:\n{out}" + + # Installing to a directory that is not on PATH warns on stderr. + out = vm.succeed("PREFIX=/opt/xyz/bin sh /mnt/git-kitten/install.sh 2>&1") + assert "not on your path" in out.lower(), f"expected PATH warning:\n{out}" + ''; }; } + diff --git a/tests/install-script.nix b/tests/install-script.nix deleted file mode 100644 index 3f084f9..0000000 --- a/tests/install-script.nix +++ /dev/null @@ -1,56 +0,0 @@ -# VM test for install.sh, run on a real Ubuntu cloud image via nix-vm-test. -# -# Returns the `.driver`: an executable (`bin/test-driver`) you *run* (via -# `nix run`), rather than `.sandboxed` which runs the VM as a hermetic nix -# build. The driver runs in the normal environment, so it has the network the -# `apt-get` steps below need and /dev/kvm — no sandbox to poke holes in. It -# boots Ubuntu (whose /bin/sh is dash and whose git is the distro package) and -# asserts the installer end-to-end. x86_64-linux + KVM only (gated in flake.nix). -{ nix-vm-test, system }: -(nix-vm-test.lib.${system}.ubuntu."24_04" { - # Mount the project source (install.sh + src/) read-only. Referenced by a - # relative path rather than the flake's `self` so this stays a plain function - # that does not depend on flake evaluation. - sharedDirs.gitKitten = { - source = "${../.}"; - target = "/mnt/git-kitten"; - }; - - testScript = '' - vm.wait_for_unit("multi-user.target") - vm.succeed("apt-get update") - vm.succeed("apt-get install -y git man-db") - - # Stub `kitten` so the wrapper's `command -v kitten` passes and we can prove - # dispatch actually reaches it (real kitty would pull GL/X deps + need a tty). - # It only needs to print a marker we can grep for. - vm.succeed("printf '#!/bin/sh\\necho KITTEN-RAN\\n' > /usr/local/bin/kitten") - vm.succeed("chmod +x /usr/local/bin/kitten") - - # Run the installer into /usr/local (on the default PATH and MANPATH). - vm.succeed("PREFIX=/usr/local/bin sh /mnt/git-kitten/install.sh") - - # Installer results: binary + man page landed where expected. - vm.succeed("test -x /usr/local/bin/git-kitten") - vm.succeed("test -f /usr/local/share/man/man1/git-kitten.1") - - # git discovers the `kitten` subcommand; short help is our usage(). - vm.succeed("git kitten -h | grep -q 'usage: git kitten diff'") - - # `git kitten --help` -> `man git-kitten` finds the installed page. - vm.succeed("MANPAGER=cat git kitten --help | col -b | grep -q GIT-KITTEN") - - # Real end-to-end on Ubuntu's dash + git: dispatch reaches our kitten stub. - vm.succeed( - "git init /tmp/r && cd /tmp/r && " - "git config user.email t@t && git config user.name t && " - "printf 'a\n' > f && git add . && git commit -m c1" - ) - vm.succeed("cd /tmp/r && printf 'b\n' > f && git kitten diff HEAD | grep -q KITTEN-RAN") - - # Installing to a directory that is not on PATH warns on stderr. - vm.succeed( - "PREFIX=/opt/xyz/bin sh /mnt/git-kitten/install.sh 2>&1 | grep -qi 'not on your PATH'" - ) - ''; -}).driver From 1147306cc8b97ebc0dd41cab31c2800b4139c907 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 8 Aug 2026 17:41:13 -0700 Subject: [PATCH 3/4] add yamlfmt, add checks to gh actions --- .github/workflows/checks.yml | 32 +++++-------- flake.nix | 1 + tests/default.nix | 92 ++++++++++++------------------------ 3 files changed, 41 insertions(+), 84 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 599653f..18d8535 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,30 +1,20 @@ -name: checks - +name: Checks on: push: branches: [master] pull_request: - jobs: install-script: - # The VM test needs an x86_64-linux host with KVM; GitHub's Linux runners - # expose /dev/kvm. runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - # Give the current user access to /dev/kvm (present on the runner, but not - # group-accessible by default). - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ - | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - uses: DeterminateSystems/nix-installer-action@main - - # Runs the VM-test driver directly (not a sandboxed build), so it has the - # network apt-get needs. Builds the Ubuntu image + driver first, then boots. + - uses: actions/checkout@v7 + - uses: cachix/install-nix-action@v31 - name: Run install.sh VM test - run: nix run .#install-script-test + run: nix-build ./tests -A install-script-test.sandboxed + flake-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: cachix/install-nix-action@v31 + - name: Run nix flake check + run: nix flake check --keep-going diff --git a/flake.nix b/flake.nix index c770909..fcf94e6 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,7 @@ nixpkgs-fmt.enable = true; shfmt.enable = true; kdlfmt.enable = true; + yamlfmt.enable = true; mdformat.enable = true; }; settings.formatter = { diff --git a/tests/default.nix b/tests/default.nix index a8f3723..8c978d6 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -1,21 +1,19 @@ let - /** - Get a node from the flake lock file. + /** Get a node from the flake lock file. - Type: getFlake' :: String -> Attrset - - :::example getFlake' "nixpkgs" - */ + getFlake' :: String -> Attrset */ getFlake' = node: let source = (builtins.fromJSON (builtins.readFile ../flake.lock)).nodes.${node}.locked; in - { - inherit (source) rev; - outPath = fetchTarball - (let inherit (source) owner repo rev narHash; in { + { + inherit (source) rev; + outPath = fetchTarball + ( + let inherit (source) owner repo rev narHash; in { url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; sha256 = narHash; - }); - }; + } + ); + }; in { nixpkgs ? getFlake' "nixpkgs" }: let @@ -26,52 +24,40 @@ let in { install-script-test = pkgs.testers.nonNixOSDistros.ubuntu."24_04" { - # Mount the project source (install.sh + src/) read-only. Referenced by a - # relative path rather than the flake's `self` so this stays a plain function - # that does not depend on flake evaluation. + # Mount the project source. sharedDirs.gitKitten = { source = "${../.}"; target = "/mnt/git-kitten"; }; - + sharedDirs.kittenPkg = { + source = "${pkgs.kitty.kitten}"; + target = "/mnt/kitten-pkg"; + }; + testScript = '' vm.wait_for_unit("multi-user.target") - # git and man are already present on the base image; nothing to apt-get. # Run the installer into /usr/local (on the default PATH and MANPATH). vm.succeed("PREFIX=/usr/local/bin sh /mnt/git-kitten/install.sh") + # Installing to a directory that is not on PATH warns on stderr. + out = vm.succeed("PREFIX=/opt/xyz/bin sh /mnt/git-kitten/install.sh 2>&1") + assert "not on your path" in out.lower(), f"expected PATH warning:\n{out}" # Installer results: binary + man page landed where expected. vm.succeed("test -x /usr/local/bin/git-kitten") vm.succeed("test -f /usr/local/share/man/man1/git-kitten.1") - # Without `kitten` (kitty) on PATH, git-kitten fails fast with exit 127 - # -- the `command -v kitten` guard runs before any argument parsing, so - # this must be checked before the stub below is created. 127 is the - # meaningful signal here (a missing dependency, distinct from the usage - # errors below which all use exit 2), so check the status explicitly - # via `execute` rather than the plain "did it fail" of `vm.fail`. + # Without `kitten` on PATH, git-kitten fails fast with exit 127. + # The `command -v kitten` guard runs before any argument parsing, so + # this must be checked before the real binary below is installed. status, out = vm.execute("git kitten -h 2>&1") assert status == 127, f"expected exit 127, got {status}:\n{out}" assert "'kitten' not found" in out, f"expected 'kitten' not found message:\n{out}" - # Stub `kitten` (real kitty needs a GPU/terminal we don't have here) that - # records the two dir-diff directories git difftool hands it, so we can - # later prove git-kitten materializes the *correct* diff content, not - # just that dispatch reaches some binary. - vm.succeed( - "printf '#!/bin/sh\\n" - "echo KITTEN-RAN\\n" - "test $1 = diff && cp $2/f /tmp/kitten-local-f 2>/dev/null\\n" - "test $1 = diff && cp $3/f /tmp/kitten-remote-f 2>/dev/null\\n" - "exit 0\\n" - "' > /usr/local/bin/kitten" - ) - vm.succeed("chmod +x /usr/local/bin/kitten") + # Symlink the kitten binary from the shared directory mount. + vm.succeed("ln -s /mnt/kitten-pkg/bin/kitten /usr/local/bin/kitten") - # Help: short usage (bare and -h), long help via the installed man page, - # and `diff -h` showing our own usage rather than forwarding to `git - # difftool`'s help. + # Check help output and man pages work correctly. out = vm.succeed("git kitten -h") assert "usage: git kitten diff" in out, f"expected usage text:\n{out}" @@ -86,34 +72,18 @@ in assert "SYNOPSIS" in out, f"expected man page SYNOPSIS section:\n{out}" assert "EXAMPLES" in out, f"expected man page EXAMPLES section:\n{out}" - # Real end-to-end on Ubuntu's dash + git. + # Use Ubuntu's pre-installed dash and git. vm.succeed( - "git init /tmp/r && cd /tmp/r && " + "git init -q /tmp/r && cd /tmp/r && " "git config user.email t@t && git config user.name t && " "printf 'a\n' > f && git add . && git commit -m c1" ) - # Comparing a commit to itself is a no-op, not an error. + # Check no-op output when there is no diff. out = vm.succeed("cd /tmp/r && git kitten diff HEAD HEAD 2>&1") assert "no differences between the given inputs" in out, f"expected no-differences message:\n{out}" - # An actual change: dispatch reaches `kitten diff ` with - # the real old/new contents, proving the --dir-diff wiring works end to - # end, not just that dispatch reaches the binary. - vm.succeed("cd /tmp/r && printf 'b\n' > f") - - out = vm.succeed("cd /tmp/r && git kitten diff HEAD") - assert "KITTEN-RAN" in out, f"expected dispatch to reach the kitten stub:\n{out}" - - local_content = vm.succeed("cat /tmp/kitten-local-f").strip() - assert local_content == "a", f"expected LOCAL dir-diff content 'a', got {local_content!r}" - - remote_content = vm.succeed("cat /tmp/kitten-remote-f").strip() - assert remote_content == "b", f"expected REMOTE dir-diff content 'b', got {remote_content!r}" - - # Failure cases. All three are usage errors (exit 2); the message is - # what distinguishes them, so `vm.fail` (the counterpart to `succeed`) - # is enough -- no need to pin the exact status like the 127 case above. + # Failure cases. All three are usage errors with exit 2. out = vm.fail("cd / && git kitten diff 2>&1") assert "not inside a git repository" in out, f"expected not-a-repo message:\n{out}" @@ -123,10 +93,6 @@ in vm.succeed("cd /tmp/r && touch x y") out = vm.fail("cd /tmp/r && git kitten diff x y 2>&1") assert "look like files, not git revisions" in out, f"expected two-plain-files message:\n{out}" - - # Installing to a directory that is not on PATH warns on stderr. - out = vm.succeed("PREFIX=/opt/xyz/bin sh /mnt/git-kitten/install.sh 2>&1") - assert "not on your path" in out.lower(), f"expected PATH warning:\n{out}" ''; }; } From 115fd0bd9667a468ab2a10fcf2135d12fe804b83 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 8 Aug 2026 17:58:49 -0700 Subject: [PATCH 4/4] rm unused files, fix formatting --- flake.nix | 3 +-- nix/tamal/.editorconfig | 8 -------- nix/tamal/.ignore | 2 -- nix/tamal/manifest.kdl | 36 ++++++++++++++++++------------------ 4 files changed, 19 insertions(+), 30 deletions(-) delete mode 100644 nix/tamal/.editorconfig delete mode 100644 nix/tamal/.ignore diff --git a/flake.nix b/flake.nix index fcf94e6..691e293 100644 --- a/flake.nix +++ b/flake.nix @@ -65,8 +65,7 @@ default = pkgs.mkShell { inputsFrom = [ pkgs.git-kitten - self.checks.${pkgs.stdenv.buildPlatform.system}.treefmt-check - ]; + ] ++ builtins.attrValues self.checks.${pkgs.stdenv.buildPlatform.system}; packages = with pkgs; [ nil ]; }; }); diff --git a/nix/tamal/.editorconfig b/nix/tamal/.editorconfig deleted file mode 100644 index d029f52..0000000 --- a/nix/tamal/.editorconfig +++ /dev/null @@ -1,8 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_style = tab -insert_final_newline = true -trim_trailing_whitespace = true diff --git a/nix/tamal/.ignore b/nix/tamal/.ignore deleted file mode 100644 index d3dc30a..0000000 --- a/nix/tamal/.ignore +++ /dev/null @@ -1,2 +0,0 @@ -darcs_context -.silo diff --git a/nix/tamal/manifest.kdl b/nix/tamal/manifest.kdl index 4fdbd37..33592ce 100644 --- a/nix/tamal/manifest.kdl +++ b/nix/tamal/manifest.kdl @@ -4,22 +4,22 @@ version "1.0.0" default-fetch-time eval inputs { - treefmt { - archive { - url "https://github.com/numtide/treefmt-nix/archive/{{fresh_value}}.tar.gz" - } - fresh-cmd { - $ git ls-remote --heads "https://github.com/numtide/treefmt-nix.git" --refs "refs/heads/main" - | cut -f1 - } - } - nix-vm-test { - archive { - url "https://github.com/numtide/nix-vm-test/archive/{{fresh_value}}.tar.gz" - } - fresh-cmd { - $ git ls-remote --heads "https://github.com/numtide/nix-vm-test.git" --refs "refs/heads/main" - | cut -f1 - } - } + treefmt { + archive { + url "https://github.com/numtide/treefmt-nix/archive/{{fresh_value}}.tar.gz" + } + fresh-cmd { + $ git ls-remote --heads "https://github.com/numtide/treefmt-nix.git" --refs "refs/heads/main" + | cut -f1 + } + } + nix-vm-test { + archive { + url "https://github.com/numtide/nix-vm-test/archive/{{fresh_value}}.tar.gz" + } + fresh-cmd { + $ git ls-remote --heads "https://github.com/numtide/nix-vm-test.git" --refs "refs/heads/main" + | cut -f1 + } + } }