diff --git a/.clj-kondo/com.github.metabase/hawk/config.edn b/.clj-kondo/com.github.metabase/hawk/config.edn index 422faae..a34a467 100644 --- a/.clj-kondo/com.github.metabase/hawk/config.edn +++ b/.clj-kondo/com.github.metabase/hawk/config.edn @@ -1,4 +1,4 @@ {:linters {:unresolved-symbol {:exclude - [(clojure.test/is [partial= query= re= schema= sql= =?])]}}} + [(clojure.test/is [partial= re= schema= =?])]}}} diff --git a/src/mb/hawk/core.clj b/src/mb/hawk/core.clj index 3573d75..c3f9b33 100644 --- a/src/mb/hawk/core.clj +++ b/src/mb/hawk/core.clj @@ -13,9 +13,8 @@ [environ.core :as env] [mb.hawk.assert-exprs] [mb.hawk.init :as hawk.init] - [mb.hawk.junit :as hawk.junit] [mb.hawk.parallel :as hawk.parallel] - [mb.hawk.speak :as hawk.speak] + [mb.hawk.reporter.interface :as hawk.reporter] [mb.hawk.util :as u])) (set! *warn-on-reflection* true) @@ -137,18 +136,6 @@ (alter-var-root #'t/test-var (constantly run-test)) -(defn- reporter - "Create a new test reporter/event handler, a function with the signature `(handle-event event)` that gets called once - for every [[clojure.test]] event, including stuff like `:begin-test-run`, `:end-test-var`, and `:fail`." - [options] - (let [stdout-reporter (case (:mode options) - (:cli/ci :repl) eftest.report.pretty/report - :cli/local eftest.report.progress/report)] - (fn handle-event [event] - (hawk.junit/handle-event! event) - (hawk.speak/handle-event! event) - (stdout-reporter event)))) - (def ^:private env-mode (cond (env/env :hawk-mode) @@ -184,7 +171,7 @@ (merge {:capture-output? false :multithread? :vars - :report (reporter options)} + :report (hawk.reporter/reporter options)} options)) @*parallel-test-counter*)))))) diff --git a/src/mb/hawk/junit.clj b/src/mb/hawk/junit.clj deleted file mode 100644 index bd36341..0000000 --- a/src/mb/hawk/junit.clj +++ /dev/null @@ -1,109 +0,0 @@ -(ns mb.hawk.junit - (:require - [clojure.test :as t] - [mb.hawk.junit.write :as write])) - -(defmulti ^:private handle-event!* - {:arglists '([event])} - :type) - -(defn handle-event! - "Write JUnit output for a `clojure.test` event such as success or failure." - [{test-var :var, :as event}] - (let [test-var (or test-var - (when (seq t/*testing-vars*) - (last t/*testing-vars*))) - event (merge - {:var test-var} - event - (when test-var - {:ns (:ns (meta test-var))}))] - (try - (handle-event!* event) - (catch Throwable e - (throw (ex-info (str "Error handling event: " (ex-message e)) - {:event event} - e)))))) - -;; for unknown event types (e.g. `:clojure.test.check.clojure-test/trial`) just ignore them. -(defmethod handle-event!* :default - [_]) - -(defmethod handle-event!* :begin-test-run - [_] - (write/clean-output-dir!) - (write/create-thread-pool!)) - -(defmethod handle-event!* :summary - [_] - (write/wait-for-writes-to-finish)) - -(defmethod handle-event!* :begin-test-ns - [{test-ns :ns}] - (alter-meta! - test-ns assoc ::context - {:start-time-ms (System/currentTimeMillis) - :timestamp (java.time.OffsetDateTime/now) - :test-count 0 - :error-count 0 - :failure-count 0 - :results []})) - -(defmethod handle-event!* :end-test-ns - [{test-ns :ns, :as event}] - (let [context (::context (meta test-ns)) - result (merge - event - context - {:duration-ms (- (System/currentTimeMillis) (:start-time-ms context))})] - (write/write-ns-result! result))) - -(defmethod handle-event!* :begin-test-var - [{test-var :var}] - (alter-meta! - test-var assoc ::context - {:start-time-ms (System/currentTimeMillis) - :assertion-count 0 - :results []})) - -(defmethod handle-event!* :end-test-var - [{test-ns :ns, test-var :var, :as event}] - (let [context (::context (meta test-var)) - result (merge - event - context - {:duration-ms (- (System/currentTimeMillis) (:start-time-ms context))})] - (alter-meta! test-ns update-in [::context :results] conj result))) - -(defn- inc-ns-test-counts! [{test-ns :ns, :as _event} & ks] - (alter-meta! test-ns update ::context (fn [context] - (reduce - (fn [context k] - (update context k inc)) - context - ks)))) - -(defn- record-assertion-result! [{test-var :var, :as event}] - (let [event (assoc event :testing-contexts (vec t/*testing-contexts*))] - (alter-meta! test-var update ::context - (fn [context] - (-> context - (update :assertion-count inc) - (update :results conj event)))))) - -(defmethod handle-event!* :pass - [event] - (inc-ns-test-counts! event :test-count) - (record-assertion-result! event)) - -(defmethod handle-event!* :fail - [event] - (inc-ns-test-counts! event :test-count :failure-count) - (record-assertion-result! event)) - -(defmethod handle-event!* :error - [{test-var :var, :as event}] - ;; some `:error` events happen because of errors in fixture initialization and don't have associated vars/namespaces - (when test-var - (inc-ns-test-counts! event :test-count :error-count) - (record-assertion-result! event))) diff --git a/src/mb/hawk/reporter/interface.clj b/src/mb/hawk/reporter/interface.clj new file mode 100644 index 0000000..aceb8d4 --- /dev/null +++ b/src/mb/hawk/reporter/interface.clj @@ -0,0 +1,34 @@ +(ns mb.hawk.reporter.interface + (:require + [eftest.report.pretty] + [eftest.report.progress])) + +(defmulti handle-event + "a" + {:arglists '([reporter event])} + (fn [reporter event] + [reporter (:type event)])) + +(defmethod handle-event :default + [_ _]) + +(defn reporter + "Create a new test reporter/event handler, a function with the signature `(handle-event reporter event)` + that gets called once for every [[clojure.test]] event, including stuff like `:begin-test-run`, + `:end-test-var`, and `:fail`." + [options] + (let [stdout-reporter (case (:mode options) + (:cli/ci :repl) eftest.report.pretty/report + :cli/local eftest.report.progress/report) + reporters (descendants :hawk/reporter)] + (fn [event] + (doseq [reporter' reporters] + (handle-event reporter' event)) + (stdout-reporter event)))) + +(defn register-reporter! + "Register a reporter. + + `name` should be a namespaced keyword." + [name] + (derive name :hawk/reporter)) diff --git a/src/mb/hawk/reporter/junit.clj b/src/mb/hawk/reporter/junit.clj new file mode 100644 index 0000000..62cd08e --- /dev/null +++ b/src/mb/hawk/reporter/junit.clj @@ -0,0 +1,145 @@ +(ns mb.hawk.reporter.junit + (:require + [clojure.test :as t] + [mb.hawk.reporter.interface :as hawk.reporter] + [mb.hawk.reporter.junit.write :as write])) + +(hawk.reporter/register-reporter! :hawk/junit) + +(defmacro ^:private with-error-handling + [event & body] + `(try + ~@body + (catch Throwable e# + (throw (ex-info (str "Error handling event: " (ex-message e#)) + {:event ~event} + e#))))) + +(defn- with-var-and-ns + [{test-var :var, :as event}] + (let [test-var (or test-var + (when (seq t/*testing-vars*) + (last t/*testing-vars*)))] + (merge + {:var test-var} + event + (when test-var + {:ns (:ns (meta test-var))})))) + +(defn handle-event! + "Write JUnit output for a `clojure.test` event such as success or failure." + [thunk {test-var :var, :as event}] + (let [test-var (or test-var + (when (seq t/*testing-vars*) + (last t/*testing-vars*))) + event (merge + {:var test-var} + event + (when test-var + {:ns (:ns (meta test-var))}))] + (try + (thunk event) + (catch Throwable e + (throw (ex-info (str "Error handling event: " (ex-message e)) + {:event event} + e)))))) + +;; for unknown event types (e.g. `:clojure.test.check.clojure-test/trial`) just ignore them. +(defmethod hawk.reporter/handle-event [:hawk/junit :default] + [_ _]) + +(defmethod hawk.reporter/handle-event [:hawk/junit :begin-test-run] + [_ event] + (with-error-handling event + (write/clean-output-dir!) + (write/create-thread-pool!))) + +(defmethod hawk.reporter/handle-event [:hawk/junit :summary] + [_ event] + (with-error-handling event + (write/wait-for-writes-to-finish))) + +(defmethod hawk.reporter/handle-event [:hawk/junit :begin-test-ns] + [_reporter event] + (let [event (with-var-and-ns event) + {test-ns :ns} event] + (with-error-handling event + (alter-meta! + test-ns assoc ::context + {:start-time-ms (System/currentTimeMillis) + :timestamp (java.time.OffsetDateTime/now) + :test-count 0 + :error-count 0 + :failure-count 0 + :results []})))) + +(defmethod hawk.reporter/handle-event [:hawk/junit :end-test-ns] + [_ event] + (let [event (with-var-and-ns event) + {test-ns :ns} event + context (::context (meta test-ns))] + (with-error-handling event + (write/write-ns-result! (merge + event + context + {:duration-ms (- (System/currentTimeMillis) (:start-time-ms context))}))))) + +(defmethod hawk.reporter/handle-event [:hawk/junit :begin-test-var] + [_ event] + (let [event (with-var-and-ns event) + {test-var :var} event] + (alter-meta! + test-var assoc ::context + {:start-time-ms (System/currentTimeMillis) + :assertion-count 0 + :results []}))) + +(defmethod hawk.reporter/handle-event [:hawk/junit :end-test-var] + [_ {test-var :var :as event}] + (let [event (with-var-and-ns event) + {test-ns :ns} event + context (::context (meta test-var)) + result (merge + event + context + {:duration-ms (- (System/currentTimeMillis) (:start-time-ms context))})] + (alter-meta! test-ns update-in [::context :results] conj result))) + +(defn- inc-ns-test-counts! [{test-var :var :as _event} & ks] + (alter-meta! (:ns (meta test-var)) update ::context (fn [context] + (reduce + (fn [context k] + (update context k inc)) + context + ks)))) + +(defn- record-assertion-result! [{test-var :var :as event}] + (let [event (assoc event :testing-contexts (vec t/*testing-contexts*))] + (alter-meta! test-var update ::context + (fn [context] + (-> context + (update :assertion-count inc) + (update :results conj event)))))) + +(defmethod hawk.reporter/handle-event [:hawk/junit :pass] + [_ event] + (let [event (with-var-and-ns event)] + (with-error-handling event + (inc-ns-test-counts! event :test-count) + (record-assertion-result! event)))) + +(defmethod hawk.reporter/handle-event [:hawk/junit :fail] + [_ event] + (let [event (with-var-and-ns event)] + (with-error-handling event + (inc-ns-test-counts! event :test-count :failure-count) + (record-assertion-result! event)))) + +(defmethod hawk.reporter/handle-event [:hawk/junit :error] + [_ event] + (let [{test-var :var :as event} (with-var-and-ns event)] + ;; some `:error` events happen because of errors in fixture initialization and don't have associated vars/namespaces + (when test-var + (with-error-handling event + (inc-ns-test-counts! event :test-count :error-count) + (record-assertion-result! event))))) diff --git a/src/mb/hawk/junit/write.clj b/src/mb/hawk/reporter/junit/write.clj similarity index 99% rename from src/mb/hawk/junit/write.clj rename to src/mb/hawk/reporter/junit/write.clj index 919e6c8..96ddf72 100644 --- a/src/mb/hawk/junit/write.clj +++ b/src/mb/hawk/reporter/junit/write.clj @@ -1,4 +1,4 @@ -(ns mb.hawk.junit.write +(ns mb.hawk.reporter.junit.write "Logic related to writing test results for a namespace to a JUnit XML file. See https://stackoverflow.com/a/9410271/1198455 for the JUnit output spec." (:require diff --git a/src/mb/hawk/speak.clj b/src/mb/hawk/reporter/speak.clj similarity index 59% rename from src/mb/hawk/speak.clj rename to src/mb/hawk/reporter/speak.clj index 520cb76..a0c2406 100644 --- a/src/mb/hawk/speak.clj +++ b/src/mb/hawk/reporter/speak.clj @@ -1,16 +1,14 @@ -(ns mb.hawk.speak - (:require [clojure.java.shell :as sh])) +(ns mb.hawk.reporter.speak + (:require + [clojure.java.shell :as sh] + [mb.hawk.reporter.interface :as hawk.reporter])) -(defmulti handle-event! - "Handles a test event by speaking(!?) it if appropriate" - :type) +(hawk.reporter/register-reporter! :hawk/speak) (defn- enabled? [] (some? (System/getenv "SPEAK_TEST_RESULTS"))) -(defmethod handle-event! :default [_] nil) - -(defmethod handle-event! :summary - [{:keys [error fail]}] +(defmethod hawk.reporter/handle-event [:hawk/speak :summary] + [_ {:keys [error fail]}] (when (enabled?) (apply sh/sh "say" (if (zero? (+ error fail)) diff --git a/test/mb/hawk/reporter/speak_test.clj b/test/mb/hawk/reporter/speak_test.clj new file mode 100644 index 0000000..418b594 --- /dev/null +++ b/test/mb/hawk/reporter/speak_test.clj @@ -0,0 +1,18 @@ +(ns mb.hawk.reporter.speak-test + (:require + [clojure.java.shell :as sh] + [clojure.test :refer :all] + [mb.hawk.reporter.interface :as hawk.reporter] + [mb.hawk.reporter.speak :as hawk.reporter.speak])) + +(deftest speak-results-test + (are [error fail expected] (let [sh-args (atom nil)] + (with-redefs [hawk.reporter.speak/enabled? (constantly true) + sh/sh (fn [& args] (reset! sh-args (vec args)))] + (hawk.reporter/handle-event :hawk/speak {:type :summary :error error :fail fail}) + (= (into ["say"] expected) @sh-args))) + 0 0 ["all tests passed"] + 1 0 ["tests failed" "1 error"] + 2 0 ["tests failed" "2 errors"] + 2 1 ["tests failed" "2 errors" "1 failure"] + 0 2 ["tests failed" "2 failures"])) diff --git a/test/mb/hawk/speak_test.clj b/test/mb/hawk/speak_test.clj deleted file mode 100644 index 862375a..0000000 --- a/test/mb/hawk/speak_test.clj +++ /dev/null @@ -1,16 +0,0 @@ -(ns mb.hawk.speak-test - (:require [clojure.java.shell :as sh] - [clojure.test :refer :all] - [mb.hawk.speak :as hawk.speak])) - -(deftest speak-results-test - (are [error fail expected] (let [sh-args (atom nil)] - (with-redefs [hawk.speak/enabled? (constantly true) - sh/sh (fn [& args] (reset! sh-args (vec args)))] - (hawk.speak/handle-event! {:type :summary :error error :fail fail}) - (= (into ["say"] expected) @sh-args))) - 0 0 ["all tests passed"] - 1 0 ["tests failed" "1 error"] - 2 0 ["tests failed" "2 errors"] - 2 1 ["tests failed" "2 errors" "1 failure"] - 0 2 ["tests failed" "2 failures"]))