Skip to content

Commit 503a89e

Browse files
committed
Add main function for CLI usage
- Allow execution from any tool that can launch from -main (lein run -m, clj -m, etc) weavejester/eftest/#43
1 parent e3878e8 commit 503a89e

File tree

2 files changed

+225
-1
lines changed

2 files changed

+225
-1
lines changed

eftest/project.clj

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
:license {:name "Eclipse Public License"
55
:url "http://www.eclipse.org/legal/epl-v10.html"}
66
:dependencies [[org.clojure/clojure "1.7.0"]
7+
[org.clojure/tools.cli "0.3.5"]
78
[org.clojure/tools.namespace "0.2.11"]
9+
[org.clojure/tools.reader "1.2.2"]
810
[progrock "0.1.2"]
911
[io.aviso/pretty "0.1.34"]
10-
[mvxcvi/puget "1.0.2"]])
12+
[mvxcvi/puget "1.0.2"]]
13+
:main eftest.main)

eftest/src/eftest/main.clj

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
(ns eftest.main
2+
(:require [clojure.java.io :as io]
3+
[clojure.set :as set]
4+
[clojure.string :as string]
5+
[clojure.tools.cli :as cli]
6+
[clojure.tools.namespace.find :as find]
7+
[clojure.tools.reader.edn :as edn]
8+
[eftest.report.junit :as junit]
9+
[eftest.runner :as runner]))
10+
11+
12+
(def +default-options+
13+
{:directories ["test"]
14+
:fail-fast? false
15+
:capture-output? true
16+
:multithread? true})
17+
18+
(defn- update-in-for-coll
19+
[m f]
20+
(reduce (fn [acc [k v]]
21+
(assoc acc k (if (coll? v) (f v) v)))
22+
{} m))
23+
24+
(defn- keywords->var-filters
25+
[keywords]
26+
(into #{}
27+
(map (fn [kw] `(or (contains? (meta ~'%) ~kw)
28+
(contains? (meta (:ns (meta ~'%))) ~kw))))
29+
keywords))
30+
31+
(defn- normalize-options
32+
[options]
33+
(-> options
34+
(update-in-for-coll set)
35+
(update :directories #(into #{} (map io/file) %))
36+
(update :included-ns-regexs #(into #{} (map re-pattern) %))
37+
(update :excluded-ns-regexs #(into #{} (map re-pattern) %))
38+
(as-> m
39+
(assoc m :meta-keywords-as-var-filters
40+
(keywords->var-filters (:meta-keywords m))))
41+
(as-> m
42+
(update m :var-filters
43+
#(into % (:meta-keywords-as-var-filters m))))
44+
(cond-> (:junit-output options) (assoc :report junit/report))))
45+
46+
(defn- find-namespaces
47+
[directories]
48+
(into #{}
49+
(mapcat #(find/find-namespaces-in-dir %))
50+
directories))
51+
52+
(defn- filter-namespaces-with-names
53+
[included-namespaces excluded-namespaces namespaces]
54+
(let [incl-ns? (or included-namespaces (constantly true))
55+
excl-ns? (or excluded-namespaces (constantly false))]
56+
(into #{}
57+
(comp (filter incl-ns?)
58+
(remove excl-ns?))
59+
namespaces)))
60+
61+
(defn- ns-match-some-regexs?
62+
[regexs a-namespace]
63+
(some #(re-matches % (name a-namespace)) regexs))
64+
65+
(defn- filter-namespaces-with-regexs
66+
[included-ns-regexs excluded-ns-regexs namespaces]
67+
(let [incl-ns-regexs (if (empty? included-ns-regexs)
68+
[#".+"]
69+
included-ns-regexs)
70+
excl-ns-regexs (if (empty? excluded-ns-regexs)
71+
[#""]
72+
excluded-ns-regexs)]
73+
(into #{}
74+
(comp (filter #(ns-match-some-regexs? incl-ns-regexs %))
75+
(remove #(ns-match-some-regexs? excl-ns-regexs %)))
76+
namespaces)))
77+
78+
(defn- filter-namespaces
79+
[{:keys [included-namespaces excluded-namespaces
80+
included-ns-regexs excluded-ns-regexs] :as options}
81+
namespaces]
82+
(if (every? empty? [included-namespaces excluded-namespaces
83+
included-ns-regexs excluded-ns-regexs])
84+
namespaces
85+
(let [ns-set1 (if (and (empty? included-namespaces)
86+
(empty? excluded-namespaces))
87+
#{}
88+
(filter-namespaces-with-names included-namespaces
89+
excluded-namespaces
90+
namespaces))
91+
ns-set2 (if (and (empty? included-ns-regexs)
92+
(empty? excluded-ns-regexs))
93+
#{}
94+
(filter-namespaces-with-regexs included-ns-regexs
95+
excluded-ns-regexs
96+
namespaces))]
97+
(into ns-set1 ns-set2))))
98+
99+
(defn- find-test-vars
100+
[namespaces]
101+
(doseq [n namespaces]
102+
(require n :reload))
103+
(into #{}
104+
(comp (mapcat ns-publics)
105+
(map #(nth % 1))
106+
(filter #(contains? (meta %) :test)))
107+
namespaces))
108+
109+
(defn- filter-test-vars
110+
[{:keys [var-filters] :as options} vars]
111+
(if (empty? var-filters)
112+
vars
113+
(let [select-var? `(~'fn [~'%] (and ~@var-filters))
114+
select-var? (eval select-var?)]
115+
(into []
116+
(filter select-var?)
117+
vars))))
118+
119+
(defn find+run-tests
120+
"REPL friendly function to find and run tests"
121+
[options]
122+
(let [options (normalize-options (merge +default-options+ options))
123+
all-namespaces (find-namespaces (:directories options))
124+
namespaces (filter-namespaces options all-namespaces)
125+
all-test-vars (find-test-vars namespaces)
126+
test-vars (filter-test-vars options all-test-vars)]
127+
(runner/run-tests test-vars options)))
128+
129+
(defn- assoc-in-vec
130+
[m k v]
131+
(update-in m [k] (fnil conj []) v))
132+
133+
(defn- string->boolean
134+
[str]
135+
(boolean (re-matches #"(?i)1|yes|true" str)))
136+
137+
(def cli-options
138+
[["-d" "--directory DIR" "directory to look for namespaces, default: \"test\""
139+
:validate [seq "directory name cannot be empty"]
140+
:assoc-fn assoc-in-vec
141+
:id :directories]
142+
["-n" "--ns-include NS" "namespace included for search of test vars"
143+
:parse-fn symbol
144+
:assoc-fn assoc-in-vec
145+
:id :included-namespaces]
146+
["-e" "--ns-exclude NS" "namespace excluded for search of test vars"
147+
:parse-fn symbol
148+
:assoc-fn assoc-in-vec
149+
:id :excluded-namespaces]
150+
["-I" "--ns-include-re REGEX" "regex to include test namespaces"
151+
:assoc-fn assoc-in-vec
152+
:id :included-ns-regexs]
153+
["-X" "--ns-exclude-re REGEX" "regex to exclude test namespaces"
154+
:assoc-fn assoc-in-vec
155+
:id :excluded-ns-regexs]
156+
["-k" "--keyword KEYWORD" "keyword to filter namespaces or test vars, ex: \":integration\""
157+
:parse-fn edn/read-string
158+
:validate [keyword? "must be a keyword"]
159+
:assoc-fn assoc-in-vec
160+
:id :meta-keywords]
161+
[nil "--filter EXPR" "expression to filter test vars, a clojure expression with the var bounded to %"
162+
:parse-fn edn/read-string
163+
:assoc-fn assoc-in-vec
164+
:id :var-filters]
165+
[nil "--junit-output" "output a JUnit XML report"]
166+
[nil "--fail-fast BOOL" "stop after first failure or error, default false"
167+
:parse-fn string->boolean
168+
:id :fail-fast?]
169+
[nil "--capture-output BOOL" "catch test output and print it only if the test fails, default: true"
170+
:parse-fn string->boolean
171+
:id :capture-output?]
172+
[nil "--multithread BOOL-or-KEYWORD" "one of: true, false, :namespaces or :vars, default: true"
173+
:parse-fn edn/read-string
174+
:validate [#(or (instance? Boolean %) (#{:namespaces :vars} %)) "must be a boolean or specific keyword"]
175+
:id :multithread?]
176+
[nil "--test-warn-time MILLIS" "print a warning for any test that exceeds given time in milliseconds"
177+
:parse-fn #(Long/parseLong %)]
178+
["-h" "--help"]])
179+
180+
(defn- humain-error
181+
[error]
182+
(as-> error $
183+
(re-matches #"(Error [^:]+):.+|(.+)" $)
184+
(subvec $ 1)
185+
(apply str $)))
186+
187+
(defn- humain-errors
188+
[errors]
189+
(->> errors
190+
(into [] (map humain-error))
191+
(string/join "\n")))
192+
193+
(defn- print-args-errors
194+
[errors]
195+
(binding [*out* *err*]
196+
(println (humain-errors errors))))
197+
198+
(defn- print-usage
199+
[summary]
200+
;; TODO: add usage examples
201+
;; and info about cross option behaviours (-n, -e, -I and -X)
202+
(println summary))
203+
204+
(defn -main
205+
[& args]
206+
(let [{:keys [errors summary options]} (cli/parse-opts args cli-options)
207+
_ (when errors
208+
(print-args-errors errors)
209+
(System/exit 1))
210+
_ (when (:help options)
211+
(print-usage summary)
212+
(System/exit 0))
213+
214+
{:keys [fail error]} (find+run-tests options)
215+
exit-code (cond
216+
(and (pos? fail)
217+
(pos? error)) 30
218+
(pos? error) 20
219+
(pos? fail) 10
220+
:else 0)]
221+
(System/exit exit-code)))

0 commit comments

Comments
 (0)