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: 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/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/flake.nix b/flake.nix index 11ee820..482528a 100644 --- a/flake.nix +++ b/flake.nix @@ -40,6 +40,7 @@ doCheck = true; nativeCheckInputs = attrValues { inherit (pkgs) ocamlformat; + inherit (pkgs) rustc clippy rustfmt; }; checkPhase = '' red() { @@ -50,9 +51,24 @@ printf "$(red "Error:") Generated opam file does not match provided opam file\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 + if ! clippy-driver --test "${finalAttrs.src}/test/test_ns.rs" -Dwarnings; then + printf "\n" + printf "$(red "Error:") test.rs contains warnings\n" + exit 1 + 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_ns.rs"; then + printf "\n" + printf "$(red "Error:") test.rs is not properly formatted\n" exit 1 fi ''; @@ -78,7 +94,7 @@ default = pkgs.mkShell { inputsFrom = [ self.packages.${pkgs.stdenv.hostPlatform.system}.default ]; packages = attrValues { - inherit (pkgs) ocamlformat nil; + inherit (pkgs) ocamlformat nil rustc clippy rustfmt; inherit (pkgs.ocamlPackages) ocaml-lsp odoc; }; shellHook = '' @@ -86,6 +102,9 @@ ''; }; }); + checks = eachSystem (pkgs: { + inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) default; + }); formatter = eachSystem (pkgs: pkgs.nixpkgs-fmt); }; } 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/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." diff --git a/test/dune b/test/dune new file mode 100644 index 0000000..979cd4b --- /dev/null +++ b/test/dune @@ -0,0 +1,14 @@ +;; Define the test stanza for dune test + +(test + (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_ns.rs) ; path relative to this dune file + (action + (run rustc --test %{deps} -o %{targets} --allow warnings))) 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_ns.ml b/test/test_ns.ml new file mode 100644 index 0000000..db2bd53 --- /dev/null +++ b/test/test_ns.ml @@ -0,0 +1,10 @@ +let color_mode = + Option.value + ~default:"auto" + (Option.map + (fun force -> if force = "1" then "always" else "never") + (Sys.getenv_opt "CLICOLOR_FORCE")) +;; + +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 new file mode 100644 index 0000000..11a5dbe --- /dev/null +++ b/test/test_ns.rs @@ -0,0 +1,369 @@ +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("..") + .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") +} + +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() + ) + } +} + +mod nix_develop_tests { + use super::*; + + #[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()); + assert_stdout_eq( + &output.stdout, + &[ + "nix", + "develop", + &flake_dir().to_string_lossy(), + "--command", + env_shell(), + ], + ); + } + + #[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"); + + assert!(output.status.success()); + assert_stdout_eq( + &output.stdout, + &["nix", "develop", &flake_uri, "--command", env_shell()], + ); + } + + #[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()); + assert_stdout_eq( + &output.stdout, + &[ + "nix", + "develop", + &flake_dir().to_string_lossy(), + "--command", + env_shell(), + ], + ); + } + + #[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()); + assert_stdout_eq( + &output.stdout, + &["nix", "develop", &flake_uri, "--command", env_shell()], + ); + } +} + +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_stdout_eq( + &output.stdout, + &[ + "nix-shell", + &shell_dir().to_string_lossy(), + "--command", + env_shell(), + ], + ); + } + + #[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()); + assert_stdout_eq( + &output.stdout, + &[ + "nix-shell", + "--attr", + "default", + &shell_dir().to_string_lossy(), + "--command", + env_shell(), + ], + ); + } +} + +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()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri]); + } + + #[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()); + assert_stdout_eq( + &output.stdout, + &["nix", "shell", &flake_uri, &flake_dir().to_string_lossy()], + ); + } + + #[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()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri, &flake_uri]); + } + + #[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()); + assert_stdout_eq(&output.stdout, &["nix", "shell", &flake_uri]); + } + + #[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()); + assert_stdout_eq( + &output.stdout, + &["nix", "shell", &flake_uri, &flake_dir().to_string_lossy()], + ); + } + + #[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()); + 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, 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(), + 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, 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, 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(), + 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, 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], + ); + } +}