From 6d29762483e34d730ad76bdc2dc51dd4920f2fa3 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Fri, 30 Jan 2026 16:00:41 -0800 Subject: [PATCH 1/9] feat: Add test harness --- bin/main.ml | 4 ++-- flake.nix | 10 ++++++++-- lib/cli.ml | 14 +++++++++++--- lib/cmd.ml | 3 +++ test/dune | 14 ++++++++++++++ test/ns.ml | 2 ++ test/test.rs | 17 +++++++++++++++++ 7 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 test/dune create mode 100644 test/ns.ml create mode 100644 test/test.rs diff --git a/bin/main.ml b/bin/main.ml index 30d82c3..f2a2594 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -2,7 +2,7 @@ open Ns open Ns.Cmd open Ns.Util -let main ({ installables; target_info } : Cli.strategy) = +let main ({ installables; target_info; printcmd } : Cli.strategy) = let ({ entrypoint; attribute; subshell_dir } : Cli.target_info) = target_info in Option.value ~default:(Option.value ~default:(Sys.getcwd ()) entrypoint) subshell_dir |> Unix.cd; @@ -26,7 +26,7 @@ let main ({ installables; target_info } : Cli.strategy) = |>+ [ "--command"; Unix.shell ] | None -> Cmd.builder "nix" |>+ [ "shell" ] |>+ installables in - ignore (Cmd.run cmd) + if printcmd then print_endline (Cmd.to_string cmd) else ignore (Cmd.run cmd) ;; Cli.eval main diff --git a/flake.nix b/flake.nix index 11ee820..3d3462e 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,7 @@ doCheck = true; nativeCheckInputs = attrValues { inherit (pkgs) ocamlformat; + inherit (pkgs) rustc rustfmt; }; checkPhase = '' red() { @@ -52,7 +53,12 @@ fi if ! dune fmt --diff-command="diff --color=always"; then printf "\n" - printf "$(red "Error:") Some files are not properly formatted\n" + printf "$(red "Error:") Some OCaml files are not properly formatted\n" + exit 1 + fi + if ! rustfmt --check --color=always "${finalAttrs.src}/test/test.rs"; then + printf "\n" + printf "$(red "Error:") test.rs is not properly formatted\n" exit 1 fi ''; @@ -78,7 +84,7 @@ default = pkgs.mkShell { inputsFrom = [ self.packages.${pkgs.stdenv.hostPlatform.system}.default ]; packages = attrValues { - inherit (pkgs) ocamlformat nil; + inherit (pkgs) ocamlformat nil rustc rustfmt; inherit (pkgs.ocamlPackages) ocaml-lsp odoc; }; shellHook = '' diff --git a/lib/cli.ml b/lib/cli.ml index 9229e32..ab0b812 100644 --- a/lib/cli.ml +++ b/lib/cli.ml @@ -17,14 +17,20 @@ module Cli = struct type strategy = { installables : string list ; target_info : target_info + ; printcmd : bool } (** Positional URI arguments passed to the program via the command line. *) let uris = Arg.(value & pos_all dirpath [] & info []) + let printcmd = + let doc = "Print the command to stdout instead of executing it." in + Arg.(value & flag & info [ "printcmd" ] ~doc) + ;; + (** Processes the args so that the main function knows what to do with them. *) let make_strategy = - let build original_args = + let build original_args printcmd = let installables, target_info = let default_installables = [] and default_target = @@ -81,9 +87,9 @@ module Cli = struct , { entrypoint = None; attribute = None; subshell_dir = Some subshell_dir } ) | _ -> Uri.parse_targets_tr original_args, default_target) in - { installables; target_info } + { installables; target_info; printcmd } in - Term.(const build $ uris) + Term.(const build $ uris $ printcmd) ;; let cmd entrypoint = @@ -104,6 +110,8 @@ module Cli = struct ; `P "If a single SOURCE is provided and it is a directory, ns switches into it by \ default unless a TARGET_DIR is explicitly given." + ; `S Manpage.s_options + ; `S "" ; `S Manpage.s_common_options ; `S "" ; `S Manpage.s_examples diff --git a/lib/cmd.ml b/lib/cmd.ml index bd1ef80..828847c 100644 --- a/lib/cmd.ml +++ b/lib/cmd.ml @@ -16,6 +16,9 @@ module Cmd = struct | args -> List.fold_left (fun cmd arg -> Cmd.add_arg cmd arg) cmd args ;; + (** Convert a command into a string *) + let to_string cmd = Cmd.to_string cmd + (** Run a command Example: diff --git a/test/dune b/test/dune new file mode 100644 index 0000000..88b0006 --- /dev/null +++ b/test/dune @@ -0,0 +1,14 @@ +;; Define the test stanza for dune test + +(test + (name ns) + (deps test-ns) + (flags (:standard))) + +;; Rule to compile the Rust test file into a binary + +(rule + (targets test-ns) ; the Dune target name + (deps test.rs) ; path relative to this dune file + (action + (run rustc --test %{deps} -o %{targets}))) diff --git a/test/ns.ml b/test/ns.ml new file mode 100644 index 0000000..bbce5f7 --- /dev/null +++ b/test/ns.ml @@ -0,0 +1,2 @@ +Sys.command "./test-ns --color=always --nocapture" + diff --git a/test/test.rs b/test/test.rs new file mode 100644 index 0000000..b494563 --- /dev/null +++ b/test/test.rs @@ -0,0 +1,17 @@ +use std::env; +use std::process::Command; + +#[test] +fn flake_current_dir() { + let project_root = env::current_dir().expect("failed to get test directory"); + let bin = project_root + .join("../bin/main.exe") + .canonicalize() + .expect("failed to canonicalize path"); + let ns = Command::new(bin) + .args([".", "--printcmd"]) + .status() + .expect("failed to get command status"); + + assert!(ns.success()); +} From 60c5ad7099a2196fed7088187268c413e73903c8 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 31 Jan 2026 00:33:11 -0800 Subject: [PATCH 2/9] add test harness --- flake.nix | 13 ++++++- test/dune | 8 ++-- test/ns.ml | 2 - test/src/flake/flake.nix | 12 ++++++ test/src/shell/shell.nix | 6 +++ test/test.rs | 17 --------- test/test_ns.ml | 9 +++++ test/test_ns.rs | 82 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 124 insertions(+), 25 deletions(-) delete mode 100644 test/ns.ml create mode 100644 test/src/flake/flake.nix create mode 100644 test/src/shell/shell.nix delete mode 100644 test/test.rs create mode 100644 test/test_ns.ml create mode 100644 test/test_ns.rs diff --git a/flake.nix b/flake.nix index 3d3462e..09f50c4 100644 --- a/flake.nix +++ b/flake.nix @@ -42,6 +42,10 @@ inherit (pkgs) ocamlformat; inherit (pkgs) rustc rustfmt; }; + env = { + CLICOLOR_FORCE = 1; + DUNE_DIFF_COMMAND = "diff --color=always"; + }; checkPhase = '' red() { printf "\033[1;31m%s\033[0m" "$*" @@ -51,16 +55,21 @@ printf "$(red "Error:") Generated opam file does not match provided opam file\n" exit 1 fi - if ! dune fmt --diff-command="diff --color=always"; then + if ! dune fmt; then printf "\n" printf "$(red "Error:") Some OCaml files are not properly formatted\n" exit 1 fi - if ! rustfmt --check --color=always "${finalAttrs.src}/test/test.rs"; then + if ! rustfmt --check --color=always "${finalAttrs.src}/test/test_ns.rs"; then printf "\n" printf "$(red "Error:") test.rs is not properly formatted\n" exit 1 fi + if ! dune test --force --no-buffer --display=quiet; then + printf "\n" + printf "$(red "Error:") dune test reported test failures\n" + exit 1 + fi ''; postInstall = '' ${cmdliner}/bin/cmdliner install tool-support $out/bin/ns $out diff --git a/test/dune b/test/dune index 88b0006..dc74b12 100644 --- a/test/dune +++ b/test/dune @@ -1,14 +1,14 @@ ;; Define the test stanza for dune test (test - (name ns) - (deps test-ns) + (name test_ns) + (deps test_ns src/flake/flake.nix src/shell/shell.nix) (flags (:standard))) ;; Rule to compile the Rust test file into a binary (rule - (targets test-ns) ; the Dune target name - (deps test.rs) ; path relative to this dune file + (targets test_ns) ; the Dune target name + (deps test_ns.rs) ; path relative to this dune file (action (run rustc --test %{deps} -o %{targets}))) diff --git a/test/ns.ml b/test/ns.ml deleted file mode 100644 index bbce5f7..0000000 --- a/test/ns.ml +++ /dev/null @@ -1,2 +0,0 @@ -Sys.command "./test-ns --color=always --nocapture" - diff --git a/test/src/flake/flake.nix b/test/src/flake/flake.nix new file mode 100644 index 0000000..2550caa --- /dev/null +++ b/test/src/flake/flake.nix @@ -0,0 +1,12 @@ +{ + inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + outputs = { self, nixpkgs }: + let + inherit (nixpkgs.lib) genAttrs; + inherit (nixpkgs.lib.systems) flakeExposed; + eachSystem = f: genAttrs flakeExposed (system: f (import nixpkgs { inherit system; })); + in + { + packages = eachSystem (pkgs: { default = pkgs.hello; }); + }; +} diff --git a/test/src/shell/shell.nix b/test/src/shell/shell.nix new file mode 100644 index 0000000..e586841 --- /dev/null +++ b/test/src/shell/shell.nix @@ -0,0 +1,6 @@ +{ pkgs ? import { } }: +pkgs.mkShell { + buildInputs = builtins.attrValues { + inherit (pkgs) hello; + }; +} diff --git a/test/test.rs b/test/test.rs deleted file mode 100644 index b494563..0000000 --- a/test/test.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::env; -use std::process::Command; - -#[test] -fn flake_current_dir() { - let project_root = env::current_dir().expect("failed to get test directory"); - let bin = project_root - .join("../bin/main.exe") - .canonicalize() - .expect("failed to canonicalize path"); - let ns = Command::new(bin) - .args([".", "--printcmd"]) - .status() - .expect("failed to get command status"); - - assert!(ns.success()); -} diff --git a/test/test_ns.ml b/test/test_ns.ml new file mode 100644 index 0000000..52406d8 --- /dev/null +++ b/test/test_ns.ml @@ -0,0 +1,9 @@ +let color_mode = + Option.value + ~default:"auto" + (Option.map + (fun force -> if force = "1" then "always" else "never") + (Sys.getenv_opt "CLICOLOR_FORCE")) +and cmd color_mode = Printf.sprintf "./test_ns --color=%s --nocapture" color_mode in +let code = Sys.command (cmd color_mode) in +exit code diff --git a/test/test_ns.rs b/test/test_ns.rs new file mode 100644 index 0000000..8bea959 --- /dev/null +++ b/test/test_ns.rs @@ -0,0 +1,82 @@ +use std::{env, path, process, sync}; + +static DUNE_TEST_DIR: sync::OnceLock = sync::OnceLock::new(); + +fn dune_test_dir() -> &'static path::PathBuf { + DUNE_TEST_DIR.get_or_init(|| env::current_dir().expect("failed to get test directory")) +} +fn ns() -> path::PathBuf { + dune_test_dir() + .join("..") + .join("bin") + .join("main.exe") + .canonicalize() + .expect("failed to canonicalize path to ns binary") +} +fn flake_dir() -> path::PathBuf { + dune_test_dir() + .join("src") + .join("flake") + .canonicalize() + .expect("failed to canonicalize path to ns binary") +} +fn shell_dir() -> path::PathBuf { + dune_test_dir() + .join("src") + .join("shell") + .canonicalize() + .expect("failed to canonicalize path to ns binary") +} + +#[test] +fn single_arg_flake_no_attr() { + let output = process::Command::new(ns()) + .args([flake_dir().as_os_str(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); +} + +#[test] +fn single_arg_shell_no_attr() { + let output = process::Command::new(ns()) + .args([shell_dir().as_os_str(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); +} + +#[test] +fn single_arg_flake_single_attr() { + let flake_uri = format!("{}#default", flake_dir().display()); + let output = process::Command::new(ns()) + .args([flake_uri, "--printcmd".into()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); +} + +#[test] +fn single_arg_shell_single_attr() { + let shell_uri = format!("{}#default", shell_dir().display()); + let output = process::Command::new(ns()) + .args([shell_uri, "--printcmd".into()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); +} + +#[test] +fn single_arg_flake_multi_attr() { + let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let output = process::Command::new(ns()) + .args([flake_uri, "--printcmd".into()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); +} From 19799fdadf0561ed00de36c1797dbc396350032f Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 31 Jan 2026 16:13:48 -0800 Subject: [PATCH 3/9] Add basic tests and utils --- test/test_ns.ml | 7 +- test/test_ns.rs | 248 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 212 insertions(+), 43 deletions(-) diff --git a/test/test_ns.ml b/test/test_ns.ml index 52406d8..db2bd53 100644 --- a/test/test_ns.ml +++ b/test/test_ns.ml @@ -4,6 +4,7 @@ let color_mode = (Option.map (fun force -> if force = "1" then "always" else "never") (Sys.getenv_opt "CLICOLOR_FORCE")) -and cmd color_mode = Printf.sprintf "./test_ns --color=%s --nocapture" color_mode in -let code = Sys.command (cmd color_mode) in -exit code +;; + +let cmd = Printf.sprintf {| sh -c ./test_ns --color=%s --no-capture |} color_mode in +exit (Sys.command cmd) diff --git a/test/test_ns.rs b/test/test_ns.rs index 8bea959..5882b70 100644 --- a/test/test_ns.rs +++ b/test/test_ns.rs @@ -28,55 +28,223 @@ fn shell_dir() -> path::PathBuf { .expect("failed to canonicalize path to ns binary") } -#[test] -fn single_arg_flake_no_attr() { - let output = process::Command::new(ns()) - .args([flake_dir().as_os_str(), "--printcmd".as_ref()]) - .output() - .expect("failed to get command output"); - - assert!(output.status.success()); +fn assert_stdout_eq(stdout: &[u8], expected: &[&str]) { + let stdout = String::from_utf8_lossy(stdout); + let stdout = stdout.replace('\'', ""); + let stdout: Vec<&str> = stdout.split_whitespace().collect(); + + if expected != stdout.as_slice() { + panic!( + "\nExpected: \x1b[32m{:#?}\x1b[0m\nObserved: \x1b[31m{:#?}\x1b[0m", + expected, + stdout.as_slice() + ) + } } -#[test] -fn single_arg_shell_no_attr() { - let output = process::Command::new(ns()) - .args([shell_dir().as_os_str(), "--printcmd".as_ref()]) - .output() - .expect("failed to get command output"); +mod nix_develop_tests { + use super::*; - assert!(output.status.success()); -} + #[test] + fn single_arg_no_attr() { + let output = process::Command::new(ns()) + .args([flake_dir().as_os_str(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn single_arg_single_attr() { + let flake_uri = format!("{}#default", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, "--printcmd"]) + .output() + .expect("failed to get command output"); -#[test] -fn single_arg_flake_single_attr() { - let flake_uri = format!("{}#default", flake_dir().display()); - let output = process::Command::new(ns()) - .args([flake_uri, "--printcmd".into()]) - .output() - .expect("failed to get command output"); + assert!(output.status.success()); + } - assert!(output.status.success()); + #[test] + fn two_args_no_attrs() { + let output = process::Command::new(ns()) + .args([flake_dir().as_os_str(), ".".as_ref(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn two_args_single_attr_lhs() { + let flake_uri = format!("{}#default", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, ".", "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } } -#[test] -fn single_arg_shell_single_attr() { - let shell_uri = format!("{}#default", shell_dir().display()); - let output = process::Command::new(ns()) - .args([shell_uri, "--printcmd".into()]) - .output() - .expect("failed to get command output"); +mod legacy_nix_shell_tests { + use super::*; + + #[test] + fn single_arg_no_attr() { + let output = process::Command::new(ns()) + .args([shell_dir().as_os_str(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); - assert!(output.status.success()); + assert!(output.status.success()); + } + + #[test] + fn single_arg_single_attr() { + let shell_uri = format!("{}#default", shell_dir().display()); + let output = process::Command::new(ns()) + .args([&shell_uri, "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } } -#[test] -fn single_arg_flake_multi_attr() { - let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); - let output = process::Command::new(ns()) - .args([flake_uri, "--printcmd".into()]) - .output() - .expect("failed to get command output"); +mod nix_shell_tests { + use super::*; + + #[test] + fn single_arg_multi_attr() { + let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn two_args_single_attr_rhs() { + let flake_uri = format!("{}#default", flake_dir().display()); + let output = process::Command::new(ns()) + .args([flake_dir().as_os_str(), flake_uri.as_ref(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn two_args_single_attrs() { + let flake_uri = format!("{}#default", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, &flake_uri, "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn two_args_multi_attr_lhs() { + let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, ".", "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn two_args_multi_attr_rhs() { + let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let output = process::Command::new(ns()) + .args([flake_dir().as_os_str(), flake_uri.as_ref(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn two_args_multi_attrs() { + let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, &flake_uri, "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn three_args_single_attrs_lhs() { + let flake_uri = format!("{}#default", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, "github:NixOS/nixpkgs#hello", ".", "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn three_args_single_attrs_rhs() { + let flake_uri = format!("{}#default", flake_dir().display()); + let output = process::Command::new(ns()) + .args([flake_dir().as_os_str(), flake_uri.as_ref(), "github:NixOS/nixpkgs#hello".as_ref(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn three_args_single_attrs() { + let flake_uri = format!("{}#default", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, &flake_uri, "github:NixOS/nixpkgs#hello", "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn three_args_multi_attrs_lhs() { + let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, "github:NixOS/nixpkgs#{hello,cowsay}", ".", "--printcmd"]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn three_args_multi_attrs_rhs() { + let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let output = process::Command::new(ns()) + .args([flake_dir().as_os_str(), flake_uri.as_ref(), "github:NixOS/nixpkgs#{hello,cowsay}".as_ref(), "--printcmd".as_ref()]) + .output() + .expect("failed to get command output"); + + assert!(output.status.success()); + } + + #[test] + fn three_args_multi_attrs() { + let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let output = process::Command::new(ns()) + .args([&flake_uri, &flake_uri, "github:NixOS/nixpkgs#{hello,cowsay}", "--printcmd"]) + .output() + .expect("failed to get command output"); - assert!(output.status.success()); + assert!(output.status.success()); + } } From 418743177b5d3f904861f36028b52a082b95f589 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 31 Jan 2026 16:14:52 -0800 Subject: [PATCH 4/9] rustfmt --- test/test_ns.rs | 51 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/test/test_ns.rs b/test/test_ns.rs index 5882b70..f5f2467 100644 --- a/test/test_ns.rs +++ b/test/test_ns.rs @@ -131,7 +131,11 @@ mod nix_shell_tests { fn two_args_single_attr_rhs() { let flake_uri = format!("{}#default", flake_dir().display()); let output = process::Command::new(ns()) - .args([flake_dir().as_os_str(), flake_uri.as_ref(), "--printcmd".as_ref()]) + .args([ + flake_dir().as_os_str(), + flake_uri.as_ref(), + "--printcmd".as_ref(), + ]) .output() .expect("failed to get command output"); @@ -164,7 +168,11 @@ mod nix_shell_tests { fn two_args_multi_attr_rhs() { let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); let output = process::Command::new(ns()) - .args([flake_dir().as_os_str(), flake_uri.as_ref(), "--printcmd".as_ref()]) + .args([ + flake_dir().as_os_str(), + flake_uri.as_ref(), + "--printcmd".as_ref(), + ]) .output() .expect("failed to get command output"); @@ -181,7 +189,7 @@ mod nix_shell_tests { assert!(output.status.success()); } - + #[test] fn three_args_single_attrs_lhs() { let flake_uri = format!("{}#default", flake_dir().display()); @@ -197,7 +205,12 @@ mod nix_shell_tests { fn three_args_single_attrs_rhs() { let flake_uri = format!("{}#default", flake_dir().display()); let output = process::Command::new(ns()) - .args([flake_dir().as_os_str(), flake_uri.as_ref(), "github:NixOS/nixpkgs#hello".as_ref(), "--printcmd".as_ref()]) + .args([ + flake_dir().as_os_str(), + flake_uri.as_ref(), + "github:NixOS/nixpkgs#hello".as_ref(), + "--printcmd".as_ref(), + ]) .output() .expect("failed to get command output"); @@ -208,7 +221,12 @@ mod nix_shell_tests { fn three_args_single_attrs() { let flake_uri = format!("{}#default", flake_dir().display()); let output = process::Command::new(ns()) - .args([&flake_uri, &flake_uri, "github:NixOS/nixpkgs#hello", "--printcmd"]) + .args([ + &flake_uri, + &flake_uri, + "github:NixOS/nixpkgs#hello", + "--printcmd", + ]) .output() .expect("failed to get command output"); @@ -219,18 +237,28 @@ mod nix_shell_tests { fn three_args_multi_attrs_lhs() { let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); let output = process::Command::new(ns()) - .args([&flake_uri, "github:NixOS/nixpkgs#{hello,cowsay}", ".", "--printcmd"]) + .args([ + &flake_uri, + "github:NixOS/nixpkgs#{hello,cowsay}", + ".", + "--printcmd", + ]) .output() .expect("failed to get command output"); assert!(output.status.success()); } - + #[test] fn three_args_multi_attrs_rhs() { let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); let output = process::Command::new(ns()) - .args([flake_dir().as_os_str(), flake_uri.as_ref(), "github:NixOS/nixpkgs#{hello,cowsay}".as_ref(), "--printcmd".as_ref()]) + .args([ + flake_dir().as_os_str(), + flake_uri.as_ref(), + "github:NixOS/nixpkgs#{hello,cowsay}".as_ref(), + "--printcmd".as_ref(), + ]) .output() .expect("failed to get command output"); @@ -241,7 +269,12 @@ mod nix_shell_tests { fn three_args_multi_attrs() { let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); let output = process::Command::new(ns()) - .args([&flake_uri, &flake_uri, "github:NixOS/nixpkgs#{hello,cowsay}", "--printcmd"]) + .args([ + &flake_uri, + &flake_uri, + "github:NixOS/nixpkgs#{hello,cowsay}", + "--printcmd", + ]) .output() .expect("failed to get command output"); From ba60b95a09c320fb3e7a0b622b4fe637cf387c38 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 31 Jan 2026 17:50:37 -0800 Subject: [PATCH 5/9] finish test assertions --- flake.nix | 25 +++++----- test/dune | 2 +- test/test_ns.rs | 128 ++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 121 insertions(+), 34 deletions(-) diff --git a/flake.nix b/flake.nix index 09f50c4..fcbe5a5 100644 --- a/flake.nix +++ b/flake.nix @@ -40,11 +40,7 @@ doCheck = true; nativeCheckInputs = attrValues { inherit (pkgs) ocamlformat; - inherit (pkgs) rustc rustfmt; - }; - env = { - CLICOLOR_FORCE = 1; - DUNE_DIFF_COMMAND = "diff --color=always"; + inherit (pkgs) rustc clippy rustfmt; }; checkPhase = '' red() { @@ -55,19 +51,24 @@ printf "$(red "Error:") Generated opam file does not match provided opam file\n" exit 1 fi - if ! dune fmt; then + if ! dune test --force --no-buffer --display=quiet; then printf "\n" - printf "$(red "Error:") Some OCaml files are not properly formatted\n" + printf "$(red "Error:") dune test reported test failures\n" exit 1 fi - if ! rustfmt --check --color=always "${finalAttrs.src}/test/test_ns.rs"; then + if ! clippy-driver --test "${finalAttrs.src}/test/test_ns.rs" -Dwarnings; then printf "\n" - printf "$(red "Error:") test.rs is not properly formatted\n" + printf "$(red "Error:") test.rs contains warnings\n" exit 1 fi - if ! dune test --force --no-buffer --display=quiet; then + if ! dune fmt --diff-command="diff --color=always"; then printf "\n" - printf "$(red "Error:") dune test reported test failures\n" + printf "$(red "Error:") Some OCaml files are not properly formatted\n" + exit 1 + fi + if ! rustfmt --check --color=always "${finalAttrs.src}/test/test_ns.rs"; then + printf "\n" + printf "$(red "Error:") test.rs is not properly formatted\n" exit 1 fi ''; @@ -93,7 +94,7 @@ default = pkgs.mkShell { inputsFrom = [ self.packages.${pkgs.stdenv.hostPlatform.system}.default ]; packages = attrValues { - inherit (pkgs) ocamlformat nil rustc rustfmt; + inherit (pkgs) ocamlformat nil rustc clippy rustfmt; inherit (pkgs.ocamlPackages) ocaml-lsp odoc; }; shellHook = '' diff --git a/test/dune b/test/dune index dc74b12..979cd4b 100644 --- a/test/dune +++ b/test/dune @@ -11,4 +11,4 @@ (targets test_ns) ; the Dune target name (deps test_ns.rs) ; path relative to this dune file (action - (run rustc --test %{deps} -o %{targets}))) + (run rustc --test %{deps} -o %{targets} --allow warnings))) diff --git a/test/test_ns.rs b/test/test_ns.rs index f5f2467..fab0b11 100644 --- a/test/test_ns.rs +++ b/test/test_ns.rs @@ -1,10 +1,15 @@ use std::{env, path, process, sync}; static DUNE_TEST_DIR: sync::OnceLock = sync::OnceLock::new(); +static ENV_SHELL: sync::OnceLock = sync::OnceLock::new(); fn dune_test_dir() -> &'static path::PathBuf { DUNE_TEST_DIR.get_or_init(|| env::current_dir().expect("failed to get test directory")) } +fn env_shell() -> &'static String { + ENV_SHELL.get_or_init(|| env::var("SHELL").expect("failed to get shell from environment")) +} + fn ns() -> path::PathBuf { dune_test_dir() .join("..") @@ -53,6 +58,16 @@ mod nix_develop_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &[ + "nix", + "develop", + &flake_dir().to_string_lossy(), + "--command", + &env_shell(), + ], + ); } #[test] @@ -64,6 +79,10 @@ mod nix_develop_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &["nix", "develop", &flake_uri, "--command", &env_shell()], + ); } #[test] @@ -74,6 +93,16 @@ mod nix_develop_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &[ + "nix", + "develop", + &flake_dir().to_string_lossy(), + "--command", + &env_shell(), + ], + ); } #[test] @@ -85,6 +114,10 @@ mod nix_develop_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &["nix", "develop", &flake_uri, "--command", &env_shell()], + ); } } @@ -99,6 +132,15 @@ mod legacy_nix_shell_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &[ + "nix-shell", + &shell_dir().to_string_lossy(), + "--command", + &env_shell(), + ], + ); } #[test] @@ -110,6 +152,17 @@ mod legacy_nix_shell_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &[ + "nix-shell", + "--attr", + "default", + &shell_dir().to_string_lossy(), + "--command", + &env_shell(), + ], + ); } } @@ -125,6 +178,7 @@ mod nix_shell_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri]); } #[test] @@ -140,6 +194,10 @@ mod nix_shell_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &["nix", "shell", &flake_uri, &flake_dir().to_string_lossy()], + ); } #[test] @@ -151,6 +209,7 @@ mod nix_shell_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, &flake_uri]); } #[test] @@ -162,6 +221,7 @@ mod nix_shell_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri]); } #[test] @@ -177,6 +237,10 @@ mod nix_shell_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &["nix", "shell", &flake_uri, &flake_dir().to_string_lossy()], + ); } #[test] @@ -188,96 +252,118 @@ mod nix_shell_tests { .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, &flake_uri]); } #[test] fn three_args_single_attrs_lhs() { let flake_uri = format!("{}#default", flake_dir().display()); + let remote_uri = "github:NixOS/nixpkgs#hello"; let output = process::Command::new(ns()) - .args([&flake_uri, "github:NixOS/nixpkgs#hello", ".", "--printcmd"]) + .args([&flake_uri, remote_uri, ".", "--printcmd"]) .output() .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, remote_uri]); } #[test] fn three_args_single_attrs_rhs() { let flake_uri = format!("{}#default", flake_dir().display()); + let remote_uri = "github:NixOS/nixpkgs#hello"; let output = process::Command::new(ns()) .args([ flake_dir().as_os_str(), flake_uri.as_ref(), - "github:NixOS/nixpkgs#hello".as_ref(), + remote_uri.as_ref(), "--printcmd".as_ref(), ]) .output() .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &[ + "nix", + "shell", + remote_uri, + &flake_uri, + &flake_dir().to_string_lossy(), + ], + ); } #[test] fn three_args_single_attrs() { let flake_uri = format!("{}#default", flake_dir().display()); + let remote_uri = "github:NixOS/nixpkgs#hello"; let output = process::Command::new(ns()) - .args([ - &flake_uri, - &flake_uri, - "github:NixOS/nixpkgs#hello", - "--printcmd", - ]) + .args([&flake_uri, &flake_uri, remote_uri, "--printcmd"]) .output() .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &["nix", "shell", remote_uri, &flake_uri, &flake_uri], + ); } #[test] fn three_args_multi_attrs_lhs() { let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let remote_uri = "github:NixOS/nixpkgs#{hello,cowsay}"; let output = process::Command::new(ns()) - .args([ - &flake_uri, - "github:NixOS/nixpkgs#{hello,cowsay}", - ".", - "--printcmd", - ]) + .args([&flake_uri, remote_uri, ".", "--printcmd"]) .output() .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, remote_uri]); } #[test] fn three_args_multi_attrs_rhs() { let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let remote_uri = "github:NixOS/nixpkgs#{hello,cowsay}"; let output = process::Command::new(ns()) .args([ flake_dir().as_os_str(), flake_uri.as_ref(), - "github:NixOS/nixpkgs#{hello,cowsay}".as_ref(), + remote_uri.as_ref(), "--printcmd".as_ref(), ]) .output() .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &[ + "nix", + "shell", + remote_uri, + &flake_uri, + &flake_dir().to_string_lossy(), + ], + ); } #[test] fn three_args_multi_attrs() { let flake_uri = format!("{}#{{hello,cowsay}}", flake_dir().display()); + let remote_uri = "github:NixOS/nixpkgs#{hello,cowsay}"; let output = process::Command::new(ns()) - .args([ - &flake_uri, - &flake_uri, - "github:NixOS/nixpkgs#{hello,cowsay}", - "--printcmd", - ]) + .args([&flake_uri, &flake_uri, remote_uri, "--printcmd"]) .output() .expect("failed to get command output"); assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &["nix", "shell", remote_uri, &flake_uri, &flake_uri], + ); } } From 5984d2c951624eaea4d0cd546317ecd620fae2ae Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 31 Jan 2026 17:51:45 -0800 Subject: [PATCH 6/9] clippy --- test/test_ns.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_ns.rs b/test/test_ns.rs index fab0b11..11a5dbe 100644 --- a/test/test_ns.rs +++ b/test/test_ns.rs @@ -65,7 +65,7 @@ mod nix_develop_tests { "develop", &flake_dir().to_string_lossy(), "--command", - &env_shell(), + env_shell(), ], ); } @@ -81,7 +81,7 @@ mod nix_develop_tests { assert!(output.status.success()); assert_stdout_eq( &output.stdout, - &["nix", "develop", &flake_uri, "--command", &env_shell()], + &["nix", "develop", &flake_uri, "--command", env_shell()], ); } @@ -100,7 +100,7 @@ mod nix_develop_tests { "develop", &flake_dir().to_string_lossy(), "--command", - &env_shell(), + env_shell(), ], ); } @@ -116,7 +116,7 @@ mod nix_develop_tests { assert!(output.status.success()); assert_stdout_eq( &output.stdout, - &["nix", "develop", &flake_uri, "--command", &env_shell()], + &["nix", "develop", &flake_uri, "--command", env_shell()], ); } } @@ -138,7 +138,7 @@ mod legacy_nix_shell_tests { "nix-shell", &shell_dir().to_string_lossy(), "--command", - &env_shell(), + env_shell(), ], ); } @@ -160,7 +160,7 @@ mod legacy_nix_shell_tests { "default", &shell_dir().to_string_lossy(), "--command", - &env_shell(), + env_shell(), ], ); } From f36c30d189b3e84d01e15579c567ff567cd74933 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 31 Jan 2026 17:59:23 -0800 Subject: [PATCH 7/9] bump to 0.4.1 --- dune-project | 2 +- ns.opam | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dune-project b/dune-project index 7d2b2a5..59731f7 100644 --- a/dune-project +++ b/dune-project @@ -2,7 +2,7 @@ (name ns) -(version 0.4.0) +(version 0.4.1) (generate_opam_files true) diff --git a/ns.opam b/ns.opam index 4dd23b4..e3a0d57 100644 --- a/ns.opam +++ b/ns.opam @@ -1,6 +1,6 @@ # This file is generated by dune, edit dune-project instead opam-version: "2.0" -version: "0.4.0" +version: "0.4.1" synopsis: "A unified interface for nix shell." description: "An intuitive nix shell interface that unifies the `nix shell`, `nix develop` and `nix-shell` commands." From d6fcb614276a30167c795e1e01d48fbc201b2b54 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 31 Jan 2026 18:04:22 -0800 Subject: [PATCH 8/9] add default derivation to flake checks --- flake.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flake.nix b/flake.nix index fcbe5a5..482528a 100644 --- a/flake.nix +++ b/flake.nix @@ -102,6 +102,9 @@ ''; }; }); + checks = eachSystem (pkgs: { + inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) default; + }); formatter = eachSystem (pkgs: pkgs.nixpkgs-fmt); }; } From 5bac466c59a670a7c434cbe4bc4bca1884568f08 Mon Sep 17 00:00:00 2001 From: eureka-cpu Date: Sat, 31 Jan 2026 18:14:51 -0800 Subject: [PATCH 9/9] fix readme description block --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8c975c6..4e7b253 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ The entire interface can be summarized by the following excerpt of the manpage: ns [OPTION]… [SOURCE]… [TARGET_DIR] ``` +>
 > Each SOURCE is positional and may be one of:
 > 
 >   URI#ATTR            select a devshell or package from a flake
@@ -23,6 +24,7 @@ ns [OPTION]… [SOURCE]… [TARGET_DIR]
 > 
 > If a single SOURCE is provided and it is a directory, ns switches into
 > it by default unless a TARGET_DIR is explicitly given.
+> 
Try it from anywhere: