├── .gitignore ├── .lein-classpath ├── README.md ├── doc └── screen.png ├── project.clj ├── src ├── leiningen │ ├── quickie.clj │ └── quickp.clj └── quickie │ ├── autotest.clj │ └── runner.clj └── test └── quickie ├── doop_test.clj └── runner_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | *.jar 7 | *.class 8 | .lein-deps-sum 9 | .lein-failures 10 | .lein-plugins 11 | *.asc 12 | /.lein-repl-history 13 | /.nrepl-port 14 | -------------------------------------------------------------------------------- /.lein-classpath: -------------------------------------------------------------------------------- 1 | src/leiningen 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quickie 2 | 3 | [![Clojars Project](http://clojars.org/quickie/latest-version.svg)](http://clojars.org/quickie) 4 | 5 | A Leiningen plugin that will magically re-run all your tests when a file changes. 6 | 7 | ![Screenshot](doc/screen.png) 8 | 9 | ## Features 10 | 11 | * Uses the builtin clojure.test test runner so you don't need to rewrite your tests 12 | * Tools.namespace will unload and reload namespaces as needed to keep process in sync 13 | * Runs every time a clojure file in your project changes 14 | * Uses (Clansi)[https://github.com/ams-clj/clansi] to show a red or green bar to know if you tests are passing 15 | * Filters out exception stacktraces to remove cruft 16 | * Pass in a test matcher to change which tests are run from the command line. 17 | 18 | ## Installation 19 | 20 | Use this for project-level plugins: 21 | 22 | Put `[quickie "0.4.1"]` into the `:plugins` vector of your project.clj. 23 | 24 | ## Autotest Usage (quickie) 25 | 26 | Will rerun your test namespaces as clojure files in your project change 27 | 28 | ``` 29 | lein quickie 30 | ``` 31 | 32 | By default all namespaces in your classpath that contain your project name and end with the word `test` will be tested on each run. To change this, add a line like this to your project.clj: `:test-matcher #"my regular expression"`. Alternatively, you can call quickie via the command line with the regex you wish to use: `lein quickie "my-project.*\.test\..*"`. 33 | 34 | Hit ctrl+c whenever you are done. Have fun! 35 | 36 | ## Parallel Testing Usage (quickp) 37 | 38 | Will run your tests across across multiple threads (currently set to 20). 39 | 40 | ``` 41 | lein quickp 42 | ``` 43 | 44 | Running tests against multiple threads could cause test failures. If you use `with-redefs` in your tests you will needs to switch over to something like `with-local-redefs` as seen on [this gist](https://gist.github.com/gfredericks/7143494). 45 | 46 | ## License 47 | 48 | Copyright © 2015 Jake Pearson 49 | 50 | Distributed under the Eclipse Public License, the same as Clojure. 51 | 52 | ## Contributors 53 | * Adam Esterline 54 | * Jeff Smith 55 | * Russ Teabeault 56 | * Chris Perkins 57 | * Jake Pearson 58 | -------------------------------------------------------------------------------- /doc/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakepearson/quickie/ef005df803173b7707d16b63e24696e09264973a/doc/screen.png -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject quickie "0.4.2" 2 | :description "Automatically run tests when clj files change" 3 | :url "http://github.com/jakepearson/quickie" 4 | :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} 5 | :signing {:gpg-key "37634C19"} 6 | :eval-in-leiningen true 7 | :dependencies [[org.clojure/clojure "1.6.0"] 8 | [org.clojure/tools.namespace "0.2.10"] 9 | [com.climate/claypoole "1.0.0"] 10 | [myguidingstar/clansi "1.3.0"]]) 11 | -------------------------------------------------------------------------------- /src/leiningen/quickie.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.quickie 2 | (:require [leiningen.core.eval :as eval] 3 | [leiningen.core.project :as lein-project])) 4 | 5 | (defn paths [parameters project] 6 | (assoc parameters :paths (vec (concat (:source-paths project) (:test-paths project))))) 7 | 8 | (defn default-pattern [project] 9 | (let [name (or (:name project) 10 | (:group project))] 11 | (re-pattern (str name ".*test")))) 12 | 13 | (defn test-matcher [project args] 14 | (cond 15 | (> (count args) 0) (re-pattern (first args)) 16 | (:test-matcher project) (:test-matcher project) 17 | :else (default-pattern project))) 18 | 19 | (defn run-parallel [project & args] 20 | (eval/eval-in-project 21 | (update-in project [:dependencies] conj ['quickie "0.4.1"]) 22 | (let [parameters (-> {} 23 | (paths project) 24 | (assoc :test-matcher (test-matcher project args)))] 25 | `(quickie.runner/run-parallel ~parameters)) 26 | `(require 'quickie.runner))) 27 | 28 | (defn quickie 29 | "Automatically run tests when clj files change" 30 | [project & args] 31 | (let [project (-> project 32 | (lein-project/merge-profiles [:test]) 33 | (update-in [:dependencies] conj ['quickie "0.4.1"])) 34 | parameters (-> {} 35 | (paths project) 36 | (assoc :test-matcher (test-matcher project args)))] 37 | (eval/eval-in-project 38 | project 39 | `(quickie.autotest/run ~parameters) 40 | `(require 'quickie.autotest)))) 41 | -------------------------------------------------------------------------------- /src/leiningen/quickp.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.quickp 2 | (:require [leiningen.quickie :as quickie])) 3 | 4 | (defn quickp 5 | "Run each test in a different thread" 6 | [project & args] 7 | (apply quickie/run-parallel project args)) 8 | 9 | -------------------------------------------------------------------------------- /src/quickie/autotest.clj: -------------------------------------------------------------------------------- 1 | (ns quickie.autotest 2 | (:require [clojure.tools.namespace.repl :as repl] 3 | [quickie.runner :as runner])) 4 | 5 | (defn clear-console [] 6 | (println (str (char 27) "[2J"))) 7 | 8 | (defn reload [] 9 | (repl/refresh) 10 | (when *e 11 | (.printStackTrace *e) 12 | (set! *e nil))) 13 | 14 | (defn reload-and-test [project] 15 | (clear-console) 16 | (reload) 17 | (runner/run project)) 18 | 19 | (defn all-files [paths] 20 | (mapcat #(file-seq (clojure.java.io/file %)) paths)) 21 | 22 | (defn clj-cljc-file? [^java.io.File file] 23 | (let [name (.getName file)] 24 | (and (or (.endsWith name ".clj") (.endsWith name ".cljc")) 25 | (not (.startsWith name ".#")) ; exclude Emacs temp files 26 | ))) 27 | 28 | 29 | 30 | (defn all-clj-cljc-files [paths] 31 | (filter clj-cljc-file? (all-files paths))) 32 | 33 | (defn get-file-state [paths] 34 | (reduce (fn [result file] 35 | (assoc result 36 | (.getAbsolutePath file) 37 | (.lastModified file))) 38 | {} 39 | (all-clj-cljc-files paths))) 40 | 41 | (defn run-tests-forever [project] 42 | (loop [current-state (get-file-state (:paths project)) 43 | changes current-state] 44 | (when (not (= changes current-state)) 45 | (reload-and-test project)) 46 | (Thread/sleep 1000) 47 | (recur changes (get-file-state (:paths project))))) 48 | 49 | (defn run [project] 50 | (apply repl/set-refresh-dirs (:paths project)) 51 | (try 52 | (reload) 53 | 54 | (do 55 | (runner/run project) 56 | (run-tests-forever project)) 57 | 58 | (catch Exception e (println e)) 59 | (finally 60 | (shutdown-agents)))) 61 | -------------------------------------------------------------------------------- /src/quickie/runner.clj: -------------------------------------------------------------------------------- 1 | (ns quickie.runner 2 | (:require [clojure.test :as test] 3 | [clansi.core :as clansi] 4 | [com.climate.claypoole :as threadpool] 5 | 6 | [clojure.pprint] 7 | [clojure.tools.namespace.find :as find] 8 | [clojure.tools.namespace.track :as track] 9 | [clojure.tools.namespace.file :as file] 10 | [clojure.tools.namespace.repl :as repl] 11 | 12 | [clojure.string :as string])) 13 | 14 | 15 | (defn- pad [length orig-str pad-char fu] 16 | (string/replace (format (fu length) orig-str) " " pad-char)) 17 | 18 | (defn- rpad 19 | ([length orig-str pad-char] 20 | (let [rfstr (fn [length] (str "%-" length "s"))] 21 | (pad length orig-str pad-char rfstr))) 22 | ([length orig-str] 23 | (rpad length orig-str "."))) 24 | 25 | (defn- lpad 26 | ([length orig-str pad-char] 27 | (let [lfstr (fn [length] (str "%" length "s"))] 28 | (pad length orig-str pad-char lfstr))) 29 | ([length orig-str] 30 | (lpad length orig-str "."))) 31 | 32 | (defn out-str-result [f] 33 | (let [string-writer (new java.io.StringWriter)] 34 | (binding [test/*test-out* string-writer] 35 | (let [result (f)] 36 | {:output (-> (str string-writer) 37 | (string/split #"\n") 38 | butlast) 39 | :result result})))) 40 | 41 | (def matchers 42 | [#"^quickie\.runner" 43 | #"^quickie\.autotest" 44 | #"^user\$eval" 45 | #"^clojure\.lang" 46 | #"^clojure\.test" 47 | #"^clojure\.core" 48 | #"^clojure\.main" 49 | #"^java\.lang" 50 | #"^java\.util\.concurrent\.ThreadPoolExecutor\$Worker" 51 | #"^com\.climate\.claypoole" 52 | #"^java\.util\.concurrent"]) 53 | 54 | (defn needed-line [line] 55 | (let [line-trimmed (string/trim line) 56 | needed (not-any? #(re-seq % line-trimmed) matchers)] 57 | {:line line 58 | :needed needed})) 59 | 60 | (def group-size 5) 61 | (def prefix-size (long (/ group-size 2))) 62 | 63 | (defn group-lines [lines] 64 | (let [matched-lines (map needed-line lines) 65 | prefix (repeat prefix-size nil) 66 | partionable-lines (concat prefix matched-lines prefix)] 67 | (partition group-size 1 partionable-lines))) 68 | 69 | (defn filter-lines [lines] 70 | (let [groups (group-lines lines) 71 | needed-line? (fn [group] (some :needed group)) 72 | needed-lines (filter needed-line? groups)] 73 | (->> (map #(nth % prefix-size) needed-lines) 74 | (map :line)))) 75 | 76 | (defn print-pass [result] 77 | (-> (:output result) 78 | last 79 | println) 80 | (println (clansi/style " All Tests Passing! " :black :bg-green))) 81 | 82 | (defn print-fail [result] 83 | (let [{:keys [error fail]} (:result result) 84 | lines (:output result)] 85 | (->> lines 86 | filter-lines 87 | (string/join "\n") 88 | println) 89 | (println (clansi/style (str " " error " errors and " fail " failures ") :black :bg-red)))) 90 | 91 | (defn print-result [result] 92 | (let [{:keys [error fail]} (:result result)] 93 | (if (= 0 (+ error fail)) 94 | (print-pass result) 95 | (print-fail result)))) 96 | 97 | (defn- duration-string [d] 98 | (str "(" (clansi/style (str d "ms") :magenta) ")")) 99 | 100 | (defn print-ns-result [{:keys [success? ns duration output]} namespace-length] 101 | (let [result (if success? 102 | (clansi/style "Pass" :black :bg-green) 103 | (clansi/style "Fail" :black :bg-red)) 104 | rpad-length (+ 20 namespace-length) 105 | lpad-length 18 106 | namespace-string (rpad rpad-length (ns-name ns)) 107 | duration-string (lpad lpad-length (duration-string duration)) 108 | output-summary (str namespace-string duration-string " " result)] 109 | (println output-summary) 110 | (let [filtered-lines (->> (string/split output #"\n") 111 | (drop 2) 112 | filter-lines 113 | (string/join "\n"))] 114 | (when-not (string/blank? filtered-lines) 115 | (println filtered-lines))))) 116 | 117 | (defn- test-result [errors] 118 | {:pass 0 119 | :test 0 120 | :error errors 121 | :fail 0 122 | :success? (= 0 errors)}) 123 | 124 | (defn capture-result-and-output [f] 125 | (let [output (java.io.StringWriter.)] 126 | (binding [*out* output 127 | test/*test-out* output] 128 | [(f) (str output)]))) 129 | 130 | (defn- test-ns [ns] 131 | (let [start-time (System/currentTimeMillis) 132 | [result output] (capture-result-and-output 133 | (fn [] (try 134 | (test/test-ns ns) 135 | (catch Exception e 136 | (.printStackTrace e) 137 | (assoc (test-result 1) :exception e)))))] 138 | (merge result 139 | {:output (str output) 140 | :success? (= 0 (+ (:error result) (:fail result))) 141 | :ns ns 142 | :duration (- (System/currentTimeMillis) start-time)}))) 143 | 144 | (defn- summarize [test-results] 145 | (reduce (fn [results result] 146 | (-> results 147 | (assoc-in [:tests (str (ns-name (:ns result)))] result) 148 | (update-in [:summary :pass] + (:pass result)) 149 | (update-in [:summary :test] + (:test result)) 150 | (update-in [:summary :error] + (:error result)) 151 | (update-in [:summary :fail] + (:fail result)))) 152 | {:summary (test-result 0)} 153 | test-results)) 154 | 155 | (defn- longest-namespace-name-length [namespaces] 156 | (->> namespaces 157 | (map ns-name) 158 | (map str) 159 | (map count) 160 | (apply max 0))) 161 | 162 | (defn- test-namespaces [matcher] 163 | (let [filter-fn (fn [ns] (let [ns-name (-> ns ns-name str)] 164 | (re-find matcher ns-name)))] 165 | (->> (all-ns) 166 | (filter filter-fn)))) 167 | 168 | (defn run-parallel [project] 169 | (try 170 | (apply repl/set-refresh-dirs (:paths project ["./"])) 171 | (let [[refresh-result output] (capture-result-and-output repl/refresh)] 172 | (if (= :ok refresh-result) 173 | (let [test-nses (test-namespaces (:test-matcher project)) 174 | longest-ns-length (longest-namespace-name-length test-nses) 175 | lock (Object.) 176 | start-time (System/currentTimeMillis) 177 | results (->> test-nses 178 | (threadpool/upmap 20 179 | (fn [ns] 180 | (let [result (test-ns ns)] 181 | (locking lock 182 | (print-ns-result result longest-ns-length)) 183 | result))) 184 | summarize 185 | doall) 186 | total-errors (+ (get-in results [:summary :error]) 187 | (get-in results [:summary :fail])) 188 | total-pass (get-in results [:summary :pass]) 189 | summary-string (str "\nTests Passed: " total-pass " Tests Failed: " total-errors) 190 | duration (duration-string (- (System/currentTimeMillis) start-time)) 191 | background-color (if (= 0 total-errors) :bg-green :bg-red)] 192 | (println (clansi/style summary-string :black background-color) " " duration) 193 | (System/exit total-errors) 194 | results) 195 | (do 196 | (println output) 197 | (throw refresh-result)))) 198 | (catch Exception e 199 | (println e) 200 | (.printStackTrace e) 201 | (System/exit 1)))) 202 | 203 | (defn run [project] 204 | (try 205 | (let [matcher (:test-matcher project #"test") 206 | result (out-str-result (partial test/run-all-tests matcher))] 207 | (print-result result)) 208 | (catch Exception e 209 | (println (.getMessage e)) 210 | (.printStackTrace e)))) 211 | 212 | -------------------------------------------------------------------------------- /test/quickie/doop_test.clj: -------------------------------------------------------------------------------- 1 | 2 | (ns quickie.doop-test 3 | (:require 4 | [clojure.test :refer :all])) 5 | 6 | (deftest stuff 7 | (is (= 1 3))) 8 | 9 | (deftest throw 10 | (throw (Exception.))) 11 | -------------------------------------------------------------------------------- /test/quickie/runner_test.clj: -------------------------------------------------------------------------------- 1 | (ns quickie.runner-test 2 | (:require [clojure.test :refer :all] 3 | [quickie.runner :as runner])) 4 | 5 | (deftest needed-line 6 | (are [expected input] (= expected (:needed (runner/needed-line input))) 7 | true "a" 8 | true "" 9 | false "clojure.lang")) 10 | 11 | (deftest group-lines 12 | (is (= [[nil nil {:line "a" :needed true} nil nil]] (runner/group-lines ["a"])))) 13 | 14 | (deftest filter-lines 15 | (testing "keep important lines" 16 | (is (= ["a" "b"] (runner/filter-lines ["a" "b"])))) 17 | (testing "should toss unimportant lines" 18 | (is (= [] (runner/filter-lines ["clojure.lang"])))) 19 | (testing "should keep lines near important lines" 20 | (let [lines ["clojure.lang" "a" "clojure.core" "clojure.core"]] 21 | (is (= lines (runner/filter-lines lines)))) 22 | (let [lines ["a" "clojure.core" "clojure.core" "clojure.lang"]] 23 | (is (= 3 (count (runner/filter-lines lines))))))) 24 | --------------------------------------------------------------------------------