(ns leiningen.test "Run the project's tests." (:refer-clojure :exclude [test]) (:require [clojure.java.io :as io] [bultitude.core :as b] [leiningen.core.eval :as eval] [leiningen.core.main :as main] [leiningen.core.project :as project]) (:import (java.io File PushbackReader))) (def ^:dynamic *exit-after-tests* true) ;; This is a massive and terrible monkeypatch to work around the fact ;; that the built-in clojure.test library does not accept patches from ;; outside the core team. At this point the monkeypatching code below ;; should be considered legacy, and rather than trying to improve it, ;; all efforts should be instead directed towards improving external ;; libraries such as https://github.com/circleci/circleci.test, which ;; has a superset of these features and also doesn't require you to ;; stop using clojure.test to write your tests. ;; We recommend that projects override the test task with an alias ;; that calls out to a third-party testing library instead. (def form-for-suppressing-unselected-tests "A function that figures out which vars need to be suppressed based on the given selectors, moves their :test metadata to :leiningen/skipped-test (so that clojure.test won't think they are tests), runs the given function, and then sets the metadata back." `(fn [namespaces# selectors# func#] (let [copy-meta# (fn [var# from-key# to-key#] (if-let [x# (get (meta var#) from-key#)] (alter-meta! var# #(-> % (assoc to-key# x#) (dissoc from-key#))))) vars# (if (seq selectors#) (->> namespaces# (mapcat (comp vals ns-interns)) (remove (fn [var#] (some (fn [[selector# args#]] (let [sfn# (if (vector? selector#) (second selector#) selector#)] (apply sfn# (merge (-> var# meta :ns meta) (assoc (meta var#) ::var var#)) args#))) selectors#))))) copy# #(doseq [v# vars#] (copy-meta# v# %1 %2))] (copy# :test :leiningen/skipped-test) (try (func#) (finally (copy# :leiningen/skipped-test :test)))))) (defn- form-for-select-namespaces [namespaces selectors] `(reduce (fn [acc# [f# args#]] (if (vector? f#) (filter #(apply (first f#) % args#) acc#) acc#)) '~namespaces ~selectors)) (defn- form-for-nses-selectors-match [selectors ns-sym] `(distinct (for [ns# ~ns-sym [_# var#] (ns-publics ns#) :when (some (fn [[selector# args#]] (apply (if (vector? selector#) (second selector#) selector#) (merge (-> var# meta :ns meta) (assoc (meta var#) ::var var#)) args#)) ~selectors)] ns#))) ;; TODO: make this an arg to form-for-testing-namespaces in 3.0. (def ^:private ^:dynamic *monkeypatch?* true) (defn form-for-testing-namespaces "Return a form that when eval'd in the context of the project will test each namespace and print an overall summary." ([namespaces _ & [selectors]] (let [ns-sym (gensym "namespaces")] `(let [~ns-sym ~(form-for-select-namespaces namespaces selectors)] (when (seq ~ns-sym) (apply require :reload ~ns-sym)) (let [failures# (atom {}) selected-namespaces# ~(form-for-nses-selectors-match selectors ns-sym) _# (when ~*monkeypatch?* (leiningen.core.injected/add-hook #'clojure.test/test-ns (fn [test-ns# ns#] (try (test-ns# ns#) (catch Throwable t# (binding [clojure.test/*report-counters* (ref clojure.test/*initial-report-counters*) clojure.test/*testing-vars* (list (with-meta 'test {:name ns# :ns ns#}))] (clojure.test/do-report {:type :error :message "Uncaught exception in test fixture" :expected nil :actual t#}) (clojure.test/do-report {:type :end-test-ns :ns (the-ns ns#)}) @clojure.test/*report-counters*))))) (leiningen.core.injected/add-hook #'clojure.test/report (fn [report# m# & args#] (when (#{:error :fail} (:type m#)) (when-let [first-var# (-> clojure.test/*testing-vars* first meta)] (let [ns-name# (-> first-var# :ns ns-name name) test-name# (-> first-var# :name name)] (swap! failures# (fn [_#] (update-in @failures# [ns-name#] (fnil conj []) test-name#))) (newline) (println "lein test :only" (str ns-name# "/" test-name#))))) (if (= :begin-test-ns (:type m#)) (clojure.test/with-test-out (newline) (println "lein test" (ns-name (:ns m#)))) (apply report# m# args#))))) summary# (binding [clojure.test/*test-out* *out*] (~form-for-suppressing-unselected-tests selected-namespaces# ~selectors #(apply ~'clojure.test/run-tests selected-namespaces#)))] (spit ".lein-failures" (if ~*monkeypatch?* (pr-str @failures#) "#")) (let [total# (+ (int (:error summary#)) (int (:fail summary#)))] (if ~*exit-after-tests* (System/exit total#) total#))))))) (defn- split-selectors [args] (let [[nses selectors] (split-with (complement keyword?) args)] [nses (loop [acc {} [selector & selectors] selectors] (if (seq selectors) (let [[args next] (split-with (complement keyword?) selectors)] (recur (assoc acc selector (list 'quote args)) next)) (if selector (assoc acc selector ()) acc)))])) (defn- partial-selectors [project-selectors selectors] (for [[k v] selectors :let [selector-form (k project-selectors)] :when selector-form] [selector-form v])) (def ^:private only-form ['(fn [ns & vars] ((set (for [v vars] (-> (str v) (.split "/") (first) (symbol)))) ns)) '(fn [m & vars] (some #(let [var (str "#'" %)] (if (some #{\/} var) (= var (-> m ::var str)) (= % (ns-name (:ns m))))) vars))]) (defn- convert-to-ns [possible-file] (if (and (re-matches #".*\.cljc?" possible-file) (.exists (io/file possible-file))) (str (second (b/ns-form-for-file possible-file))) possible-file)) (defn ^:internal read-args [args project] (let [args (->> args (map convert-to-ns) (map read-string)) [nses given-selectors] (split-selectors args) nses (or (seq nses) (sort (b/namespaces-on-classpath :classpath (map io/file (distinct (:test-paths project))) :ignore-unreadable? false))) selectors (partial-selectors (merge {:all '(constantly true)} {:only only-form} (:test-selectors project)) given-selectors) selectors (if (and (empty? selectors) (:default (:test-selectors project))) [[(:default (:test-selectors project)) ()]] selectors)] (when (and (empty? selectors) (seq given-selectors)) (main/abort "Please specify :test-selectors in project.clj")) [nses selectors])) (defn test "Run the project's tests. Marking deftest or ns forms with metadata allows you to pick selectors to specify a subset of your test suite to run: (deftest ^:integration network-heavy-test (is (= [1 2 3] (:numbers (network-operation))))) Write the selectors in project.clj: :test-selectors {:default (complement :integration) :integration :integration} Arguments to this task will be considered test selectors if they are keywords, otherwise arguments must be test namespaces or files to run. With no arguments the :default test selector is used if present, otherwise all tests are run. Test selector arguments must come after the list of namespaces. A default :only test-selector is available to run select tests. For example, `lein test :only leiningen.test.test/test-default-selector` only runs the specified test. A default :all test-selector is available to run all tests." [project & tests] (binding [main/*exit-process?* (if (= :leiningen (:eval-in project)) false main/*exit-process?*) *exit-after-tests* (if (= :leiningen (:eval-in project)) false *exit-after-tests*) *monkeypatch?* (:monkeypatch-clojure-test project true)] (let [project (project/merge-profiles project [:leiningen/test :test]) [nses selectors] (read-args tests project) _ (eval/prep project) form (form-for-testing-namespaces nses nil (vec selectors))] (try (when-let [n (eval/eval-in-project project form '(require 'clojure.test))] (when (and (number? n) (pos? n)) (throw (ex-info "Tests Failed" {:exit-code n})))) (catch clojure.lang.ExceptionInfo e (main/abort "Tests failed."))))))