diff --git a/deps.edn b/deps.edn index 45ee090e..0fe8864c 100644 --- a/deps.edn +++ b/deps.edn @@ -123,7 +123,9 @@ org.clojure/algo.monads {:mvn/version "0.1.6"} io.lambdaforge/datalog-parser {:mvn/version "0.1.9"} clj-stacktrace/clj-stacktrace {:mvn/version "0.2.8"} - clojure-msgpack/clojure-msgpack {:mvn/version "1.2.1"}} + clojure-msgpack/clojure-msgpack {:mvn/version "1.2.1"} + cli-matic {:git/url "https://github.com/l3nz/cli-matic.git", :git/sha "9cd53ba7336363e3d06650dbad413b6f8b06e471"} + cli-matic/cli-matic {:git/url "https://github.com/l3nz/cli-matic.git", :git/sha "9cd53ba7336363e3d06650dbad413b6f8b06e471"}} :classpath-overrides {org.clojure/clojure nil org.clojure/spec.alpha nil}} :clj-nvd diff --git a/doc/libraries.csv b/doc/libraries.csv index 77b045cb..c8b044fc 100644 --- a/doc/libraries.csv +++ b/doc/libraries.csv @@ -8,6 +8,7 @@ borkdude/missing.test.assertions,https://github.com/borkdude/missing.test.assert borkdude/rewrite-edn,https://github.com/borkdude/rewrite-edn camel-snake-kebab/camel-snake-kebab,https://github.com/clj-commons/camel-snake-kebab circleci/bond,https://github.com/circleci/bond +cli-matic/cli-matic,https://github.com/l3nz/cli-matic.git clj-commons/clj-yaml,https://github.com/clj-commons/clj-yaml clj-commons/multigrep,https://github.com/clj-commons/multigrep clj-stacktrace/clj-stacktrace,https://github.com/mmcgrana/clj-stacktrace diff --git a/resources/META-INF/babashka/deps.edn b/resources/META-INF/babashka/deps.edn index b1f1c027..0fe8864c 100644 --- a/resources/META-INF/babashka/deps.edn +++ b/resources/META-INF/babashka/deps.edn @@ -25,7 +25,7 @@ org.clojure/data.csv {:mvn/version "1.0.0"}, cheshire/cheshire {:mvn/version "5.10.2"} org.clojure/data.xml {:mvn/version "0.2.0-alpha6"} - clj-commons/clj-yaml {:mvn/version "0.7.107"} + clj-commons/clj-yaml {:mvn/version "0.7.108"} com.cognitect/transit-clj {:mvn/version "1.0.329"} org.clojure/test.check {:mvn/version "1.1.1"} nrepl/bencode {:mvn/version "1.1.0"} @@ -123,7 +123,9 @@ org.clojure/algo.monads {:mvn/version "0.1.6"} io.lambdaforge/datalog-parser {:mvn/version "0.1.9"} clj-stacktrace/clj-stacktrace {:mvn/version "0.2.8"} - clojure-msgpack/clojure-msgpack {:mvn/version "1.2.1"}} + clojure-msgpack/clojure-msgpack {:mvn/version "1.2.1"} + cli-matic {:git/url "https://github.com/l3nz/cli-matic.git", :git/sha "9cd53ba7336363e3d06650dbad413b6f8b06e471"} + cli-matic/cli-matic {:git/url "https://github.com/l3nz/cli-matic.git", :git/sha "9cd53ba7336363e3d06650dbad413b6f8b06e471"}} :classpath-overrides {org.clojure/clojure nil org.clojure/spec.alpha nil}} :clj-nvd diff --git a/test-resources/lib_tests/babashka/run_all_libtests.clj b/test-resources/lib_tests/babashka/run_all_libtests.clj index 5736d1f1..19a8f04f 100644 --- a/test-resources/lib_tests/babashka/run_all_libtests.clj +++ b/test-resources/lib_tests/babashka/run_all_libtests.clj @@ -20,11 +20,21 @@ (or (empty? ns-args) (contains? ns-args ns))) +(defn- filter-vars! + [ns filter-fn] + (doseq [[_name var] (ns-publics ns)] + (when (:test (meta var)) + (when (not (filter-fn var)) + (alter-meta! var #(-> % + (assoc ::test (:test %)) + (dissoc :test))))))) + (defn test-namespaces [& namespaces] (let [namespaces (seq (filter test-namespace? namespaces))] (when (seq namespaces) (doseq [n namespaces] - (require n)) + (require n) + (filter-vars! (find-ns n) #(-> % meta :skip-bb not))) (let [m (apply t/run-tests namespaces)] (swap! status (fn [status] (merge-with + status (dissoc m :type)))))))) diff --git a/test-resources/lib_tests/bb-tested-libs.edn b/test-resources/lib_tests/bb-tested-libs.edn index 81052baa..dafbe85b 100644 --- a/test-resources/lib_tests/bb-tested-libs.edn +++ b/test-resources/lib_tests/bb-tested-libs.edn @@ -106,4 +106,5 @@ org.clojure/algo.monads {:git-url "https://github.com/clojure/algo.monads", :test-namespaces (clojure.algo.test-monads), :git-sha "3a985b0b099110b1654d568fecf597bc9c8d1ff5"} io.lambdaforge/datalog-parser {:git-url "https://github.com/lambdaforge/datalog-parser", :test-namespaces (datalog.parser.pull-test datalog.parser.test.util datalog.parser.impl-test datalog.parser-test datalog.unparser-test), :git-sha "02d193f397afc3f93da704e7c6c850b194f0e797"} clj-stacktrace/clj-stacktrace {:git-url "https://github.com/mmcgrana/clj-stacktrace", :test-namespaces (clj-stacktrace.repl-test clj-stacktrace.core-test), :git-sha "94dc2dd748710e79800e94b713e167e5dc525717"} - clojure-msgpack/clojure-msgpack {:git-url "https://github.com/edma2/clojure-msgpack", :test-namespaces (msgpack.core-check msgpack.core-test), :git-sha "a4bca2cf064a87d9c4a564c634c6ebb65578dad5"}} + clojure-msgpack/clojure-msgpack {:git-url "https://github.com/edma2/clojure-msgpack", :test-namespaces (msgpack.core-check msgpack.core-test), :git-sha "a4bca2cf064a87d9c4a564c634c6ebb65578dad5"} + cli-matic/cli-matic {:git-url "https://github.com/l3nz/cli-matic.git", :test-namespaces (cli-matic.utils-test cli-matic.presets-test cli-matic.help-gen-test cli-matic.utils-convert-config-test cli-matic.utils-candidates-test cli-matic.core-test cli-matic.utils-v2-test), :git-sha "9cd53ba7336363e3d06650dbad413b6f8b06e471"}} diff --git a/test-resources/lib_tests/cli_matic/core_test.cljc b/test-resources/lib_tests/cli_matic/core_test.cljc new file mode 100644 index 00000000..20f1742f --- /dev/null +++ b/test-resources/lib_tests/cli_matic/core_test.cljc @@ -0,0 +1,712 @@ +(ns cli-matic.core-test + (:require [clojure.test :refer [is are deftest testing]] + [cli-matic.platform :as P] + [cli-matic.platform-macros :refer [try-catch-all]] + [clojure.spec.alpha :as s] + [clojure.string :as str] + [cli-matic.core :refer [parse-command-line + run-cmd* + ->RV + assert-unique-values + assert-cfg-sanity + parse-cmds-with-defaults]] + [cli-matic.utils-v2 :as U2])) + +(defn cmd_foo [& opts] nil) +(defn cmd_bar [& opts] nil) +(defn cmd_save_opts [& opts] + ;(prn "Called" opts) + opts) + +(defn cmd_returnstructure [opts] + {:myopts opts + :somedata "hiyo"}) + +(def cli-options + [;; First three strings describe a short-option, long-option with optional + ;; example argument description, and a description. All three are optional + ;; and positional. + ["-p" "--port PORT" "Port number" + :default 80 + :parse-fn #(P/parseInt %) + :validate [#(< 0 % 0x10000) "Must be a number between 0 and 65536"]] + ["-H" "--hostname HOST" "Remote host" + :default 0 + ;; Specify a string to output in the default column in the options summary + ;; if the default value's string representation is very ugly + :default-desc "localhost" + :parse-fn #(P/parseInt %)] + ;; If no required argument description is given, the option is assumed to + ;; be a boolean option defaulting to nil + [nil "--detach" "Detach from controlling process"] + ["-v" nil "Verbosity level; may be specified multiple times to increase value" + ;; If no long-option is specified, an option :id must be given + :id :verbosity + :default 0 + ;; Use assoc-fn to create non-idempotent options + :assoc-fn (fn [m k _] (update-in m [k] inc))] + ;; A boolean option that can explicitly be set to false + ["-d" "--[no-]daemon" "Daemonize the process" :default true] + ["-h" "--help"]]) + +(def SIMPLE-SUBCOMMAND-CFG + {:app {:command "dummy" + :description "I am some command" + :version "0.1.2"} + :global-opts [{:option "aa" :as "A" :type :int} + {:option "bb" :as "B" :type :int}] + :commands [{:command "foo" :short "f" + :description "I am function foo" + :opts [{:option "cc" :as "C" :type :int} + {:option "dd" :as "D" :type :int}] + :runs cmd_foo} + + ; another one + {:command "bar" + :description "I am function bar" + :opts [{:option "ee" :as "E" :type :int} + {:option "ff" :as "F" :type :int}] + :runs cmd_bar} + + ; this one to check return structs + {:command "rets" + :description "I return a structure" + :opts [] + :runs cmd_returnstructure}]}) + +(def SIMPLE-SUBCOMMAND-CFG-v2 + (U2/convert-config-v1->v2 SIMPLE-SUBCOMMAND-CFG)) + +(deftest simple-subcommand + (testing "A simple subcommand - v2" + + ;; Normal subcomand + (is (= (parse-command-line + ["--bb" "1" "foo" "--cc" "2" "--dd" "3"] + SIMPLE-SUBCOMMAND-CFG-v2) + + {:commandline {:bb 1 :cc 2 :dd 3 :_arguments []} + :subcommand "dummy foo" + :subcommand-path ["dummy" "foo"] + :parse-errors :NONE + :error-text "" + :subcommand-def {:command "foo" + :short "f" + :description "I am function foo" + :opts [{:as "C" + :option "cc" + :type :int} + {:as "D" + :option "dd" + :type :int}] + :runs cmd_foo}})) + + + + ;; short subcommand + + + (is (= (parse-command-line + ["--bb" "1" "f" "--cc" "2" "--dd" "3"] + SIMPLE-SUBCOMMAND-CFG-v2) + + {:commandline {:bb 1 :cc 2 :dd 3 :_arguments []} + :subcommand "dummy foo" + :subcommand-path ["dummy" "foo"] + :parse-errors :NONE + :error-text "" + :subcommand-def {:command "foo" + :short "f" + :description "I am function foo" + :opts [{:as "C" + :option "cc" + :type :int} + {:as "D" + :option "dd" + :type :int}] + :runs cmd_foo}})) + + ;; unknown subcommand + (is (= (parse-command-line + ["--bb" "1" "unknown" "--cc" "2" "--dd" "3"] + SIMPLE-SUBCOMMAND-CFG-v2) + + {:commandline {} + :error-text "Unknown sub-command: 'dummy unknown'." + :parse-errors :ERR-UNKNOWN-SUBCMD + :subcommand "dummy unknown" + :subcommand-path ["dummy" "unknown"] + :subcommand-def nil})))) + +(deftest run-examples + (testing "Some real-life behavior for our SIMPLE case - v2" + (are [i o] + (= (run-cmd* SIMPLE-SUBCOMMAND-CFG-v2 i) o) + + ; no parameters - displays cmd help + [] + (->RV -1 :ERR-NO-SUBCMD :HELP-GLOBAL ["dummy"] "No sub-command specified.") + + ["x"] + (->RV -1 :ERR-UNKNOWN-SUBCMD :HELP-GLOBAL ["dummy" "x"] "Unknown sub-command: 'dummy x'.") + + ["--lippa" "foo"] + (->RV -1 :ERR-PARMS-GLOBAL :HELP-GLOBAL ["dummy"] "Global option error: Unknown option: \"--lippa\"") + + ; help globale + ["-?"] + (->RV 0 :OK :HELP-GLOBAL ["dummy"] nil) + + ["--help"] + (->RV 0 :OK :HELP-GLOBAL ["dummy"] nil) + + ; help sub-commands (incl short version) + ["foo" "-?"] + (->RV 0 :OK :HELP-SUBCMD ["dummy" "foo"] nil) + + ["bar" "--help"] + (->RV 0 :OK :HELP-SUBCMD ["dummy" "bar"] nil) + + ["f" "-?"] + (->RV 0 :OK :HELP-SUBCMD ["dummy" "foo"] nil) + + ["rets"] + (->RV 0 :OK nil nil nil)))) + +(def MANDATORY-SUBCOMMAND-CFG + {:app {:command "dummy" + :description "I am some command" + :version "0.1.2"} + :global-opts [{:option "aa" :as "A" :type :int :default :present} + {:option "bb" :as "B" :type :int}] + :commands [{:command "foo" :short "f" + :description "I am function foo" + :opts [{:option "cc" :as "C" :type :int :default :present} + {:option "dd" :as "D" :type :int}] + :runs cmd_foo}]}) + +(def MANDATORY-SUBCOMMAND-CFG-v2 + (U2/convert-config-v1->v2 MANDATORY-SUBCOMMAND-CFG)) + +(deftest check-mandatory-options + (testing "Some real-life behavior with mandatory options" + (are [i o] + (= (run-cmd* MANDATORY-SUBCOMMAND-CFG-v2 i) o) + + ; no parameters - displays cmd help + [] + (->RV -1 :ERR-NO-SUBCMD :HELP-GLOBAL ["dummy"] "No sub-command specified.") + + ["x"] + (->RV -1 :ERR-PARMS-GLOBAL :HELP-GLOBAL ["dummy"] "Global option error: Missing option: aa") + + ["--lippa" "foo"] + (->RV -1 :ERR-PARMS-GLOBAL :HELP-GLOBAL ["dummy"] "Global option error: Unknown option: \"--lippa\"") + + ; help globale + ["-?"] + (->RV 0 :OK :HELP-GLOBAL ["dummy"] nil) + + ; help sub-commands (incl short version) + ["--aa" "1" "foo" "-?"] + (->RV 0 :OK :HELP-SUBCMD ["dummy" "foo"] nil) + + ; error no global cmd + ["foo" "--cc" "1"] + (->RV -1 :ERR-PARMS-GLOBAL :HELP-GLOBAL ["dummy"] "Global option error: Missing option: aa") + + ; error no sub cmd + ["--aa" "1" "foo" "--dd" "1"] + (->RV -1 :ERR-PARMS-SUBCMD :HELP-SUBCMD ["dummy" "foo"] "Option error: Missing option: cc") + + ; works + ["--aa" "1" "foo" "--cc" "1"] + (->RV 0 :OK nil nil nil)))) + + +; Problems +; -------- +; +; Types +; +; lein run -m cli-matic.toycalc -- add --a x +; ** ERROR: ** +; Error: +; and nothing else + + +;; VALIDATION OF CONFIGURATION +;; + + +(deftest check-unique-options + (testing "Unique options" + (are [i o] + (= (try-catch-all + (apply assert-unique-values i) + (fn [_] :ERR)) + o) + + ; empty + ["a" [] :x] + nil + + ; ok + ["pippo" + [{:option "a" :as "Parameter A" :type :int :default 0} + {:option "b" :as "Parameter B" :type :int :default 0}] + :option] + nil + + ; dupe + ["pippo" + [{:option "a" :as "Parameter A" :type :int :default 0} + {:option "a" :as "Parameter B" :type :int :default 0}] + :option] + :ERR))) + +(comment + ;;; TO DO + + (deftest check-cfg-format + (testing "Cfg format" + (are [i o] + (= o + (try-catch-all + (-> i + U2/add-setup-defaults-v1 + assert-cfg-sanity) + (fn [_] + ;(prn e) + :ERR))) + + ;; OK + {:app {:command "toycalc" :description "A" :version "0.0.1"} + + :global-opts [{:option "base" :as "T" :type :int :default 10}] + + :commands [{:command "add" :description "Adds" :runs identity + :opts [{:option "a" :as "Addendum 1" :type :int} + {:option "b" :as "Addendum 2" :type :int :default 0}]}]} + nil + + ;; No global options - still OK (bug #35) + {:app {:command "toycalc" :description "A" :version "0.0.1"} + :commands [{:command "add" :description "Adds" :runs identity + :opts [{:option "a" :as "Addendum 1" :type :int} + {:option "b" :as "Addendum 2" :type :int :default 0}]}]} + nil + + ;; double in global + {:app {:command "toycalc" :description "A" :version "0.0.1"} + + :global-opts [{:option "base" :as "T" :type :int :default 10} + {:option "base" :as "X" :type :int :default 10}] + + :commands [{:command "add" :description "Adds" :runs identity + :opts [{:option "a" :as "Addendum 1" :type :int} + {:option "b" :as "Addendum 2" :type :int :default 0}]}]} + :ERR + + ;; double in specific + {:app {:command "toycalc" :description "A" :version "0.0.1"} + + :global-opts [{:option "base" :as "T" :type :int :default 10}] + + :commands [{:command "add" :description "Adds" :runs identity + :opts [{:option "a" :short "q" :as "Addendum 1" :type :int} + {:option "b" :short "q" :as "Addendum 2" :type :int :default 0}]}]} + :ERR + + ;; positional subcmds in global opts + {:app {:command "toycalc" :description "A" :version "0.0.1"} + + :global-opts [{:option "base" :short 0 :as "T" :type :int :default 10}] + + :commands [{:command "add" :description "Adds" :runs identity + :opts [{:option "a" :short "q" :as "Addendum 1" :type :int} + {:option "b" :short "d" :as "Addendum 2" :type :int :default 0}]}]} + :ERR))) + ;;;; + ) + +(def POSITIONAL-SUBCOMMAND-CFG + {:app {:command "dummy" + :description "I am some command" + :version "0.1.2"} + :global-opts [{:option "aa" :as "A" :type :int :default :present} + {:option "bb" :as "B" :type :int}] + :commands [{:command "foo" :short "f" + :description "I am function foo" + :opts [{:option "cc" :short 0 :as "C" :type :int :default :present} + {:option "dd" :as "D" :type :int} + {:option "ee" :short 1 :as "E" :type :int}] + :runs cmd_save_opts}]}) + +(def POSITIONAL-SUBCOMMAND-CFG-v2 + (U2/convert-config-v1->v2 POSITIONAL-SUBCOMMAND-CFG)) + +(deftest check-positional-options + (testing "Some real-life behavior with mandatory options" + (are [i o] + (= (select-keys + (parse-command-line i POSITIONAL-SUBCOMMAND-CFG-v2) + [:commandline :error-text]) o) + + ;; a simple case + ["--aa" "10" "foo" "1" "2"] + {:commandline {:_arguments ["1" "2"] + :aa 10 + :cc 1 + :ee 2} + :error-text ""} + + ;; positional arg does not exist but is default present + ["--aa" "10" "foo"] + {:commandline {} + :error-text "Missing option: cc"} + + ;; positional arg does not exist and it is not default present + ["--aa" "10" "foo" "1"] + {:commandline {:_arguments ["1"] + :aa 10 + :cc 1} + :error-text ""}))) + +(defn env-helper [s] + (get {"VARA" "10" + "VARB" "HELLO"} s)) + +(deftest check-environmental-vars + (testing "Parsing with env - global opts" + (are [opts cmdline result] + (= (dissoc (parse-cmds-with-defaults opts cmdline true env-helper) :summary) result) + + ;; a simple case - no env vars + [{:option "cc" :short 0 :as "C" :type :int :default :present} + {:option "dd" :as "D" :type :string}] + + ["--cc" "0" "pippo" "pluto"] + + {:arguments ["pippo" "pluto"] + :errors nil + :options {:cc 0}} + + ;; a simple case - absent, with env set, integer + [{:option "cc" :short 0 :as "C" :type :int :default :present} + {:option "dd" :as "D" :type :int :env "VARA"}] + + ["--cc" "0" "pippo" "pluto"] + + {:arguments ["pippo" "pluto"] + :errors nil + :options {:cc 0 :dd 10}} + + ;; present, with env set, integer + [{:option "cc" :short 0 :as "C" :type :int :default :present} + {:option "dd" :as "D" :type :int :env "VARA"}] + + ["--cc" "0" "--dd" "23" "pippo" "pluto"] + + {:arguments ["pippo" "pluto"] + :errors nil + :options {:cc 0 :dd 23}} + + ;; absent, with env missing, integer + [{:option "cc" :short 0 :as "C" :type :int :default :present} + {:option "dd" :as "D" :type :int :env "NO-VARA"}] + + ["--cc" "0" "pippo" "pluto"] + + {:arguments ["pippo" "pluto"] + :errors nil + :options {:cc 0}}))) + +; ======================================================================= +; ======== S P E C S ========== +; ======================================================================= + +; We add a stupid spec check +; Specs are checked after parsing, both on parameters and globally. +; if presents, specs are checked + +(s/def ::ODD-NUMBER odd?) + +(s/def ::GENERAL-SPEC-FOO #(= 99 (:ee %))) + +(def SPEC-CFG + {:app {:command "dummy" + :description "I am some command" + :version "0.1.2"} + :global-opts [{:option "aa" :as "A" :type :int :default :present :spec ::ODD-NUMBER} + {:option "bb" :as "B" :type :int :spec ::ODD-NUMBER}] + :commands [{:command "foo" + :short "f" + :description "I am function foo" + :opts [{:option "cc" :short 0 :as "C" :type :int :default :present} + {:option "dd" :as "D" :type :int :spec ::ODD-NUMBER} + {:option "ee" :short 1 :as "E" :type :int :spec ::ODD-NUMBER}] + :spec ::GENERAL-SPEC-FOO + :runs cmd_save_opts}]}) +(def SPEC-CFG-v2 + (U2/convert-config-v1->v2 SPEC-CFG)) + +(defn keep-1st-line-stderr + "To avoid issues with expound changing messages, we remove all + but the first line in stderr for testing." + [{:keys [stderr] :as all}] + + (let [nv (if (and (vector? stderr) (pos? (count stderr))) + (let [lines (str/split-lines (first stderr)) + vec-of-fline [(first lines)]] + + vec-of-fline) stderr)] + + (assoc all + :stderr nv))) + +;; ------------ + + +(deftest check-specs + (are [i o] + (= (keep-1st-line-stderr (run-cmd* SPEC-CFG-v2 i)) o) + + ; all of the should pass + ["--aa" "3" "--bb" "7" "foo" "--cc" "2" "--dd" "3" "--ee" "99"] + (->RV 0 :OK nil nil []) + + ; aa (global) not odd + ["--aa" "2" "--bb" "7" "foo" "--cc" "2" "--dd" "3" "--ee" "99"] + (->RV -1 :ERR-PARMS-GLOBAL :HELP-GLOBAL ["dummy"] ["Global option error: Spec failure for global option 'aa'"]) + + ; bb does not exist but it's not mandatory + ["--aa" "3" "foo" "--cc" "2" "--dd" "3" "--ee" "99"] + (->RV 0 :OK nil nil nil) + + ; dd (local) + ["--aa" "3" "--bb" "7" "foo" "--cc" "2" "--dd" "4" "--ee" "99"] + (->RV -1 :ERR-PARMS-SUBCMD :HELP-SUBCMD ["dummy" "foo"] ["Option error: Spec failure for option 'dd'"]) + + ; dd is missing - spec not checked - bug #105 + ["--aa" "3" "--bb" "7" "foo" "--cc" "2" "--ee" "99"] + (->RV 0 :OK nil nil nil) + + ; ee non 99 (validazione globale subcmd) + ["--aa" "3" "--bb" "7" "foo" "--cc" "2" "--dd" "5" "--ee" "97"] + (->RV -1 :ERR-PARMS-SUBCMD :HELP-SUBCMD ["dummy" "foo"] ["Option error: Spec failure for subcommand 'dummy foo'"]))) + + ;;;;; + + +; ================================================================= +; +; ================================================================= + + +(def SETS-CFG + {:app {:command "dummy" + :description "I am some command" + :version "0.1.2"} + :global-opts [] + :commands [{:command "foo" :short "f" + :description "I am function foo" + :opts [{:option "kw" :as "blabla" :type #{:a :b :zebrafuffa}}] + :runs cmd_save_opts}]}) + +(def SETS-CFG-v2 + (U2/convert-config-v1->v2 SETS-CFG)) + +(deftest check-sets + (are [i o] + (= (run-cmd* SETS-CFG-v2 i) o) + + ; all of the should pass + ["foo" "--kw" "a"] + (->RV 0 :OK nil nil []) + + ["foo" "--kw" "B"] + (->RV 0 :OK nil nil []) + + ["foo" "--kw" "zebrafufa"] + {:help :HELP-SUBCMD + :retval -1 + :status :ERR-PARMS-SUBCMD + :stderr ["Option error: Error while parsing option \"--kw zebrafufa\": clojure.lang.ExceptionInfo: Value 'zebrafufa' not allowed. Did you mean ':zebrafuffa'? {}"] + :subcmd ["dummy" "foo"]})) + + +; ================================================================= +; +; ================================================================= + + +(def FLAGS-CFG + {:app {:command "dummy" + :description "I am some command" + :version "0.1.2"} + :global-opts [] + :commands [{:command "foo" :short "f" + :description "I am function foo" + :opts [{:option "bar" :as "bar" :type :with-flag :default false} + {:option "flag" :as "flag" :type :flag :default false}] + :runs cmd_save_opts}]}) + +(def FLAGS-CFG-v2 + (U2/convert-config-v1->v2 FLAGS-CFG)) + +(deftest check-flags + (are [input expected] + (= expected (run-cmd* FLAGS-CFG-v2 input)) + + ["foo" "--bar"] + (->RV 0 :OK nil nil []) + + ["foo" "--no-bar"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "Y"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "Yes"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "On"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "T"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "True"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "1"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "N"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "No"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "Off"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "F"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "False"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "false"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "0"] + (->RV 0 :OK nil nil []) + + ["foo" "--flag" "2"] + (->RV -1 :ERR-PARMS-SUBCMD :HELP-SUBCMD ["dummy" "foo"] + "Option error: Error while parsing option \"--flag 2\": clojure.lang.ExceptionInfo: Unsupported flag value {:flag \"2\"}") + + ["foo" "--bar" "--flag" "Y"] + (->RV 0 :OK nil nil []) + + ["foo" "--no-bar" "--flag" "0"] + (->RV 0 :OK nil nil []))) + +(deftest check-flags-more-complex + (are [input expected] + (= expected (parse-command-line input FLAGS-CFG-v2)) + + ["foo" "--bar"] + {:commandline {:_arguments [] :bar true :flag false} + :error-text "" + :parse-errors :NONE + :subcommand "dummy foo" + :subcommand-path ["dummy" "foo"] + :subcommand-def {:command "foo" + :description "I am function foo" + :opts [{:as "bar" + :default false + :option "bar" + :type :with-flag} + {:as "flag" + :default false + :option "flag" + :type :flag}] + :runs cmd_save_opts + :short "f"}} + + ["foo" "--no-bar"] + {:commandline {:_arguments [] :bar false :flag false} + :error-text "" + :parse-errors :NONE + :subcommand "dummy foo" + :subcommand-path ["dummy" "foo"] + :subcommand-def {:command "foo" + :description "I am function foo" + :opts [{:as "bar" + :default false + :option "bar" + :type :with-flag} + {:as "flag" + :default false + :option "flag" + :type :flag}] + :runs cmd_save_opts + :short "f"}} + + ["foo" "--no-bar" "--flag" "Y"] + {:commandline {:_arguments [] :bar false :flag true} + :error-text "" + :parse-errors :NONE + :subcommand "dummy foo" + :subcommand-path ["dummy" "foo"] + :subcommand-def {:command "foo" + :description "I am function foo" + :opts [{:as "bar" + :default false + :option "bar" + :type :with-flag} + {:as "flag" + :default false + :option "flag" + :type :flag}] + :runs cmd_save_opts + :short "f"}} + + ["foo" "--no-bar" "--flag" "Off"] + {:commandline {:_arguments [] :bar false :flag false} + :error-text "" + :parse-errors :NONE + :subcommand "dummy foo" + :subcommand-path ["dummy" "foo"] + :subcommand-def {:command "foo" + :description "I am function foo" + :opts [{:as "bar" + :default false + :option "bar" + :type :with-flag} + {:as "flag" + :default false + :option "flag" + :type :flag}] + :runs cmd_save_opts + :short "f"}})) + +(deftest spec-on-options-bug105 + (let [CFG {:command "toycalc" + :description "A command-line toy adder" + :version "0.0.1" + :opts [{:as "Parameter A" + :default 0 + :option "a" + :type :int} + {:as "Parameter B" + :default 0 + :option "b" + :type :int}] + :runs cmd_foo}])) + + + diff --git a/test-resources/lib_tests/cli_matic/edn_simple.edn b/test-resources/lib_tests/cli_matic/edn_simple.edn new file mode 100644 index 00000000..3faa801c --- /dev/null +++ b/test-resources/lib_tests/cli_matic/edn_simple.edn @@ -0,0 +1,3 @@ +{:list [1 2 "hi"] + :intval 100 + :strval "good"} diff --git a/test-resources/lib_tests/cli_matic/help_gen_test.cljc b/test-resources/lib_tests/cli_matic/help_gen_test.cljc new file mode 100644 index 00000000..3666cb5c --- /dev/null +++ b/test-resources/lib_tests/cli_matic/help_gen_test.cljc @@ -0,0 +1,406 @@ +(ns cli-matic.help-gen-test + (:require [clojure.test :refer [is are deftest testing]] + #?(:clj [cli-matic.platform-macros :refer [try-catch-all]] + :cljs [cli-matic.platform-macros :refer-macros [try-catch-all]]) + [cli-matic.help-gen :refer [generate-possible-mistypes + generate-section + generate-a-command + generate-global-help + generate-subcmd-help]] + [cli-matic.utils-v2 :as U2])) + +(deftest generate-possible-mistypes-test + + (are [w c a o] + (= o (generate-possible-mistypes w c a)) + + ; + "purchasse" + ["purchase" "purchasing" "add" "remove"] + [nil "PP" "A" "R"] + ["purchase" "purchasing"])) + +(deftest generate-section-test + + (are [t l o] + (= o (-> (generate-section t l) + flatten + vec)) + + ; full + "tit" + ["a" "b"] + ["tit:" " a" " b" ""] + + ; empty + "tittel" + [] + [])) + +(deftest generate-a-command-test + + (are [c s d o] + (= o (generate-a-command {:command c :short s :description d})) + + ; full + "tit" "t" ["a" "b"] + " tit, t a" + + ; linea singola + "tit" "t" "singel" + " tit, t singel" + + ; no description + "tit" "t" [] + " tit, t ?")) + +(defn dummy-cmd [_]) + +(def CONFIGURATION-TOYCALC + {:app {:command "toycalc" + :description "A command-line toy calculator" + :version "0.0.1"} + :global-opts [{:option "base" + :as "The number base for output" + :type :int + :default 10}] + :commands [{:command "add" :short "a" + :description ["Adds two numbers together" + "" + "Looks great, doesn't it?"] + :opts [{:option "a1" :short "a" :env "AA" :as "First addendum" :type :int :default 0} + {:option "a2" :short "b" :as "Second addendum" :type :int :default 0}] + :runs dummy-cmd} + {:command "sub" :short "s" + :description "Subtracts parameter B from A" + :opts [{:option "pa" :short "a" :as "Parameter A" :type :int :default 0} + {:option "pb" :short "b" :as "Parameter B" :type :int :default 0}] + :runs dummy-cmd}]}) + +(def CONFIGURATION-TOYCALC-v2 + (U2/convert-config-v1->v2 CONFIGURATION-TOYCALC)) + +(def CONFIGURATION-TOYCALC-NESTED + {:command "toycalc" + :description "A command-line toy calculator" + :version "0.0.1" + :opts [{:as "The number base for output" + :default 10 + :option "base" + :type :int}] + :subcommands [{:command "add" + :short "a" + :description "Adds two numbers together" + :version "3.3.3" + :examples ["Example One" "Example Two"] + :opts [{:as "Addendum 1" + :option "a" + :type :int} + {:as "Addendum 2" + :default 0 + :option "b" + :type :int}] + :runs dummy-cmd} + {:command "subc" + :description "Subtracts parameter B from A" + :examples "Just one example" + :version "1.2.3" + :opts [{:as "Parameter q" + :default 0 + :option "q" + :type :int}] + :subcommands [{:command "sub" + :description "Subtracts" + :opts [{:as "Parameter A to subtract from" + :default 0 + :option "a" + :type :int} + {:as "Parameter B" + :default 0 + :option "b" + :type :int}] + :runs dummy-cmd}]}]}) + +(deftest generate-global-help-test + + (is + (= ["NAME:" + " toycalc - A command-line toy calculator" + "" + "USAGE:" + " toycalc [global-options] command [command options] [arguments...]" + "" + "VERSION:" + " 0.0.1" + "" + "COMMANDS:" + " add, a Adds two numbers together" + " sub, s Subtracts parameter B from A" + "" + "GLOBAL OPTIONS:" + " --base N 10 The number base for output" + " -?, --help" + ""] + (generate-global-help CONFIGURATION-TOYCALC-v2 ["toycalc"])))) + +(deftest generate-global-help-test-nested + + (is + (= ["NAME:" + " toycalc - A command-line toy calculator" + "" + "USAGE:" + " toycalc [global-options] command [command options] [arguments...]" + "" + "VERSION:" + " 0.0.1" + "" + "COMMANDS:" + " add, a Adds two numbers together" + " subc Subtracts parameter B from A" + "" + "GLOBAL OPTIONS:" + " --base N 10 The number base for output" + " -?, --help" + ""] + (generate-global-help CONFIGURATION-TOYCALC-NESTED ["toycalc"]))) + + (is + (= ["NAME:" + " toycalc - A command-line toy calculator" + "" + "USAGE:" + " toycalc [global-options] command [command options] [arguments...]" + "" + "VERSION:" + " 0.0.1" + "" + "COMMANDS:" + " add, a Adds two numbers together" + " subc Subtracts parameter B from A" + "" + "GLOBAL OPTIONS:" + " --base N 10 The number base for output" + " -?, --help" + ""] + (generate-global-help CONFIGURATION-TOYCALC-NESTED []))) + + (is + (= ["NAME:" + " toycalc subc - Subtracts parameter B from A" + "" + "USAGE:" + " toycalc subc [global-options] command [command options] [arguments...]" + "" + "EXAMPLES:" + " Just one example" + "" + "VERSION:" + " 1.2.3" + "" + "COMMANDS:" + " sub Subtracts" + "" + "GLOBAL OPTIONS:" + " --q N 0 Parameter q" + " -?, --help" + ""] + (generate-global-help CONFIGURATION-TOYCALC-NESTED ["toycalc" "subc"])))) + +(deftest generate-subcmd-help-test-nested + (is + (= ["NAME:" + " toycalc add - Adds two numbers together" + "" + "USAGE:" + " toycalc [add|a] [command options] [arguments...]" + "" + "EXAMPLES:" + " Example One" + " Example Two" + "" + "VERSION:" + " 3.3.3" + "" + "OPTIONS:" + " --a N Addendum 1" + " --b N 0 Addendum 2" + " -?, --help" + ""] + (generate-subcmd-help CONFIGURATION-TOYCALC-NESTED ["toycalc" "add"])))) + +(deftest generate-subcmd-help-test + + (is + (= ["NAME:" + " toycalc add - Adds two numbers together" + " " + " Looks great, doesn't it?" + "" + "USAGE:" + " toycalc [add|a] [command options] [arguments...]" + "" + "OPTIONS:" + " -a, --a1 N 0 First addendum [$AA]" + " -b, --a2 N 0 Second addendum" + " -?, --help" + ""] + (generate-subcmd-help CONFIGURATION-TOYCALC-v2 ["toycalc" "add"]))) + + (is + (= ["NAME:" + " toycalc sub - Subtracts parameter B from A" + "" + "USAGE:" + " toycalc [sub|s] [command options] [arguments...]" + "" + "OPTIONS:" + " -a, --pa N 0 Parameter A" + " -b, --pb N 0 Parameter B" + " -?, --help" + ""] + (generate-subcmd-help CONFIGURATION-TOYCALC-v2 ["toycalc" "sub"]))) + + (is + (= :ERR + (try-catch-all + (generate-subcmd-help CONFIGURATION-TOYCALC-v2 ["toycalc" "undefined-cmd"]) + (fn [_] :ERR))))) + +(def CONFIGURATION-POSITIONAL-TOYCALC + {:app {:command "toycalc" + :description "A command-line toy calculator" + :version "0.0.1"} + :global-opts [{:option "base" + :as "The number base for output" + :type :int + :default 10}] + :commands [{:command "add" :short "a" + :description ["Adds two numbers together" + "" + "Looks great, doesn't it?"] + :opts [{:option "a1" :short 0 :env "AA" :as "First addendum" :type :int :default 0} + {:option "a2" :short 1 :as "Second addendum" :type :int :default 0}] + :runs dummy-cmd}]}) + +(def CONFIGURATION-POSITIONAL-TOYCALC-v2 + (U2/convert-config-v1->v2 CONFIGURATION-POSITIONAL-TOYCALC)) + +(deftest generate-subcmd-positional-test + (is + (= ["NAME:" + " toycalc add - Adds two numbers together" + " " + " Looks great, doesn't it?" + "" + "USAGE:" + " toycalc [add|a] [command options] a1 a2" + "" + "OPTIONS:" + " --a1 N 0 First addendum [$AA]" + " --a2 N 0 Second addendum" + " -?, --help" + ""] + (generate-subcmd-help CONFIGURATION-POSITIONAL-TOYCALC-v2 ["toycalc" "add"])))) + +(def CONFIGURATION-MULTILINES + {:command "multiline" + :description ["An app description can span" + "multiple lines."] + :version "1.2.3" + :opts [{:option "global-opt" + :as ["Global opt help" + "with" + "multiple lines."] + :type :int + :default 10}] + :subcommands [{:command "mycmd" + :description ["A command description" + "" + "Can contain multiple lines." + "Only the first line is displayed on global help."] + :opts [{:option "mycmd-opt1" :short "a" :type :int :default 0 + :as ["not really a multiline but just fine"]} + {:option "long-name-here-should-stretch-things-out" :short "l" :type :keyword + :as ["testing out how a longer" + "option affects things."]} + {:option "opt2" :type :int :default 0 + :as ["text that is long" + "can be split" + "over" + "many" + "lines" + "and" + " will" + " be" + " indented" + " appropriately" + "and" + "can" + "include empty" + "" + "lines."]} + {:option "opt3" :short "c" :env "ENV_VAR" :type :string :multiple true :default :present + :as ["here's what happens to a multiline with" + "env also set"]}] + + :runs dummy-cmd}]}) + +(deftest multilines-global-help-test + (is + (= ["NAME:" + " multiline - An app description can span" + " multiple lines." + "" + "USAGE:" + " multiline [global-options] command [command options] [arguments...]" + "" + "VERSION:" + " 1.2.3" + "" + "COMMANDS:" + " mycmd A command description" + "" + "GLOBAL OPTIONS:" + " --global-opt N 10 Global opt help" + " with" + " multiple lines." + " -?, --help" + ""] + (generate-global-help CONFIGURATION-MULTILINES ["multiline"])))) + +(deftest multilines-cmd-help-test + (is + (= ["NAME:" + " multiline mycmd - A command description" + " " + " Can contain multiple lines." + " Only the first line is displayed on global help." + "" + "USAGE:" + " multiline mycmd [command options] [arguments...]" + "" + "OPTIONS:" + " -a, --mycmd-opt1 N 0 not really a multiline but just fine" + " -l, --long-name-here-should-stretch-things-out S testing out how a longer" + " option affects things." + " --opt2 N 0 text that is long" + " can be split" + " over" + " many" + " lines" + " and" + " will" + " be" + " indented" + " appropriately" + " and" + " can" + " include empty" + " " + " lines." + " -c, --opt3 S* here's what happens to a multiline with" + " env also set [$ENV_VAR]" + " -?, --help" + ""] + (generate-subcmd-help CONFIGURATION-MULTILINES ["multiline" "mycmd"])))) diff --git a/test-resources/lib_tests/cli_matic/json_simple.json b/test-resources/lib_tests/cli_matic/json_simple.json new file mode 100644 index 00000000..966a419d --- /dev/null +++ b/test-resources/lib_tests/cli_matic/json_simple.json @@ -0,0 +1,5 @@ +{ + "list": [1, 2, "hi"], + "intval": 100, + "strval": "good" +} \ No newline at end of file diff --git a/test-resources/lib_tests/cli_matic/presets_test.cljc b/test-resources/lib_tests/cli_matic/presets_test.cljc new file mode 100644 index 00000000..cba2c584 --- /dev/null +++ b/test-resources/lib_tests/cli_matic/presets_test.cljc @@ -0,0 +1,453 @@ +(ns cli-matic.presets-test + (:require [clojure.test :refer [is are deftest testing]] + [cljc.java-time.zone-id :as zone-id] + [cljc.java-time.local-date :as local-date] + [cljc.java-time.local-date-time :as local-date-time] + [cljc.java-time.zoned-date-time :as zoned-date-time] + [cli-matic.core :refer [parse-command-line]] + [cli-matic.utils-v2 :refer [convert-config-v1->v2]] + [cli-matic.presets :refer [set-help-values set-find-value set-find-didyoumean]] + [clojure.java.io :as io])) + +(defn cmd_foo [v] + (prn "Foo:" v) + 0) + +(defn mkDummyCfg + [myOption] + + (convert-config-v1->v2 + + {:app {:command "dummy" + :description "I am some command" + :version "0.1.2"} + :global-opts [] + :commands [{:command "foo" + :description "I am function foo" + :opts [myOption] + :runs cmd_foo}]})) + +; :subcommand "foo" +; :subcommand-def + +(defn parse-cmds-simpler [args cfg] + (dissoc + (parse-command-line args cfg) + :subcommand + :subcommand-path + :subcommand-def)) + +(defn str-val + "Rewrites a value for float comparison to a string" + [o] + (let [v (get-in o [:commandline :val]) + vs (str v)] + (assoc-in o [:commandline :val] vs))) + +; :int values + +(deftest test-ints + + (testing "int value" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :int})) + o) + + ; integers + ["foo" "--val" "7"] + {:commandline {:_arguments [] + :val 7} + :error-text "" + :parse-errors :NONE})) + + ; + (testing "int-0 value" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :int-0})) + o) + + ; integers + ["foo" "--val" "7"] + {:commandline {:_arguments [] + :val 7} + :error-text "" + :parse-errors :NONE} + + ; integers + ["foo"] + {:commandline {:_arguments [] + :val 0} + :error-text "" + :parse-errors :NONE}))) + +;; float values (float and float-0) +;; to compare them, we rewrite them to strings +(deftest test-float + + (testing "float value" + (are [i o] + (= (str-val (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :float}))) + + (str-val o)) + + ; integers as floats + ["foo" "--val" "7"] + {:commandline {:_arguments [] + :val 7.0} + :error-text "" + :parse-errors :NONE} + + ; floats as floats + ["foo" "--val" "3.14"] + {:commandline {:_arguments [] + :val 3.14} + :error-text "" + :parse-errors :NONE})) + + (testing "float0 value" + (are [i o] + (= (str-val (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :float-0}))) + + (str-val o)) + + ; integers as floats + ["foo" "--val" "7"] + {:commandline {:_arguments [] + :val 7.0} + :error-text "" + :parse-errors :NONE} + + ; floats as floats + ["foo" "--val" "3.14"] + {:commandline {:_arguments [] + :val 3.14} + :error-text "" + :parse-errors :NONE} + + ; missing as zero + ["foo"] + {:commandline {:_arguments [] + :val 0.0} + :error-text "" + :parse-errors :NONE}))) + +;; :keyword +(deftest test-keyword + (testing "simple keyword" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :keyword})) o) + + ; + ["foo" "--val" "abcd"] + {:commandline {:_arguments [] + :val :abcd} + :error-text "" + :parse-errors :NONE})) + (testing "Already keyword" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :keyword})) o) + + ; + ["foo" "--val" ":core/xyz"] + {:commandline {:_arguments [] + :val :core/xyz} + :error-text "" + :parse-errors :NONE})) + (testing "double colon" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :keyword})) o) + + ; + ["foo" "--val" "::abcd"] + {:commandline {:_arguments [] + :val :user/abcd} + :error-text "" + :parse-errors :NONE}))) + +; :string +(deftest test-string + (testing "just strings" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :string})) o) + + ; + ["foo" "--val" "abcd"] + {:commandline {:_arguments [] + :val "abcd"} + :error-text "" + :parse-errors :NONE})) + + (testing + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :short "v" :as "x" :type :string})) o) + + ; + ["foo" "-v" "abcd" "aaarg"] + {:commandline {:_arguments ["aaarg"] + :val "abcd"} + :error-text "" + :parse-errors :NONE})) + + (testing "multiple strings" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :string :multiple true})) o) + ; + ["foo" "--val" "abcd"] + {:commandline {:_arguments [] + :val ["abcd"]} + :error-text "" + :parse-errors :NONE} + + ["foo" "--val" "abcd" "--val" "defg"] + {:commandline {:_arguments [] + :val ["abcd" "defg"]} + :error-text "" + :parse-errors :NONE})) + + (testing "multiple strings but no multiple option" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :string :multiple false})) o) + + ; + ["foo" "--val" "abcd"] + {:commandline {:_arguments [] + :val "abcd"} + :error-text "" + :parse-errors :NONE} + + ["foo" "--val" "abcd" "--val" "defg"] + {:commandline {:_arguments [] + :val "defg"} + :error-text "" + :parse-errors :NONE}))) + +; :yyyy-mm-dd + +(deftest test-dates + (testing "YYYY-MM-DD suck" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :yyyy-mm-dd})) o) + + ; this works + ["foo" "--val" "2018-01-01"] + {:commandline {:_arguments [] + :val (-> (local-date/parse "2018-01-01") + (local-date/at-start-of-day) + (local-date-time/at-zone (zone-id/system-default)) + (zoned-date-time/to-instant) + (java.util.Date/from))} + :error-text "" + :parse-errors :NONE} + + ; this does not + ["foo" "--val" "pippo"] + {:commandline {:_arguments [] + :val nil} + :error-text "" + :parse-errors :NONE}))) + +; Slurping di file di testo + +;; BB_TEST_PATCH: rewrite where the tests get resources from +(def base-dir (.getParent (io/file *file*))) +(def resource (fn [x] (str (io/file base-dir x)))) + +(deftest test-slurping + (testing "Slurping all-in-one" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :slurp})) o) + + ; one file + ["foo" "--val" (resource "three_lines.txt")] + {:commandline {:_arguments [] + :val "L1\nLine2\nline 3\n"} + :error-text "" + :parse-errors :NONE})) (testing "Slurping multiline" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :slurplines})) o) + + ; one file + ["foo" "--val" (resource "three_lines.txt")] + {:commandline {:_arguments [] + :val ["L1" "Line2" "line 3"]} + :error-text "" + :parse-errors :NONE}))) + +; JSON + +(deftest test-json + (testing "JSON single value" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :json})) o) + + ; one file + ["foo" "--val" "{\"a\":1, \"b\":2}"] + {:commandline {:_arguments [] + :val {"a" 1 + "b" 2}} + :error-text "" + :parse-errors :NONE})) + + (testing "Slurping multiline JSON" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :jsonfile})) o) + + ; one file + ["foo" "--val" (resource "json_simple.json")] + {:commandline {:_arguments [] + :val {"list" [1 2 "hi"] + "intval" 100 + "strval" "good"}} + :error-text "" + :parse-errors :NONE}))) + +; EDN + +(deftest test-edn + (testing "EDN single value" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :edn})) o) + + ; one file + ["foo" "--val" "{:a 1, :b 2}"] + {:commandline {:_arguments [] + :val {:a 1 + :b 2}} + :error-text "" + :parse-errors :NONE})) + + (testing "Slurping multiline EDN" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :ednfile})) o) + + ; one file + ["foo" "--val" (resource "edn_simple.edn")] + {:commandline {:_arguments [] + :val {:list [1 2 "hi"] + :intval 100 + :strval "good"}} + :error-text "" + :parse-errors :NONE}))) + +; YAML + +(deftest test-yaml + (testing "YAML single value" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :yaml})) o) + + ; one file + ["foo" "--val" "a: 1\nb: 2"] + {:commandline {:_arguments [] + :val {"a" 1 + "b" 2}} + :error-text "" + :parse-errors :NONE})) + + (testing "Slurping multiline YAML" + (are [i o] + (= (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :yamlfile})) o) + + ; one file + ["foo" "--val" (resource "yaml_simple.yaml")] + {:commandline {:_arguments [] + :val {"list" [1 2 "hi"] + "intval" 100 + "strval" "good"}} + :error-text "" + :parse-errors :NONE})) + + (testing "Complex multiline YAML" + (are [i o] + (= (-> (parse-cmds-simpler + i + (mkDummyCfg {:option "val" :as "x" :type :yamlfile})) + (get-in [:commandline :val]) + (select-keys ["invoice" "date"])) o) + + ; one file + ["foo" "--val" (resource "yaml_full.yaml")] + {"invoice" 34843 + "date" #inst "2001-01-23"} + #_{:commandline {:_arguments [] + :val {"list" [1 2 "hi"] + "intval" 100 + "strval" "good"}} + :error-text "" + :parse-errors :NONE}))) + + +; =============== SETS ========== + + +(deftest set-help-values-test + (are [s o] + (= o (set-help-values s)) + + #{:a :b} "a|b" + #{:a} "a" + + #{"A" "b" "CD"} "A|CD|b")) + +(deftest set-find-value-test + (are [st v o] + (= o (set-find-value st v)) + + #{:a :b} "a" :a + #{:a :b} "A" :a + #{:a :b} "x" nil + + #{:a :b} ":a" :a + + #{"A" "B"} "a" "A" + #{"A" "B"} ":A" "A" + #{"A" "B"} "Q" nil + #{":A" ":B"} "a" ":A")) + +(deftest set-find-didyoumean-test + (are [st v cds] + (= cds (set-find-didyoumean st v)) + + #{:anna :babbo} "aNni" [:anna] + #{:anna :babbo :anno} ":anni" [:anna :anno] + #{:anna :babbo :anno} "ZebrA" [])) diff --git a/test-resources/lib_tests/cli_matic/three_lines.txt b/test-resources/lib_tests/cli_matic/three_lines.txt new file mode 100644 index 00000000..5fc4971c --- /dev/null +++ b/test-resources/lib_tests/cli_matic/three_lines.txt @@ -0,0 +1,3 @@ +L1 +Line2 +line 3 diff --git a/test-resources/lib_tests/cli_matic/utils_candidates_test.cljc b/test-resources/lib_tests/cli_matic/utils_candidates_test.cljc new file mode 100644 index 00000000..1822c543 --- /dev/null +++ b/test-resources/lib_tests/cli_matic/utils_candidates_test.cljc @@ -0,0 +1,58 @@ +(ns cli-matic.utils-candidates-test + (:require [clojure.test :refer :all]) + (:require [cli-matic.utils-candidates :refer [str-distance + candidate-suggestions]])) + +(defn abs [n] (max n (- n))) + +(defn float= + "Approximate float equality. + + Jeez, in each and every language I used in my life I + had to write this. Sometimes I wonder which way thing are + going. + " + [a b] + (let [fa (float a) + fb (float b) + err 0.001] + (> err (abs (- fa fb))))) + +(deftest str-distance-test + (are [s1 s2 d] + (float= d (str-distance s1 s2)) + + ; same string = 0 + "pippo" "pippo" 0 + + ; one change + "pippo" "Pippo" 0.20 + + ; compute as prc of longest + "pippox" "Pippo" 0.334 + + ; nils? + "xxx" nil 1 + + ; both empty + "" "" 0 + + ; both nil + nil nil 0)) + +(deftest candidate-suggestions-test + + (are [c t r] + (= r (vec (candidate-suggestions c t 0.5))) + + ; only one + ["foo" "bar" "baz" "buzz"] "baar" ["bar" "baz"] + + ;none + ["foo" "bar" "baz" "buzz"] "zebra" [] + + ; best comes first + ["foo" "bara" "barrr" "buzz" "o"] "bar" ["bara" "barrr"] + + ;none found + ["foo" "bara" "barrr" "buzz" "o"] "qaqaqa" [])) diff --git a/test-resources/lib_tests/cli_matic/utils_convert_config_test.cljc b/test-resources/lib_tests/cli_matic/utils_convert_config_test.cljc new file mode 100644 index 00000000..3a12c56c --- /dev/null +++ b/test-resources/lib_tests/cli_matic/utils_convert_config_test.cljc @@ -0,0 +1,45 @@ +(ns cli-matic.utils-convert-config-test + (:require [clojure.test :refer [is are deftest testing]] + [cli-matic.optionals :as OPT] + + [cli-matic.utils-convert-config + :refer [unmangle-fn-name + unmangle-fn + fn->className]])) + + +; +; Some example fns +; + + +(defn add_numbers [x] x) +(defn add-numbers [y] (inc y)) + + +; +; Tests +; + + +(deftest ^:skip-bb unmangle-fn-name-test + (are [i o] + (= o (unmangle-fn-name i)) + + ;; A moderately complex name + "cli_matic.utils_v2$convert_config_v1__GT_v2" + "cli-matic.utils-v2/convert-config-v1->v2")) + +(deftest ^:skip-bb unmangle-fn-test + (are [i o] + (= o (unmangle-fn i)) + + ;; A moderately complex name + add-numbers + 'cli-matic.utils-convert-config-test/add-numbers + +; add-numbers +; "cli-matic.utils-convert-config-test/add-numbers" + )) + +(OPT/orchestra-instrument) diff --git a/test-resources/lib_tests/cli_matic/utils_test.cljc b/test-resources/lib_tests/cli_matic/utils_test.cljc new file mode 100644 index 00000000..07b1b952 --- /dev/null +++ b/test-resources/lib_tests/cli_matic/utils_test.cljc @@ -0,0 +1,197 @@ +(ns cli-matic.utils-test + (:require [clojure.test :refer [is are deftest testing]] + [cli-matic.utils :refer [asString asStrVec + indent-string indent + pad deep-merge + all-subcommands-aliases + all-subcommands + canonicalize-subcommand + mk-cli-option + exit! exception-info]] + [cli-matic.platform-macros :refer [try-catch-all]] + [cli-matic.platform :as P] + [cli-matic.presets :as PR])) + +(deftest asString-test + + (are [i o] (= (asString i) o) + + ; a string + "x" "x" + + ; a vector of strings + ["a" "b"] "a\nb" + + ; a vector of vectors + ["a" ["b" "c"] "d"] "a\nb\nc\nd" + + + + ; add more cases..... + )) + +(deftest asStrVec-test + + (are [i o] + (= (asStrVec i) o) + + ; a string + "x" ["x"] + + ; a vector of strings + nil [] + + ; a vector of vectors + ["a" ["b" "c"] "d"] ["a" ["b" "c"] "d"] + + + + ; add more cases..... + )) + +(deftest indent-string-test + (are [i o] (= (indent-string i) o) + "a" " a")) + +(deftest indent-test + (are [i o] (= (indent i) o) + + ; a string + + "a" " a" + + ; a vector + ["a" "b"] [" a" " b"])) + +(deftest pad-test + (are [s s1 t o] (= (pad s s1 t) o) + + ; shorter + "pippo" nil 3 "pip" + + ; longer + "pippo" nil 7 "pippo " + + ; with merged string + "pippo" "pluto" 10 "pippo, plu")) + +(deftest deep-merge-test + + (is (= + {:one 4 :two {:three 6}} + + (deep-merge {:one 1 :two {:three 3}} + {:one 2 :two {:three 4}} + {:one 3 :two {:three 5}} + {:one 4 :two {:three 6}})))) + + +; +; cli-matic specific +; + + +(defn cmd_foo [& opts]) +(defn cmd_bar [& opts]) +(defn cmd_returnstructure [opts] + {:myopts opts + :somedata "hiyo"}) + +(def SIMPLE-SUBCOMMAND-CFG + {:app {:command "dummy" + :description "I am some command" + :version "0.1.2"} + :global-opts [{:option "aa" :as "A" :type :int} + {:option "bb" :as "B" :type :int}] + :commands [{:command "foo" :short "f" + :description "I am function foo" + :opts [{:option "cc" :as "C" :type :int} + {:option "dd" :as "D" :type :int}] + :runs cmd_foo} + + ; another one + {:command "bar" + :description "I am function bar" + :opts [{:option "ee" :as "E" :type :int} + {:option "ff" :as "F" :type :int}] + :runs cmd_bar} + + ; this one to check return structs + {:command "rets" + :description "I return a structure" + :opts [] + :runs cmd_returnstructure}]}) + +(deftest subcommands-and-aliases + (testing "Subcommands and aliases" + (is (= (all-subcommands-aliases SIMPLE-SUBCOMMAND-CFG) + {"bar" "bar" + "f" "foo" + "foo" "foo" + "rets" "rets"}))) + + (testing "All subcommands" + (is (= (all-subcommands SIMPLE-SUBCOMMAND-CFG) + #{"bar" + "f" + "foo" + "rets"}))) + + (testing "Canonicalize-subcommand" + (is (= (canonicalize-subcommand SIMPLE-SUBCOMMAND-CFG "foo") + "foo")) + (is (= (canonicalize-subcommand SIMPLE-SUBCOMMAND-CFG "f") + "foo")) + (is (= (canonicalize-subcommand SIMPLE-SUBCOMMAND-CFG "bar") + "bar")))) + +(deftest make-option + (testing "Build a tools.cli option" + (are [i o] + (= o (mk-cli-option i)) + + ; simplest example + {:option "extra" :short "x" :as "Port number" :type :int} + ["-x" "--extra N" "Port number" + :parse-fn P/parseInt] + + ; no shorthand + {:option "extra" :as "Port number" :type :int} + [nil "--extra N" "Port number" + :parse-fn P/parseInt] + + ; with a default + {:option "extra" :as "Port number" :type :int :default 13} + [nil "--extra N" "Port number" + :parse-fn P/parseInt :default 13] + + ; :present means there is no default + {:option "extra" :as "Port number" :type :int :default :present} + [nil "--extra N*" "Port number" + :parse-fn P/parseInt] + + ; with-flag option + {:option "true" :short "t" :as "A with-flag option" :type :with-flag :default false} + ["-t" "--[no-]true" "A with-flag option" + :default false] + + ; flag option + {:option "flag" :short "f" :as "A flag option" :type :flag :default false} + ["-f" "--flag F" "A flag option" + :parse-fn PR/parseFlag + :default false]))) + +(deftest test-exit! + + (is (= ["Ciao" 23] + (try-catch-all + (exit! "Ciao" 23) + (fn [t] + (exception-info t))))) + + (is (= -1 + (last (try-catch-all + (/ 10 0) + (fn [t] + (exception-info t))))))) + diff --git a/test-resources/lib_tests/cli_matic/utils_v2_test.cljc b/test-resources/lib_tests/cli_matic/utils_v2_test.cljc new file mode 100644 index 00000000..dbf55d31 --- /dev/null +++ b/test-resources/lib_tests/cli_matic/utils_v2_test.cljc @@ -0,0 +1,243 @@ +(ns cli-matic.utils-v2-test + (:require [clojure.test :refer [is are deftest testing]] + #?(:clj [cli-matic.platform-macros :refer [try-catch-all]] + :cljs [cli-matic.platform-macros :refer-macros [try-catch-all]]) + [cli-matic.optionals :as OPT] + [cli-matic.utils-v2 :refer [convert-config-v1->v2 + walk + can-walk? + as-canonical-path + get-most-specific-value]])) + + +; +; dummy functions +; + + +(defn add_numbers [x] x) +(defn subtract_numbers [x] x) + +(deftest convert-config-v1->v2-test + + (are [i o] (= (convert-config-v1->v2 i) o) + + ; ============== TEST 1 =============== + ; Input + {:app {:command "toycalc" + :description "A command-line toy calculator" + :version "0.0.1"} + + :global-opts [{:option "base" + :as "The number base for output" + :type :int + :default 10}] + + :commands [{:command "add" + :description "Adds two numbers together" + :opts [{:option "a" :as "Addendum 1" :type :int} + {:option "b" :as "Addendum 2" :type :int :default 0}] + :runs add_numbers} + + {:command "sub" + :description "Subtracts parameter B from A" + :opts [{:option "a" :as "Parameter A" :type :int :default 0} + {:option "b" :as "Parameter B" :type :int :default 0}] + :runs subtract_numbers}]} + + ; Output + {:command "toycalc" + :description "A command-line toy calculator" + :version "0.0.1" + :opts [{:as "The number base for output" + :default 10 + :option "base" + :type :int}] + :subcommands [{:command "add" + :description "Adds two numbers together" + :opts [{:as "Addendum 1" + :option "a" + :type :int} + {:as "Addendum 2" + :default 0 + :option "b" + :type :int}] + :runs add_numbers} + {:command "sub" + :description "Subtracts parameter B from A" + :opts [{:as "Parameter A" + :default 0 + :option "a" + :type :int} + {:as "Parameter B" + :default 0 + :option "b" + :type :int}] + :runs subtract_numbers}]})) + +(deftest walk-test + + (let [cfg {:command "toycalc" + :description "A command-line toy calculator" + :version "0.0.1" + :opts [{:as "The number base for output" + :default 10 + :option "base" + :type :int}] + :subcommands [{:command "add" + :description "Adds two numbers together" + :opts [{:as "Addendum 1" + :option "a" + :type :int} + {:as "Addendum 2" + :default 0 + :option "b" + :type :int}] + :runs add_numbers} + {:command "subc" + :description "Subtracts parameter B from A" + :opts [{:as "Parameter q" + :default 0 + :option "q" + :type :int}] + + :subcommands [{:command "sub" + :description "Subtracts" + :opts [{:as "Parameter A" + :default 0 + :option "a" + :type :int} + {:as "Parameter B" + :default 0 + :option "b" + :type :int}] + :runs subtract_numbers}]}]}] + + (are [p o] (= + (try-catch-all + (as-canonical-path (walk cfg p)) + (fn [_] :ERR)) + + o) + + ; es 1 + ["toycalc" "add"] + ["toycalc" "add"] + + ; es 2 + ["toycalc" "subc" "sub"] + ["toycalc" "subc" "sub"] + + ; not found + ["toycalc" "addq"] + :ERR + + ["toycalc" "subc" "xx"] + :ERR) + + (are [p o] (= (can-walk? cfg p) o) + + ; es 1 + ["toycalc" "add"] + true + + ; es 2 + ["toycalc" "subc" "sub"] + true + + ; not found + ["toycalc" "addq"] + false + + ["toycalc" "subc" "xx"] + false)) + + (let [cfg-one {:command "onlyone" + :description "A single subcommand" + :version "0.0.1" + :opts [{:as "The number base for output" + :default 10 + :option "base" + :type :int}] + :runs subtract_numbers}] + + (are [p o] (= + (try-catch-all + (as-canonical-path (walk cfg-one p)) + (fn [_] :ERR)) + + o) + + ; es 1 + ["onlyone"] + ["onlyone"] + + + ; Nothing + + + [] + ["onlyone"] + + ; notfound + ["toycalc" "subc" "xx"] + :ERR))) + + + +; :on-shutdown + + +(defn shutdown_BASE [] 0) +(defn shutdown_SUB [] 1) + +(deftest get-most-specific-value-test + + (let [cfg {:command "toycalc" + :description "A command-line toy calculator" + :version "0.0.1" + :on-shutdown shutdown_BASE + :opts [] + :subcommands [{:command "add" + :description "Adds two numbers together" + :opts [] + :runs add_numbers} + {:command "subc" + :description "Subtracts parameter B from A" + :opts [] + :subcommands [{:command "sub" + :description "Subtracts" + :opts [] + :runs subtract_numbers + :on-shutdown shutdown_SUB}]}]}] + + (are [p o] + (= (try-catch-all + (get-most-specific-value cfg p :on-shutdown "-NF-") + (fn [_] :ERR)) + o) + + ; Definito nella root + ["toycalc"] + shutdown_BASE + + ; Sempre da root + ["toycalc" "add"] + shutdown_BASE + + ; non definito, quindi uso root + ["toycalc" "subc"] + shutdown_BASE + + ; definito specifico + ["toycalc" "subc" "sub"] + shutdown_SUB + + ; not found + ["toycalc" "addq"] + :ERR + + ["toycalc" "subc" "xx"] + :ERR))) + +(OPT/orchestra-instrument) \ No newline at end of file diff --git a/test-resources/lib_tests/cli_matic/yaml_full.yaml b/test-resources/lib_tests/cli_matic/yaml_full.yaml new file mode 100644 index 00000000..58650841 --- /dev/null +++ b/test-resources/lib_tests/cli_matic/yaml_full.yaml @@ -0,0 +1,29 @@ +--- +invoice: 34843 +date : 2001-01-23 +bill-to: &id001 + given : Chris + family : Dumars + address: + lines: | + 458 Walkman Dr. + Suite #292 + city : Royal Oak + state : MI + postal : 48046 +ship-to: *id001 +product: + - sku : BL394D + quantity : 4 + description : Basketball + price : 450.00 + - sku : BL4438H + quantity : 1 + description : Super Hoop + price : 2392.00 +tax : 251.42 +total: 4443.52 +comments: > + Late afternoon is best. + Backup contact is Nancy + Billsmer @ 338-4338. diff --git a/test-resources/lib_tests/cli_matic/yaml_simple.yaml b/test-resources/lib_tests/cli_matic/yaml_simple.yaml new file mode 100644 index 00000000..1138cbdc --- /dev/null +++ b/test-resources/lib_tests/cli_matic/yaml_simple.yaml @@ -0,0 +1,3 @@ +list: [1, 2, "hi"] +intval: 100 +strval: "good"