Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .clj-kondo/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Update `clj-kondo` configs for libraries using

```sh
clj-kondo --copy-configs --dependencies --lint "$(clojure -Spath)"
clojure -M:kondo --copy-configs --dependencies --lint "$(clojure -Spath -A:dev)"
```
50 changes: 47 additions & 3 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,61 @@
["../resources/clj-kondo.exports/com.github.metabase/hawk"]

:linters
{:docstring-leading-trailing-whitespace {:level :warning}
{:aliased-namespace-symbol {:level :warning}
:case-symbol-test {:level :warning}
:condition-always-true {:level :warning}
:def-fn {:level :warning}
:docstring-leading-trailing-whitespace {:level :warning}
:dynamic-var-not-earmuffed {:level :warning}
:equals-expected-position {:level :warning, :only-in-test-assertion true}
:equals-true {:level :warning}
:keyword-binding {:level :warning}
:main-without-gen-class {:level :warning}
:minus-one {:level :warning}
:misplaced-docstring {:level :warning}
:missing-body-in-when {:level :warning}
:missing-docstring {:level :warning}
:missing-else-branch {:level :warning}
:namespace-name-mismatch {:level :warning}
:non-arg-vec-return-type-hint {:level :warning}
:plus-one {:level :warning}
:reduce-without-init {:level :warning}
:redundant-call {:level :warning}
:redundant-fn-wrapper {:level :warning}
:redundant-str-call {:level :warning}
:self-requiring-namespace {:level :warning}
:shadowed-var {:level :warning}
:single-key-in {:level :warning}
:unsorted-imports {:level :warning}
:unsorted-required-namespaces {:level :warning}
:unused-alias {:level :warning}
:use {:level :warning}
:used-underscored-binding {:level :warning}
:warn-on-reflection {:level :warning}

:refer-all
{:level :warning
:exclude [clojure.test]}

:unresolved-var
{:exclude [io.aviso.ansi]}

:consistent-alias
{:aliases
{methodical.core methodical}}}
{methodical.core methodical}}

;;; disabled linters

:redundant-ignore {:level :off}} ; false positives

:config-in-comment
{:linters {:unresolved-symbol {:level :off}}}}
{:linters {:unresolved-symbol {:level :off}}}

:ns-groups
[{:pattern "^mb\\.eftest\\..*$"
:name eftest-namespaces}]

:config-in-ns
{eftest-namespaces
{:linters
{:missing-docstring {:level :off}}}}}
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,3 +277,9 @@ for more information.
```
clj -X:test '{:fail-fast? true}'
```

## License

Copyright © 2019-2026 James Reeves, 2023-2026 Metabase, Inc.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤝


Distributed under the Eclipse Public License either version 2.0 or (at your option) any later version.
17 changes: 10 additions & 7 deletions deps.edn
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@

:deps
{commons-io/commons-io {:mvn/version "2.18.0"}
eftest/eftest {:mvn/version "0.6.0"}
environ/environ {:mvn/version "1.2.0"}
io.aviso/pretty {:mvn/version "1.3"}
methodical/methodical {:mvn/version "1.0.124"}
pjstadig/humane-test-output {:mvn/version "0.11.0"}
prismatic/schema {:mvn/version "1.4.1"}
metosin/malli {:mvn/version "0.17.0"}
mvxcvi/puget {:mvn/version "1.3.4"}
org.clojure/java.classpath {:mvn/version "1.1.0"}
org.clojure/tools.namespace {:mvn/version "1.5.0"}}
org.clojure/tools.namespace {:mvn/version "1.5.0"}
pjstadig/humane-test-output {:mvn/version "0.11.0"}
prismatic/schema {:mvn/version "1.4.1"}
progrock/progrock {:mvn/version "0.1.2"}}

:aliases
{:dev
Expand All @@ -19,7 +21,8 @@
;; clj -X:dev:test
:test
{:exec-fn mb.hawk.core/find-and-run-tests-cli
:exec-args {:exclude-directories ["src" "resources"]}}
:exec-args {:exclude-directories ["src" "resources"]
:exclude-tags [:hawk.tests/skip]}}

;; clojure -T:build
:build
Expand All @@ -37,7 +40,7 @@
;; version here which may be different from the version installed on your computer.
:kondo
{:replace-deps
{clj-kondo/clj-kondo {:mvn/version "2024.08.29"}}
{clj-kondo/clj-kondo {:mvn/version "2026.05.25"}}

:main-opts
["-m" "clj-kondo.main"]}
Expand All @@ -47,5 +50,5 @@

;; Find outdated versions of dependencies. Run with `clojure -M:outdated`
:outdated {;; Note that it is `:deps`, not `:extra-deps`
:deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
:deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
:main-opts ["-m" "antq.core" "--skip=github-action"]}}}
61 changes: 61 additions & 0 deletions src/mb/eftest/output_capture.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
(ns mb.eftest.output-capture
(:import
(java.io ByteArrayOutputStream OutputStream PrintStream PrintWriter)))

(set! *warn-on-reflection* true)

(def ^:dynamic ^ByteArrayOutputStream *test-buffer* nil)

(defn read-test-buffer []
(some-> *test-buffer* (.toByteArray) (String.)))

(def active-buffers (atom #{}))

(defmacro with-test-buffer [& body]
`(let [buffer# (ByteArrayOutputStream.)]
(try
(swap! active-buffers conj buffer#)
(binding [*test-buffer* buffer#] ~@body)
(finally (swap! active-buffers disj buffer#)))))

(defn- doto-capture-buffer [f]
(if *test-buffer*
(f *test-buffer*)
(doseq [buffer @active-buffers]
(f buffer))))

(defn- create-proxy-output-stream ^OutputStream []
(proxy [OutputStream] []
(write
([data]
(if (instance? Integer data)
(doto-capture-buffer #(.write ^OutputStream % ^int data))
(doto-capture-buffer #(.write ^OutputStream % ^bytes data 0 (alength ^bytes data)))))
([data off len]
(doto-capture-buffer #(.write ^OutputStream % data off len))))))

(defn init-capture []
(let [old-out System/out
old-err System/err
proxy-output-stream (create-proxy-output-stream)
new-stream (PrintStream. proxy-output-stream)
new-writer (PrintWriter. proxy-output-stream)]
(System/setOut new-stream)
(System/setErr new-stream)
{:captured-writer new-writer
:old-system-out old-out
:old-system-err old-err}))

(defn restore-capture [{:keys [old-system-out old-system-err]}]
(System/setOut old-system-out)
(System/setErr old-system-err))

(defmacro with-capture [& body]
`(let [context# (init-capture)
writer# (:captured-writer context#)]
(try
(binding [*out* writer#, *err* writer#]
(with-redefs [*out* writer#, *err* writer#]
~@body))
(finally
(restore-capture context#)))))
33 changes: 33 additions & 0 deletions src/mb/eftest/report.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
(ns mb.eftest.report
(:require
[clojure.java.io :as io]
[clojure.test :refer [*test-out*]]))

(set! *warn-on-reflection* true)

(def ^:dynamic *context*
"Used by eftest.runner/run-tests to hold a mutable atom that persists for the
duration of the test run. This atom can be used by reporters to hold
additional statistics and information during the tests."
nil)

(def ^:dynamic *testing-path*
"2-element vector [ns scope] where scope is either :clojure.test/once-fixtures,
:clojure.test/each-fixtures or var under test"
nil)

(defn report-to-file
"Wrap a report function so that its output is directed to a file. output-file
should be something that can be coerced into a Writer."
[report output-file]
(let [output-key [::output-files output-file]]
(fn [m]
(when (= (:type m) :begin-test-run)
(io/make-parents (io/file output-file))
(swap! *context* assoc-in output-key (io/writer output-file)))
(let [^java.io.Writer writer (get-in @*context* output-key)]
(binding [*test-out* writer]
(report m))
(when (= (:type m) :summary)
(swap! *context* update ::output-files dissoc output-file)
(.close writer))))))
161 changes: 161 additions & 0 deletions src/mb/eftest/report/pretty.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
(ns mb.eftest.report.pretty
"A test reporter with an emphasis on pretty formatting."
(:refer-clojure :exclude [test])
(:require
[clojure.data :as data]
[clojure.string :as str]
[clojure.test :as test]
[fipp.engine :as fipp]
[io.aviso.ansi :as ansi]
[io.aviso.exception :as exception]
[io.aviso.repl :as repl]
[mb.eftest.output-capture :as capture]
[mb.eftest.report :as report]
[puget.printer :as puget]))

(def ^:dynamic *fonts*
"The ANSI codes to use for reporting on tests."
{:exception ansi/red-font
:reset ansi/reset-font
:message ansi/italic-font
:property ansi/bold-font
:source ansi/italic-font
:function-name ansi/blue-font
:clojure-frame ansi/white-font
:java-frame ansi/reset-font
:omitted-frame ansi/reset-font
:pass ansi/green-font
:fail ansi/red-font
:error ansi/red-font
:divider ansi/yellow-font})

(def ^:dynamic *divider*
"The divider to use between test failure and error reports."
"\n")

(defn- testing-scope-str [{:keys [file line]}]
(let [[test-ns scope] report/*testing-path*]
(str
(cond
(keyword? scope)
(str (:clojure-frame *fonts*) (ns-name test-ns) (:reset *fonts*) " during "
(:function-name *fonts*) scope (:reset *fonts*))

(var? scope)
(str (:clojure-frame *fonts*) (ns-name test-ns) "/"
(:function-name *fonts*) (:name (meta scope)) (:reset *fonts*)))
(when (or file line)
(str " (" (:source *fonts*) file ":" line (:reset *fonts*) ")")))))

(defn- diff-all [expected actuals]
(map vector actuals (map #(take 2 (data/diff expected %)) actuals)))

(defn- pretty-printer []
(puget/pretty-printer {:print-color true
:print-meta false}))

(defn- pprint-document [doc]
(fipp/pprint-document doc {:width 80}))

(defn- equals-fail-report [{:keys [actual]}]
(let [[_ [_ expected & actuals]] actual
p (pretty-printer)]
(doseq [[actual [a b]] (diff-all expected actuals)]
(pprint-document
[:group
[:span "expected: " (puget/format-doc p expected) :break]
[:span " actual: " (puget/format-doc p actual) :break]
(when (and (not= expected a) (not= actual b))
[:span " diff: "
(when a
[:span "- " (puget/format-doc p a) :break])
(when b
[:span
(if a " + " "+ ")
(puget/format-doc p b)])])]))))

(defn- predicate-fail-report [{:keys [expected actual]}]
(let [p (pretty-printer)]
(pprint-document
[:group
[:span "expected: " (puget/format-doc p expected) :break]
[:span " actual: " (puget/format-doc p actual)]])))

(defn- print-stacktrace [t]
(binding [exception/*traditional* true
exception/*fonts* *fonts*]
(repl/pretty-print-stack-trace t test/*stack-trace-depth*)))

(defn- error-report [{:keys [expected actual]}]
(if expected
(let [p (pretty-printer)]
(pprint-document
[:group
[:span "expected: " (puget/format-doc p expected) :break]
[:span " actual: " (with-out-str (print-stacktrace actual))]]))
(print-stacktrace actual)))

(defn- print-output [output]
(let [c (:divider *fonts*)
r (:reset *fonts*)]
(when-not (str/blank? output)
(println (str c "---" r " Test output " c "---" r))
(println (str/trim-newline output))
(println (str c "-------------------" r)))))

(defmulti report
"A reporting function compatible with clojure.test. Uses ANSI colors and
terminal formatting to produce readable and 'pretty' reports."
:type)

(defmethod report :default [_m])

(defmethod report :pass [_m]
(test/with-test-out (test/inc-report-counter :pass)))

(defmethod report :fail [{:keys [message expected] :as m}]
(test/with-test-out
(test/inc-report-counter :fail)
(print *divider*)
(println (str (:fail *fonts*) "FAIL" (:reset *fonts*) " in") (testing-scope-str m))
(when (seq test/*testing-contexts*) (println (test/testing-contexts-str)))
(when message (println message))
(if (and (sequential? expected)
(= (first expected) '=))
(equals-fail-report m)
(predicate-fail-report m))
(print-output (capture/read-test-buffer))))

(defmethod report :error [{:keys [message _expected _actual] :as m}]
(test/with-test-out
(test/inc-report-counter :error)
(print *divider*)
(println (str (:error *fonts*) "ERROR" (:reset *fonts*) " in") (testing-scope-str m))
(when (seq test/*testing-contexts*) (println (test/testing-contexts-str)))
(when message (println message))
(error-report m)
(some-> (capture/read-test-buffer) (print-output))))

(defn- pluralize [word n]
(if (= n 1) word (str word "s")))

(defn- format-interval [duration]
(format "%.3f seconds" (double (/ duration 1e3))))

(defmethod report :long-test [{:keys [duration] :as m}]
(test/with-test-out
(print *divider*)
(println (str (:fail *fonts*) "LONG TEST" (:reset *fonts*) " in") (testing-scope-str m))
(when duration (println "Test took" (format-interval duration) "seconds to run"))))

(defmethod report :summary [{:keys [test pass fail error duration]}]
(let [total (+ pass fail error)
color (if (= pass total) (:pass *fonts*) (:error *fonts*))]
(test/with-test-out
(print *divider*)
(println "Ran" test "tests in" (format-interval duration))
(println (str color
total " " (pluralize "assertion" total) ", "
fail " " (pluralize "failure" fail) ", "
error " " (pluralize "error" error) "."
(:reset *fonts*))))))
Loading
Loading