diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c6174..49ad7a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased - Multiple `:require-macros` clauses with `:refer` now properly accumulate instead of overwriting each other +- Add `cherry.test` with clojure.test-compatible testing macros and async support +- Add test.check support ## 0.5.34 (2025-12-18) diff --git a/bb/integration_tests.clj b/bb/integration_tests.clj index bc8f74e..881a94f 100644 --- a/bb/integration_tests.clj +++ b/bb/integration_tests.clj @@ -6,7 +6,7 @@ (deftest macro-test (let [out (:out (sh {:err :inherit :dir "test-resources/test_project"} - "npx cherry run macro_test.cljs"))] + "npx cherry run macro_test.cljs"))] (is (str/includes? out "22")) (is (str/includes? out "1")))) @@ -28,6 +28,26 @@ (sh "npx" "cherry" "compile" tmp-file) (is (.exists (java.io.File. out-file)) "compile should create output file"))) +(deftest cross-platform-jvm-test + (let [{:keys [exit]} (sh {:err :inherit} + "clojure -M:test -n cherry.cross-platform-test")] + (is (zero? exit) "cross-platform test passes on JVM"))) + +(deftest test-check-jvm-test + (let [{:keys [exit]} (sh {:err :inherit} + "clojure -M:test -n cherry.test-check-test")] + (is (zero? exit) "test.check tests pass on JVM"))) + +(deftest cross-platform-cherry-test + (let [out (:out (sh {:err :inherit} + "node lib/cli.js run test/cherry/cross_platform_test.cljc"))] + (is (str/includes? out "0 failures") "cross-platform test passes on Cherry"))) + +(deftest cross-platform-test-check-test + (let [out (:out (sh {:err :inherit} + "node lib/cli.js run test/cherry/test_check_test.cljc"))] + (is (str/includes? out "0 failures") "test.check tests pass on Cherry"))) + (defn run-tests [] (shell {:dir "test-resources/test_project"} "npm install") (let [{:keys [fail error]} (t/run-tests 'integration-tests)] diff --git a/bb/tasks.clj b/bb/tasks.clj index 7b076fb..48e7caf 100644 --- a/bb/tasks.clj +++ b/bb/tasks.clj @@ -26,6 +26,11 @@ walk-config (edn/read-string (slurp (io/resource "cherry/clojure.walk.edn"))) set-config (edn/read-string (slurp (io/resource "cherry/clojure.set.edn"))) pprint-config (edn/read-string (slurp (io/resource "cherry/clojure.pprint.edn"))) + test-config (edn/read-string (slurp (io/resource "cherry/cherry.test.edn"))) + check-config (edn/read-string (slurp (io/resource "cherry/clojure.test.check.edn"))) + gen-config (edn/read-string (slurp (io/resource "cherry/clojure.test.check.generators.edn"))) + prop-config (edn/read-string (slurp (io/resource "cherry/clojure.test.check.properties.edn"))) + clojure-test-config (edn/read-string (slurp (io/resource "cherry/clojure.test.check.clojure_test.edn"))) reserved (edn/read-string (slurp (io/resource "cherry/js_reserved.edn")))] {:modules {:cljs.core {:exports (assoc (->namespace "cljs.core" (:vars core-config) reserved) @@ -41,7 +46,16 @@ :depends-on #{:cljs.core}} :cljs.pprint {:exports (->namespace "cljs.pprint" (:vars pprint-config) reserved) :entries '[cljs.pprint] - :depends-on #{:cljs.core :clojure.string}}}})) + :depends-on #{:cljs.core :clojure.string}} + :clojure.test {:exports (->namespace "cherry.test" (:vars test-config) reserved) + :entries '[cherry.test] + :depends-on #{:cljs.core :clojure.string}} + :clojure.test.check {:exports (merge (->namespace "clojure.test.check" (:vars check-config) reserved) + (->namespace "clojure.test.check.generators" (:vars gen-config) reserved) + (->namespace "clojure.test.check.properties" (:vars prop-config) reserved) + (->namespace "clojure.test.check.clojure-test" (:vars clojure-test-config) reserved)) + :entries '[clojure.test.check clojure.test.check.generators clojure.test.check.properties clojure.test.check.clojure-test] + :depends-on #{:cljs.core :clojure.string :cljs.pprint}}}})) (def test-config '{:compiler-options {:load-tests true} diff --git a/cherry-overrides/clojure/test/check/clojure_test.cljc b/cherry-overrides/clojure/test/check/clojure_test.cljc new file mode 100644 index 0000000..be53c83 --- /dev/null +++ b/cherry-overrides/clojure/test/check/clojure_test.cljc @@ -0,0 +1,18 @@ +(ns clojure.test.check.clojure-test) + +(defmacro defspec [name num-tests-or-prop & rest] + (let [[num-tests prop] (if (number? num-tests-or-prop) + [num-tests-or-prop (first rest)] + [100 num-tests-or-prop])] + `(def ~(vary-meta name assoc :test true) + (with-meta + (fn [] + (let [result# (~'quick-check ~num-tests ~prop)] + (if (:pass? result#) + (~'report {:type :pass + :message (str "Passed " ~num-tests " trials")}) + (~'report {:type :fail + :message (str "Failed after " (:num-tests result#) " trials") + :expected '~prop + :actual (:shrunk result#)})))) + {:name '~name})))) diff --git a/cherry-overrides/clojure/test/check/properties.cljc b/cherry-overrides/clojure/test/check/properties.cljc new file mode 100644 index 0000000..d3b5095 --- /dev/null +++ b/cherry-overrides/clojure/test/check/properties.cljc @@ -0,0 +1,18 @@ +(ns clojure.test.check.properties) + +(defn- binding-vars + "Extract variable names from let-style bindings vector." + [bindings] + (map first (partition 2 bindings))) + +(defn- binding-gens + "Extract generator expressions from let-style bindings vector." + [bindings] + (map second (partition 2 bindings))) + +(defmacro for-all + [bindings & body] + (let [for-all-sym 'for-all*] + `(~for-all-sym ~(vec (binding-gens bindings)) + (fn [~@(binding-vars bindings)] + ~@body)))) diff --git a/deps.edn b/deps.edn index 82b8394..cf79aea 100644 --- a/deps.edn +++ b/deps.edn @@ -8,12 +8,13 @@ {:cljs {:extra-paths ["test"] :extra-deps {thheller/shadow-cljs {:mvn/version "3.3.4"} funcool/promesa {:mvn/version "11.0.678"} - babashka/process {:mvn/version "0.6.23"}}} + babashka/process {:mvn/version "0.6.23"} + org.clojure/test.check {:mvn/version "1.1.3"}}} :test ;; added by neil {:extra-paths ["test"] :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd" :git/url "https://github.com/cognitect-labs/test-runner"} - babashka/fs {:mvn/version "0.5.27"}} + babashka/fs {:mvn/version "0.5.27"} + org.clojure/test.check {:mvn/version "1.1.3"}} :main-opts ["-m" "cognitect.test-runner"] - :exec-fn cognitect.test-runner.api/test}} - } + :exec-fn cognitect.test-runner.api/test}}} diff --git a/package.json b/package.json index b40ddcb..14f75a4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "cljs.core.js", "lib", "node_cli.js", - "index.js" + "index.js", + "cherry-overrides" ], "bin": { "cherry": "node_cli.js" diff --git a/resources/cherry/cherry.test.edn b/resources/cherry/cherry.test.edn new file mode 100644 index 0000000..c059b14 --- /dev/null +++ b/resources/cherry/cherry.test.edn @@ -0,0 +1,20 @@ +{:vars #{report + successful_QMARK_ + empty_env + get_current_env + set_env_BANG_ + clear_env_BANG_ + update_current_env_BANG_ + inc_report_counter_BANG_ + testing_contexts_str + testing_vars_str + test_var + compose_fixtures + join_fixtures + get_each_fixtures + set_each_fixtures_BANG_ + get_once_fixtures + set_once_fixtures_BANG_ + run_tests + async_QMARK_ + wrap_async}} diff --git a/resources/cherry/clojure.test.check.clojure_test.edn b/resources/cherry/clojure.test.check.clojure_test.edn new file mode 100644 index 0000000..cfc1cbb --- /dev/null +++ b/resources/cherry/clojure.test.check.clojure_test.edn @@ -0,0 +1,11 @@ +{:vars #{default_reporter_fn + _STAR_default_test_count_STAR_ + _STAR_default_opts_STAR_ + _STAR_report_shrinking_STAR_ + _STAR_report_trials_STAR_ + _STAR_report_completion_STAR_ + _STAR_trial_report_period_STAR_ + trial_report_dots + trial_report_periodic + assert_check + process_options}} diff --git a/resources/cherry/clojure.test.check.edn b/resources/cherry/clojure.test.check.edn new file mode 100644 index 0000000..93f6174 --- /dev/null +++ b/resources/cherry/clojure.test.check.edn @@ -0,0 +1 @@ +{:vars #{quick_check}} diff --git a/resources/cherry/clojure.test.check.generators.edn b/resources/cherry/clojure.test.check.generators.edn new file mode 100644 index 0000000..7930791 --- /dev/null +++ b/resources/cherry/clojure.test.check.generators.edn @@ -0,0 +1,57 @@ +{:vars #{any + any_printable + any_printable_ascii + bind + boolean + byte + bytes + char + char_alpha + char_alpha_numeric + char_alphanumeric + char_ascii + choose + double + double_STAR_ + elements + fmap + frequency + generate + hash_map + int + keyword + keyword_ns + large_integer + large_integer_STAR_ + let + list + list_distinct + list_distinct_by + map + nat + no_shrink + not_empty + one_of + recursive_gen + resize + return + sample + scale + set + shuffle + simple_type + simple_type_printable + sized + small_integer + string + string_alpha_numeric + string_alphanumeric + string_ascii + such_that + symbol + symbol_ns + tuple + uuid + vector + vector_distinct + vector_distinct_by}} diff --git a/resources/cherry/clojure.test.check.properties.edn b/resources/cherry/clojure.test.check.properties.edn new file mode 100644 index 0000000..ce29841 --- /dev/null +++ b/resources/cherry/clojure.test.check.properties.edn @@ -0,0 +1 @@ +{:vars #{for_all_STAR_ ErrorResult}} diff --git a/src/cherry/compiler.cljc b/src/cherry/compiler.cljc index f667ea9..bfca8a3 100644 --- a/src/cherry/compiler.cljc +++ b/src/cherry/compiler.cljc @@ -222,8 +222,9 @@ (or ;; used by cherry embed: (some-> env :macros (get nss) (get nms)) - (let [resolved-ns (get-in current-ns-state [:aliases nss] nss)] - (get-in ns-state [:macros resolved-ns nms])))) + (let [resolved-ns (get-in current-ns-state [:aliases nss] nss) + macro-ns (cc/resolve-macro-ns resolved-ns)] + (get-in ns-state [:macros macro-ns nms])))) (let [refers (:refers current-ns-state)] (when-let [macro-ns (get refers nms)] (or (some-> env :macros (get (symbol macro-ns)) (get nms)) diff --git a/src/cherry/compiler/node.cljs b/src/cherry/compiler/node.cljs index 7f0be1d..f3f61e7 100644 --- a/src/cherry/compiler/node.cljs +++ b/src/cherry/compiler/node.cljs @@ -6,6 +6,7 @@ [clojure.string :as str] [edamame.core :as e] [shadow.esm :as esm] + [squint.compiler-common :as cc] [squint.internal.node.utils :as utils])) (def sci (atom nil)) @@ -35,11 +36,12 @@ (.then prev (fn [_] (let [[macro-ns & {:keys [refer as]}] require-macros + actual-ns (cc/resolve-macro-ns macro-ns) macros (js/Promise.resolve - (do (eval-form (cond-> (list 'require (list 'quote macro-ns)) + (do (eval-form (cond-> (list 'require (list 'quote actual-ns)) reload (concat [:reload]))) (let [publics (eval-form - `(ns-publics '~macro-ns)) + `(ns-publics '~actual-ns)) ks (keys publics) vs (vals publics) vs (map deref vs) @@ -48,7 +50,7 @@ (.then macros (fn [macros] (swap! ns-state (fn [ns-state] - (cond-> (assoc-in ns-state [:macros macro-ns] macros) + (cond-> (-> ns-state (assoc-in [:macros macro-ns] macros) (assoc-in [:macros actual-ns] macros)) as (assoc-in [the-ns-name :aliases as] macro-ns) refer (update-in [the-ns-name :refers] merge diff --git a/src/cherry/compiler/sci.cljs b/src/cherry/compiler/sci.cljs index 05ba4f2..adc529a 100644 --- a/src/cherry/compiler/sci.cljs +++ b/src/cherry/compiler/sci.cljs @@ -2,12 +2,13 @@ (:require ["fs" :as fs] [cherry.compiler.node :as cn :refer [sci]] [sci.core :as sci] - [squint.internal.node.utils :refer [resolve-file]])) + [squint.internal.node.utils :as utils :refer [resolve-file]])) (defn slurp [f] (fs/readFileSync f "utf-8")) (def ctx (sci/init {:load-fn (fn [{:keys [namespace]}] + (utils/set-cfg! {:paths ["." "src" "cherry-overrides"]}) (let [f (resolve-file namespace) fstr (slurp f)] {:source fstr})) diff --git a/src/cherry/test.cljc b/src/cherry/test.cljc new file mode 100644 index 0000000..83f9fc1 --- /dev/null +++ b/src/cherry/test.cljc @@ -0,0 +1,93 @@ +(ns cherry.test) + +(defn assert-expr [msg form] + (let [op (when (sequential? form) (first form)) + loc (meta form) + line (:line loc) + column (:column loc) + report (fn [type expected actual ret] + `(do (clojure.test/report {:type ~type :message ~msg :expected ~expected :actual ~actual + ~@(when line [:line line]) + ~@(when column [:column column])}) + ~ret)) + default (let [sym (gensym "value")] + `(let [~sym ~form] + (if ~sym + ~(report :pass `'~form sym sym) + ~(report :fail `'~form sym sym))))] + (case op + = (if (= 2 (count (rest form))) + (let [[expected actual] (rest form) + expected-sym (gensym "expected") + actual-sym (gensym "actual") + result-sym (gensym "result")] + `(let [~expected-sym ~expected + ~actual-sym ~actual + ~result-sym (= ~expected-sym ~actual-sym)] + (if ~result-sym + ~(report :pass expected-sym actual-sym true) + ~(report :fail expected-sym actual-sym false)))) + default) + thrown? (let [klass (second form) + body (nthnext form 2) + e-sym (gensym "e")] + `(try + (do ~@body) + ~(report :fail `'~form "No exception thrown" false) + (catch :default ~e-sym + (if (instance? ~klass ~e-sym) + ~(report :pass `'~form e-sym true) + ~(report :fail `'~form e-sym false))))) + thrown-with-msg? (let [klass (second form) + re (nth form 2) + body (nthnext form 3) + e-sym (gensym "e")] + `(try + (do ~@body) + ~(report :fail `'~form "No exception thrown" false) + (catch :default ~e-sym + (if (instance? ~klass ~e-sym) + (if (re-find ~re (.-message ~e-sym)) + ~(report :pass `'~form e-sym true) + ~(report :fail `'~form `(str "Exception message \"" (.-message ~e-sym) "\" did not match " ~re) false)) + ~(report :fail `'~form e-sym false))))) + default))) + +(defmacro deftest [name & body] + (let [fn-meta (select-keys (meta name) [:async])] + `(def ~(vary-meta name assoc :test true) + (with-meta ~(with-meta `(fn [] ~@body) fn-meta) {:name '~name})))) + +(defmacro is + ([form] `(is ~form nil)) + ([form msg] + (let [loc (meta &form) + form-with-meta (if (and loc (or (sequential? form) (symbol? form))) + (with-meta form loc) + form)] + (assert-expr msg form-with-meta)))) + +(defmacro testing [string & body] + `(do + (clojure.test/update-current-env! [:testing-contexts] conj ~string) + (try + ~@body + (finally + (clojure.test/update-current-env! [:testing-contexts] rest))))) + +(defmacro deftest- [name & body] + `(deftest ~(vary-meta name assoc :private true) ~@body)) + +(defmacro are [bindings expr & args] + (assert (pos? (count bindings)) "are requires at least one binding") + (assert (seq args) "are requires at least one test case") + (let [binding-count (count bindings)] + (assert (zero? (mod (count args) binding-count)) + (str "are: arg count (" (count args) ") must be divisible by binding count (" binding-count ")")) + `(do ~@(for [arg-group (partition binding-count args)] + `(clojure.test/is (let [~@(interleave bindings arg-group)] ~expr)))))) + +(defmacro use-fixtures [type & fns] + (case type + :once `(clojure.test/set-once-fixtures! [~@fns]) + :each `(clojure.test/set-each-fixtures! [~@fns]))) diff --git a/src/cherry/test.cljs b/src/cherry/test.cljs new file mode 100644 index 0000000..5fd5b1f --- /dev/null +++ b/src/cherry/test.cljs @@ -0,0 +1,152 @@ +(ns cherry.test + (:require [clojure.string])) + +(def ^:dynamic *current-env* nil) + +(defn empty-env [] + {:report-counters {:test 0 :pass 0 :fail 0 :error 0} + :testing-vars () + :testing-contexts () + :once-fixtures [] + :each-fixtures []}) + +(defn get-current-env [] + (or *current-env* (empty-env))) + +(defn set-env! [env] + (set! *current-env* env) + env) + +(defn clear-env! [] + (set! *current-env* nil) + nil) + +(defn update-current-env! [ks f & args] + (let [env (get-current-env) + new-env (apply update-in env ks f args)] + (set-env! new-env))) + +(defn testing-contexts-str [] + (when-let [contexts (seq (:testing-contexts (get-current-env)))] + (clojure.string/join " " (reverse contexts)))) + +(defn testing-vars-str [] + (when-let [vars (seq (:testing-vars (get-current-env)))] + (clojure.string/join " " (map str vars)))) + +(defn inc-report-counter! [name] + (when (:report-counters (get-current-env)) + (update-current-env! [:report-counters name] (fnil inc 0)))) + +(defn current-test-str [] + (let [vars (testing-vars-str) + ctx (testing-contexts-str)] + (cond + (and vars ctx) (str vars " " ctx) + vars vars + ctx ctx + :else "test"))) + +(defn report [{:keys [type message expected actual line column file] :as m}] + (inc-report-counter! type) + (let [location (when (or line column file) + (str (when file (str file ":")) + (when line line) + (when column (str ":" column))))] + (case type + :pass nil + :fail (do + (js/console.error (str "FAIL in " (current-test-str) + (when location (str " (" location ")")))) + (when message (js/console.error " " message)) + (js/console.error " expected:" (pr-str expected)) + (js/console.error " actual:" (pr-str actual))) + :error (do + (js/console.error (str "ERROR in " (current-test-str) + (when location (str " (" location ")")))) + (when message (js/console.error " " message)) + (when expected (js/console.error " expected:" (pr-str expected))) + (js/console.error " actual:" (pr-str actual))) + :begin-test-ns (js/console.log "\nTesting" (str (:ns m))) + :end-test-ns nil + :begin-test-var nil + :end-test-var nil + :summary (let [{:keys [test pass fail error]} (:report-counters (get-current-env))] + (js/console.log "\nRan" test "tests containing" (+ pass fail error) "assertions.") + (js/console.log (str fail) "failures," (str error) "errors.")) + (js/console.log "Unknown report type:" type m)))) + +(defn successful? [results] + (and (zero? (:fail results 0)) + (zero? (:error results 0)))) + +(defn async? [x] + (instance? js/Promise x)) + +(defn wrap-async + "Wraps setup/teardown fns into async-aware fixture. + Teardown waits for Promise resolution if test is async." + [setup teardown] + (fn [test-fn] + (setup) + (let [result (test-fn)] + (if (async? result) + (.finally result teardown) + (do (teardown) result))))) + +(defn compose-fixtures [f1 f2] + (fn [g] (f1 (fn [] (f2 g))))) + +(defn join-fixtures [fixtures] + (reduce compose-fixtures (fn [f] (f)) fixtures)) + +(defn get-each-fixtures [] + (get-in (get-current-env) [:each-fixtures] [])) + +(defn set-each-fixtures! [fixtures] + (update-current-env! [:each-fixtures] (constantly fixtures))) + +(defn get-once-fixtures [] + (get-in (get-current-env) [:once-fixtures] [])) + +(defn set-once-fixtures! [fixtures] + (update-current-env! [:once-fixtures] (constantly fixtures))) + +(defn test-var [v] + (when (fn? v) + (let [test-name (or (:name (meta v)) "anonymous") + each-fixtures (get-each-fixtures) + wrapped-test (if (seq each-fixtures) + (fn [] ((join-fixtures each-fixtures) v)) + v) + pop-test-name! #(update-current-env! [:testing-vars] rest)] + (update-current-env! [:testing-vars] conj test-name) + (inc-report-counter! :test) + (try + (let [result (wrapped-test)] + (if (async? result) + (-> result + (.then (fn [r] (pop-test-name!) r)) + (.catch (fn [e] + (report {:type :error :message (.-message e) :expected nil :actual e}) + (pop-test-name!)))) + (do (pop-test-name!) result))) + (catch :default e + (pop-test-name!) + (report {:type :error :message (.-message e) :expected nil :actual e})))))) + +(defn run-tests + "Runs test-vars with once-fixtures. Returns Promise if any test is async." + [& test-vars] + (let [once-fixtures (get-once-fixtures) + run-all (fn [] + (reduce + (fn [chain v] + (if (async? chain) + (.then chain (fn [_] (test-var v))) + (test-var v))) + nil + test-vars))] + (if (seq once-fixtures) + ((join-fixtures once-fixtures) run-all) + (run-all)))) diff --git a/test/cherry/cross_platform_test.cljc b/test/cherry/cross_platform_test.cljc new file mode 100644 index 0000000..cc7d201 --- /dev/null +++ b/test/cherry/cross_platform_test.cljc @@ -0,0 +1,350 @@ +(ns cherry.cross-platform-test + (:require [clojure.test :as t #?@(:clj [:refer [deftest is testing are]])] + [clojure.string :as str] + #?@(:cljs [[clojure.test.check :refer [quick-check]] + [clojure.test.check.generators :as gen] + [clojure.test.check.properties :refer [for-all*]]] + :clj [[clojure.test.check.generators :as gen] + [clojure.test.check.properties :as prop]])) + #?(:cljs (:require-macros [clojure.test :as t :refer [deftest deftest- is testing are]] + [clojure.test.check.clojure-test :refer [defspec]] + [clojure.test.check.properties :refer [for-all]]))) + +(defonce test-db (atom nil)) + +(deftest arithmetic-test + (testing "basic arithmetic with referred macros" + (is (= 4 (+ 2 2))) + (is (= 6 (* 2 3)))) + (t/testing "basic arithmetic with qualified macros" + (t/is (= 10 (+ 5 5))) + (t/is (= 12 (* 3 4))))) + +(deftest equality-test + (testing "value equality" + (is (= [1 2 3] [1 2 3])) + (is (= {:a 1} {:a 1})))) + +(deftest are-test + (testing "table-driven tests" + (are [x y expected] (= expected (+ x y)) + 1 1 2 + 2 3 5 + 10 20 30))) + +#?(:cljs + (deftest are-runs-all-cases-test + (testing "are runs every case" + (let [saved-env (t/get-current-env) + saved-counters (:report-counters saved-env)] + (t/set-env! (assoc (t/empty-env) :testing-vars (:testing-vars saved-env))) + (let [pass-before (get-in (t/get-current-env) [:report-counters :pass] 0)] + (are [x] (pos? x) + 1 2 3 4 5) + (let [pass-after (get-in (t/get-current-env) [:report-counters :pass] 0) + pass-count (- pass-after pass-before)] + (t/set-env! (assoc saved-env :report-counters saved-counters)) + (when-not (= 5 pass-count) + (throw (js/Error. (str "are should run all 5 cases, but ran " pass-count)))) + (is true "are correctly runs all cases"))))))) + +(deftest exception-test + (testing "thrown?" + (is (thrown? #?(:clj Exception :cljs js/Error) + (throw #?(:clj (Exception. "test") :cljs (js/Error. "test")))))) + (testing "thrown-with-msg?" + (is (thrown-with-msg? #?(:clj Exception :cljs js/Error) + #"test" + (throw #?(:clj (Exception. "test") :cljs (js/Error. "test"))))))) + +#?(:cljs + (deftest verify-thrown-reports-missing-exception + (testing "thrown? fails when no exception thrown" + (let [saved-env (t/get-current-env) + saved-counters (:report-counters saved-env)] + (t/set-env! (assoc (t/empty-env) :testing-vars (:testing-vars saved-env))) + (let [fail-before (get-in (t/get-current-env) [:report-counters :fail] 0)] + (is (thrown? js/Error (+ 1 1))) + (let [fail-after (get-in (t/get-current-env) [:report-counters :fail] 0)] + (t/set-env! (assoc saved-env :report-counters saved-counters)) + (when-not (> fail-after fail-before) + (throw (js/Error. (str "thrown? should fail when no exception - fail-before: " fail-before + " fail-after: " fail-after)))) + (is true "thrown? correctly fails when no exception"))))))) + +#?(:cljs + (deftest verify-thrown-with-msg-checks-pattern + (testing "thrown-with-msg? fails on wrong message pattern" + (let [saved-env (t/get-current-env) + saved-counters (:report-counters saved-env)] + (t/set-env! (assoc (t/empty-env) :testing-vars (:testing-vars saved-env))) + (let [fail-before (get-in (t/get-current-env) [:report-counters :fail] 0)] + (is (thrown-with-msg? js/Error #"WRONG PATTERN" + (throw (js/Error. "actual message")))) + (let [fail-after (get-in (t/get-current-env) [:report-counters :fail] 0)] + (t/set-env! (assoc saved-env :report-counters saved-counters)) + (when-not (> fail-after fail-before) + (throw (js/Error. (str "thrown-with-msg? should fail on wrong pattern - fail-before: " fail-before + " fail-after: " fail-after)))) + (is true "thrown-with-msg? correctly fails on wrong pattern"))))))) + +(deftest join-fixtures-empty-test + (testing "join-fixtures with empty list just calls test" + (let [called (atom false) + joined (t/join-fixtures [])] + (joined (fn [] (reset! called true))) + (is @called "test function should be called even with no fixtures")))) + +(deftest fixtures-test + (testing "compose-fixtures nests first around second" + (let [users-fixture (fn [f] + (reset! test-db {:users {}}) + (f) + (reset! test-db nil)) + data-fixture (fn [f] + (swap! test-db assoc-in [:users :user-1] {:name "Alice"}) + (f)) + composed (t/compose-fixtures users-fixture data-fixture)] + (composed (fn [] + (is (= "Alice" (get-in @test-db [:users :user-1 :name]))))) + (is (nil? @test-db) "fixture teardown clears database"))) + (testing "join-fixtures does the same for a collection" + (let [users-fixture (fn [f] + (reset! test-db {}) + (f) + (reset! test-db nil)) + alice-fixture (fn [f] + (swap! test-db assoc :user-1 {:name "Alice" :age 30}) + (f)) + bob-fixture (fn [f] + (swap! test-db assoc :user-2 {:name "Bob" :age 25}) + (f)) + joined (t/join-fixtures [users-fixture alice-fixture bob-fixture])] + (joined (fn [] + (is (= "Alice" (get-in @test-db [:user-1 :name]))) + (is (= "Bob" (get-in @test-db [:user-2 :name]))) + (is (= 2 (count @test-db))))) + (is (nil? @test-db) "fixture teardown clears database")))) + +#?(:cljs + (deftest each-fixtures-applied-test + (testing "each fixtures provide fresh database for each test" + (let [fixture (fn [test-fn] + (reset! test-db {:user-1 {:name "Alice" :age 30}}) + (test-fn) + (reset! test-db nil)) + test-1 (with-meta + (fn [] + (is (= "Alice" (get-in @test-db [:user-1 :name]))) + (swap! test-db assoc :user-1 {:name "Modified"})) + {:name 'test-1}) + test-2 (with-meta + (fn [] + (is (= "Alice" (get-in @test-db [:user-1 :name])) "fixture gives fresh DB")) + {:name 'test-2})] + (t/set-each-fixtures! [fixture]) + (t/test-var test-1) + (t/test-var test-2) + (t/set-each-fixtures! []) + (is (nil? @test-db) "fixture teardown clears database"))))) + +#?(:cljs + (deftest once-fixtures-with-run-tests-test + (testing "once fixtures set up database once for all tests" + (let [saved-env (t/get-current-env) + setup-count (atom 0) + teardown-count (atom 0) + fixture (fn [test-fn] + (swap! setup-count inc) + (reset! test-db {:user-1 {:name "Alice" :age 30} + :user-2 {:name "Bob" :age 25}}) + (test-fn) + (reset! test-db nil) + (swap! teardown-count inc)) + test-1 (with-meta + (fn [] + (is (= "Alice" (get-in @test-db [:user-1 :name]))) + (is (= 30 (get-in @test-db [:user-1 :age])))) + {:name 'test-1}) + test-2 (with-meta + (fn [] + (is (= "Bob" (get-in @test-db [:user-2 :name]))) + (is (= 25 (get-in @test-db [:user-2 :age])))) + {:name 'test-2})] + (t/set-env! (t/empty-env)) + (t/set-once-fixtures! [fixture]) + (t/run-tests test-1 test-2) + (t/set-env! saved-env) + (is (= 1 @setup-count) "setup runs exactly once") + (is (= 1 @teardown-count) "teardown runs exactly once"))))) + +#?(:cljs + (deftest ^:async async-test + (testing "async with setTimeout" + (js-await + (js/Promise. + (fn [resolve] + (js/setTimeout + (fn [] + (is (= 1 1)) + (resolve)) + 10))))))) + +#?(:cljs + (deftest verify-is-reports-failures + (testing "is reports failures" + (let [saved-env (t/get-current-env) + saved-counters (:report-counters saved-env)] + (t/set-env! (assoc (t/empty-env) :testing-vars (:testing-vars saved-env))) + (let [fail-count-before (get-in (t/get-current-env) [:report-counters :fail] 0)] + (is (= 1 2)) + (let [fail-count-after (get-in (t/get-current-env) [:report-counters :fail] 0) + pass-count-after (get-in (t/get-current-env) [:report-counters :pass] 0)] + (t/set-env! (assoc saved-env :report-counters saved-counters)) + (when-not (> fail-count-after fail-count-before) + (throw (js/Error. (str "is should report failures - fail-count-before: " fail-count-before + " fail-count-after: " fail-count-after + " pass-count-after: " pass-count-after)))) + (is true "is correctly reports failures"))))))) + +#?(:cljs + (deftest verify-assert-expr-default-reports-failures + (testing "non-equality assertions report failures via assert-expr-default" + (let [saved-env (t/get-current-env) + saved-counters (:report-counters saved-env)] + (t/set-env! (assoc (t/empty-env) :testing-vars (:testing-vars saved-env))) + (let [fail-count-before (get-in (t/get-current-env) [:report-counters :fail] 0)] + (is (neg? 1)) + (let [fail-count-after (get-in (t/get-current-env) [:report-counters :fail] 0)] + (t/set-env! (assoc saved-env :report-counters saved-counters)) + (when-not (> fail-count-after fail-count-before) + (throw (js/Error. (str "assert-expr-default should report failures - fail-count-before: " fail-count-before + " fail-count-after: " fail-count-after)))) + (is true "assert-expr-default correctly reports failures"))))))) + +(deftest testing-context-test + (testing "outer context" + (is (str/includes? (t/testing-contexts-str) "outer context")) + (testing "inner context" + (is (str/includes? (t/testing-contexts-str) "inner context"))))) + +#?(:cljs + (deftest successful-test + (testing "successful? is false with failures" + (is (not (t/successful? {:fail 1 :error 0})))) + (testing "successful? is false with errors" + (is (not (t/successful? {:fail 0 :error 1})))) + (testing "successful? is true when both zero" + (is (t/successful? {:fail 0 :error 0}))))) + +#?(:cljs + (deftest test-var-counter-test + (testing "test-var increments :test counter" + (let [test-before (get-in (t/get-current-env) [:report-counters :test] 0)] + (t/test-var (fn [] nil)) + (let [test-after (get-in (t/get-current-env) [:report-counters :test] 0)] + (is (= 1 (- test-after test-before)))))))) + +#?(:cljs + (deftest ^:async wrap-async-fixture-test + (testing "wrap-async waits for async test before teardown" + (let [log (atom []) + fixture (t/wrap-async + #(swap! log conj :setup) + #(swap! log conj :teardown)) + async-test (fn [] + (js/Promise. + (fn [resolve] + (js/setTimeout + (fn [] + (swap! log conj :test-done) + (resolve)) + 20)))) + result (fixture async-test)] + (is (instance? js/Promise result)) + (js-await result) + (js-await (js/Promise. (fn [resolve] (js/setTimeout resolve 30)))) + (is (= [:setup :test-done :teardown] @log) + "teardown should wait for async test"))))) + +#?(:cljs + (deftest- ^:async private-async-helper + (js-await (js/Promise. (fn [resolve] (js/setTimeout resolve 5)))) + (is true))) + +#?(:cljs + (deftest deftest-private-async-test + (testing "deftest- works with ^:async" + (let [result (t/test-var private-async-helper)] + (is (instance? js/Promise result) "deftest- ^:async returns Promise"))))) + +#?(:cljs + (deftest ^:async run-tests-async-test + (testing "run-tests chains async tests correctly" + (let [log (atom []) + saved-fixtures (t/get-once-fixtures)] + (t/set-once-fixtures! [(t/wrap-async + #(swap! log conj :setup) + #(swap! log conj :teardown))]) + (let [sync-first (with-meta + (fn [] + (swap! log conj :test-1) + (is (= 1 1) "sync-first assertion")) + {:name 'sync-first}) + async-delayed (with-meta + (fn [] + (js/Promise. + (fn [resolve] + (js/setTimeout + (fn [] + (swap! log conj :test-2) + (is (pos? 42) "async-delayed assertion") + (resolve)) + 20)))) + {:name 'async-delayed}) + sync-last (with-meta + (fn [] + (swap! log conj :test-3) + (is (string? "yes") "sync-last assertion")) + {:name 'sync-last}) + result (t/run-tests sync-first async-delayed sync-last)] + (js-await result) + (js-await (js/Promise. (fn [resolve] (js/setTimeout resolve 30)))) + (is (= [:setup :test-1 :test-2 :test-3 :teardown] @log) + "run-tests should chain async tests in order") + (t/set-once-fixtures! saved-fixtures)))))) + +#?(:clj + (defn -main [] + (let [result (t/run-tests 'cherry.cross-platform-test)] + (when-not (t/successful? result) + (System/exit 1)))) + :cljs + (defn ^:async -main [] + (t/set-env! (t/empty-env)) + (t/test-var arithmetic-test) + (t/test-var equality-test) + (t/test-var are-test) + (t/test-var are-runs-all-cases-test) + (t/test-var exception-test) + (t/test-var verify-thrown-reports-missing-exception) + (t/test-var verify-thrown-with-msg-checks-pattern) + (t/test-var join-fixtures-empty-test) + (t/test-var fixtures-test) + (t/test-var each-fixtures-applied-test) + (t/test-var once-fixtures-with-run-tests-test) + (js-await (t/test-var async-test)) + (t/test-var verify-is-reports-failures) + (t/test-var verify-assert-expr-default-reports-failures) + (t/test-var testing-context-test) + (t/test-var successful-test) + (t/test-var test-var-counter-test) + (js-await (t/test-var wrap-async-fixture-test)) + (t/test-var deftest-private-async-test) + (js-await (t/test-var run-tests-async-test)) + (t/report {:type :summary}) + (let [results (:report-counters (t/get-current-env))] + (when-not (t/successful? results) + (js/process.exit 1))))) + +#?(:cljs (-main)) diff --git a/test/cherry/test_check_test.cljc b/test/cherry/test_check_test.cljc new file mode 100644 index 0000000..58bd264 --- /dev/null +++ b/test/cherry/test_check_test.cljc @@ -0,0 +1,102 @@ +(ns cherry.test-check-test + (:require [clojure.test.check.generators :as gen] + #?@(:clj [[clojure.test.check.properties :as prop] + [clojure.test.check.clojure-test :refer [defspec]]] + :cljs [[clojure.test.check :refer [quick-check]] + [clojure.test.check.clojure-test :as tc-test] + [clojure.test.check.properties :as prop :refer [for-all*]] + [clojure.test :refer [report empty-env set-env! get-current-env + successful? test-var]]])) + #?(:cljs (:require-macros [clojure.test.check.clojure-test :refer [defspec]] + [clojure.test.check.properties :refer [for-all]]))) + +(defspec simple-integer-property 100 + (prop/for-all [x gen/small-integer] + (= x x))) + +(defspec vector-reversal-property 50 + (prop/for-all [v (gen/vector gen/small-integer)] + (= v (reverse (reverse v))))) + +(defspec generator-composition 50 + (prop/for-all [x (gen/fmap inc gen/small-integer)] + (> x (- x 1)))) + +(defspec multiple-generators 50 + (prop/for-all [x gen/small-integer + y gen/small-integer] + (= (+ x y) (+ y x)))) + +(defspec string-concatenation 50 + (prop/for-all [s1 gen/string-ascii + s2 gen/string-ascii] + (= (count (str s1 s2)) + (+ (count s1) (count s2))))) + +(defspec map-property 50 + (prop/for-all [m (gen/map gen/keyword gen/small-integer)] + (= (count m) (count (keys m))))) + +(defspec set-property 50 + (prop/for-all [s (gen/set gen/small-integer)] + (<= (count s) 100))) + +(defspec such-that-property 50 + (prop/for-all [x (gen/such-that pos? gen/small-integer 100)] + (pos? x))) + +(defspec pos-int-property 50 + (prop/for-all [x (gen/large-integer* {:min 1})] + (pos? x))) + +(defspec neg-int-property 50 + (prop/for-all [x (gen/large-integer* {:max -1})] + (neg? x))) + +(defspec nat-property 50 + (prop/for-all [x gen/nat] + (nat-int? x))) + +(defspec list-distinct-property 50 + (prop/for-all [xs (gen/list-distinct gen/small-integer)] + (= (count xs) (count (set xs))))) + +(defspec recursive-gen-property 20 + (prop/for-all [tree (gen/recursive-gen + (fn [inner] (gen/vector inner 0 3)) + gen/small-integer)] + (or (number? tree) (vector? tree)))) + +(defspec resize-property 50 + (prop/for-all [v (gen/resize 5 (gen/vector gen/small-integer))] + (<= (count v) 10))) + +#?(:cljs + (defn test-clojure-test-vars [] + (assert (number? tc-test/*default-test-count*) + "*default-test-count* should be a number"))) + +#?(:cljs + (defn -main [] + (set-env! (empty-env)) + (test-var simple-integer-property) + (test-var vector-reversal-property) + (test-var generator-composition) + (test-var multiple-generators) + (test-var string-concatenation) + (test-var map-property) + (test-var set-property) + (test-var such-that-property) + (test-var pos-int-property) + (test-var neg-int-property) + (test-var nat-property) + (test-var list-distinct-property) + (test-var recursive-gen-property) + (test-var resize-property) + (test-clojure-test-vars) + (report {:type :summary}) + (let [results (:report-counters (get-current-env))] + (when-not (successful? results) + (js/process.exit 1))))) + +#?(:cljs (-main))