;; clojure.test — minimal compatible implementation for clojurust.
;; Covers: deftest, is, are, testing, run-tests, test-var.
(ns clojure.test
(:require [clojure.template]))
;; ── Dynamic state ─────────────────────────────────────────────────────────────
(def ^:dynamic *testing-vars* '())
(def ^:dynamic *testing-contexts* [])
(def ^:dynamic *report-counters* nil)
(def ^:dynamic *test-out* nil)
(def ^:dynamic *verbose* false)
;; ── Reporting ─────────────────────────────────────────────────────────────────
(defmulti report :type)
(defmethod report :default [m]
(println "Unknown report type:" (:type m)))
(defmethod report :pass [m]
(when *verbose*
(println "\nPASS in" (str (first *testing-vars*)))
(doseq [ctx *testing-contexts*]
(println " " ctx))
(println "expected:" (pr-str (:expected m))))
(when *report-counters*
(set! *report-counters* (update *report-counters* :pass inc))))
(defmethod report :fail [m]
(println "\nFAIL in" (str (first *testing-vars*)))
(doseq [ctx *testing-contexts*]
(println " " ctx))
(when (:message m)
(println "Message:" (:message m)))
(println "expected:" (pr-str (:expected m)))
(println " actual:" (pr-str (:actual m)))
(when *report-counters*
(set! *report-counters* (update *report-counters* :fail inc))))
(defmethod report :error [m]
(println "\nERROR in" (str (first *testing-vars*)))
(doseq [ctx *testing-contexts*]
(println " " ctx))
(when (:message m)
(println "Message:" (:message m)))
(println "expected:" (pr-str (:expected m)))
(println " actual:" (str (:actual m)))
(when *report-counters*
(set! *report-counters* (update *report-counters* :error inc))))
(defmethod report :begin-test-ns [m]
(println "\nTesting" (ns-name (:ns m))))
(defmethod report :end-test-ns [_m])
(defmethod report :summary [m]
(println "\nRan" (:test m) "tests containing"
(+ (:pass m) (:fail m) (:error m)) "assertions.")
(println (:fail m) "failures," (:error m) "errors."))
;; ── Assertion macro ───────────────────────────────────────────────────────────
;; Note: uses __is-result__ / __is-e__ instead of gensym to avoid reader issues.
(defmacro is
([form] `(is ~form nil))
([form msg]
(cond
;; (is (thrown? ExClass expr)) or (is (thrown? expr)) — expect an exception.
(and (seq? form) (= 'thrown? (first form)))
(let [expr (if (= 3 (count form))
(nth form 2)
(nth form 1))]
`(try
~expr
(do (report {:type :fail :expected '~form :actual "no exception thrown" :message ~msg})
false)
(catch Exception __is-e__
(report {:type :pass :expected '~form :actual __is-e__ :message ~msg})
true)))
;; (is (<cmp> a b ...)) — evaluate each argument once and report the
;; individual values. A failing (is (= (f) :bar)) reports its actual as
;; (not (= <value-of-f> :bar)) rather than just `false`, so the user can
;; see both sides of the comparison without re-running anything.
(and (seq? form)
(contains? '#{= not= == < <= > >= identical?} (first form)))
(let [pred (first form)
args (rest form)
gsyms (map (fn [_] (gensym "__is-arg__")) args)
binds (vec (mapcat (fn [s a] [s a]) gsyms args))]
`(try
(let ~binds
(if (~pred ~@gsyms)
(do (report {:type :pass
:expected '~form
:actual (list '~pred ~@gsyms)
:message ~msg})
true)
(do (report {:type :fail
:expected '~form
:actual (list 'not (list '~pred ~@gsyms))
:message ~msg})
false)))
(catch Exception __is-e__
(report {:type :error :expected '~form :actual __is-e__ :message ~msg})
false)))
;; Normal boolean assertion — no structural introspection available.
:else
`(try
(let [__is-result__ ~form]
(if __is-result__
(do (report {:type :pass :expected '~form :actual __is-result__ :message ~msg})
true)
(do (report {:type :fail :expected '~form :actual __is-result__ :message ~msg})
false)))
(catch Exception __is-e__
(report {:type :error :expected '~form :actual __is-e__ :message ~msg})
false)))))
;; ── are macro ─────────────────────────────────────────────────────────────────
(defmacro are
[argv expr & args]
(if (or (and (empty? args) (not (empty? argv)))
(and (not (empty? args)) (empty? argv)))
`(do)
`(clojure.template/do-template ~argv (is ~expr) ~@args)))
;; ── Context macro ─────────────────────────────────────────────────────────────
(defmacro testing [string & body]
`(binding [*testing-contexts* (conj *testing-contexts* ~string)]
~@body))
;; ── Test definition ───────────────────────────────────────────────────────────
(defmacro deftest [name & body]
`(do
(def ~name (fn [] ~@body))
(alter-meta! (var ~name) assoc :test (fn [] ~@body))
(var ~name)))
;; ── Test runner ───────────────────────────────────────────────────────────────
(defn test-var [v]
(let [test-fn (get (meta v) :test)]
(when test-fn
(binding [*testing-vars* (conj *testing-vars* v)]
(try
(test-fn)
(catch Exception __tv-e__
(report {:type :error
:expected nil
:actual __tv-e__
:message nil})))))))
(defn run-tests
([] (run-tests *ns*))
([ns]
(let [ns (if (symbol? ns) (find-ns ns) ns)]
(binding [*report-counters* {:test 0 :pass 0 :fail 0 :error 0}]
(report {:type :begin-test-ns :ns ns})
(let [test-vars (filter (fn [v] (get (meta v) :test))
(vals (ns-publics ns)))]
(doseq [v test-vars]
(set! *report-counters* (update *report-counters* :test inc))
(test-var v)))
(report {:type :end-test-ns :ns ns})
(report {:type :summary
:test (:test *report-counters*)
:pass (:pass *report-counters*)
:fail (:fail *report-counters*)
:error (:error *report-counters*)})
*report-counters*))))