Skip to content

Commit 1ed724e

Browse files
Alexander Taggartataggart
authored andcommitted
Add support for testing logs.
1 parent e28f9db commit 1ed724e

3 files changed

Lines changed: 398 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
66
([despite its flaws](https://www.youtube.com/watch?v=oyLBGkS5ICk)).
77

88
## [Unreleased]
9+
### Added
10+
- Add support for testing logs in `clojure.tools.logging.test`
911

1012
## [0.4.1] - 2018-05-07
1113
### Fixed
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
;; Copyright (c) Alex Taggart. All rights reserved. The use
2+
;; and distribution terms for this software are covered by the Eclipse
3+
;; Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
4+
;; which can be found in the file epl-v10.html at the root of this
5+
;; distribution. By using this software in any fashion, you are
6+
;; agreeing to be bound by the terms of this license. You must not
7+
;; remove this notice, or any other, from this software.
8+
9+
10+
(ns ^{:author "Alex Taggart"
11+
:doc
12+
"Support for testing whether logging calls are made.
13+
14+
Usage example:
15+
(require '[clojure.tools.logging :as log]
16+
'[clojure.tools.logging.test :refer [logged? with-log])
17+
18+
(with-log
19+
(log/info \"Hello World!\")
20+
(log/error (Exception. \"Did a thing\") \"Error: oops\")
21+
(logged? 'user :info #\"Hello\") ; true
22+
(logged? 'user :error [Throwable #\"thing\"] #\"Error:\") ; true
23+
(logged? 'user :debug \"Hi\")) ; false"}
24+
clojure.tools.logging.test
25+
(:import [clojure.lang Fn Keyword Namespace Symbol]
26+
java.util.Set
27+
java.util.regex.Pattern)
28+
(:require [clojure.tools.logging :refer [*logger-factory*]]
29+
[clojure.tools.logging.impl :as impl]))
30+
31+
32+
(defmulti match-logger-ns?
33+
"Returns true if expected matches the actual namespace. Used by
34+
LogEntry/matches? implementation.
35+
36+
Dispatches on the types of expected and actual.
37+
38+
Provided methods' dispatch values and matching:
39+
:default if equal
40+
[Fn Object] if (f actual) is logically true
41+
[String Namespace] if string equals namespace's string
42+
[Symbol Namespace] if symbol equals namespace's symbol"
43+
(fn [expected actual]
44+
[(type expected) (type actual)]))
45+
46+
(defmethod match-logger-ns? :default
47+
[expected actual]
48+
(= expected actual))
49+
50+
(defmethod match-logger-ns? [Fn Object]
51+
[expected actual]
52+
(expected actual))
53+
54+
(defmethod match-logger-ns? [String Namespace]
55+
[expected actual]
56+
(= expected (str actual)))
57+
58+
(defmethod match-logger-ns? [Symbol Namespace]
59+
[expected ^Namespace actual]
60+
(= expected (.name actual)))
61+
62+
63+
64+
(defmulti match-level?
65+
"Returns true if expected matches the actual level. Used by
66+
LogEntry/matches? implementation.
67+
68+
Dispatches on the types of expected and actual.
69+
70+
Provided methods' dispatch values and matching:
71+
:default if equal
72+
[Fn Object] if (f actual) is logically true
73+
[Set Object] if set contains actual"
74+
(fn [expected actual]
75+
[(type expected) (type actual)]))
76+
77+
(defmethod match-level? :default
78+
[expected actual]
79+
(= expected actual))
80+
81+
(defmethod match-level? [Fn Object]
82+
[expected actual]
83+
(expected actual))
84+
85+
(defmethod match-level? [Set Object]
86+
[expected actual]
87+
(contains? expected actual))
88+
89+
90+
91+
(defmulti match-throwable?
92+
"Returns true if expected matches the actual throwable. Used by
93+
LogEntry/matches? implementation.
94+
95+
Dispatches on the types of expected and actual. If expected is a vector, will
96+
instead use a vector of the contained types.
97+
98+
Provided methods' dispatch values and matching:
99+
:default if equal
100+
[Fn Object] if (f actual) is logically true
101+
[Class Object] if actual is an instance of Class
102+
[[Class String] Throwable] ... and if string equals exception message
103+
[[Class Pattern] Throwable] ... and if pattern matches exception message"
104+
(fn [expected actual]
105+
(if (vector? expected)
106+
[(mapv type expected) (type actual)]
107+
[(type expected) (type actual)])))
108+
109+
(defmethod match-throwable? :default
110+
[expected actual]
111+
(= expected actual))
112+
113+
(defmethod match-throwable? [Fn Object]
114+
[expected actual]
115+
(expected actual))
116+
117+
(defmethod match-throwable? [Class Object]
118+
[expected actual]
119+
(instance? expected actual))
120+
121+
(defmethod match-throwable? [[Class String] Throwable]
122+
[[expected-ex expected-msg] ^Throwable actual]
123+
(and (match-throwable? expected-ex actual)
124+
(= expected-msg (.getMessage actual))))
125+
126+
(defmethod match-throwable? [[Class Pattern] Throwable]
127+
[[expected-ex expected-msg] ^Throwable actual]
128+
(and (match-throwable? expected-ex actual)
129+
(boolean (re-find expected-msg (.getMessage actual)))))
130+
131+
132+
133+
(defmulti match-message?
134+
"Returns true if expected matches the actual message. Used by
135+
LogEntry/matches? implementation.
136+
137+
Dispatches on the types of expected and actual.
138+
139+
Provided methods' dispatch values and matching:
140+
:default if equal
141+
[Fn Object] if (f actual) is logically true
142+
[Pattern String] if pattern matches actual"
143+
(fn [expected actual]
144+
[(type expected) (type actual)]))
145+
146+
(defmethod match-message? :default
147+
[expected actual]
148+
(= expected actual))
149+
150+
(defmethod match-message? [Fn Object]
151+
[expected actual]
152+
(expected actual))
153+
154+
(defmethod match-message? [Pattern String]
155+
[expected actual]
156+
(boolean (re-find expected actual)))
157+
158+
159+
160+
(defprotocol MatchableLogEntry
161+
(matches? [this logger-ns level throwable message]))
162+
163+
164+
165+
(defrecord LogEntry [logger-ns level throwable message]
166+
MatchableLogEntry
167+
(matches? [_ exp-logger-ns exp-level exp-throwable exp-message]
168+
(and (match-logger-ns? exp-logger-ns logger-ns)
169+
(match-level? exp-level level)
170+
(match-throwable? exp-throwable throwable)
171+
(match-message? exp-message message))))
172+
173+
174+
175+
(defprotocol StatefulLog
176+
(entries [this]
177+
"Returns a vector of the entries in this log, oldest first.")
178+
(append! [this logger-ns level throwable message]
179+
"Returns this log with a new log entry appended."))
180+
181+
182+
183+
(defn atomic-log
184+
"Returns a StatefulLog, appending to an atom the result of invoking
185+
log-entry-fn with the same args as append!"
186+
[log-entry-fn]
187+
(let [log (atom [])]
188+
(reify
189+
StatefulLog
190+
(entries [_]
191+
(deref log))
192+
(append! [this logger-ns level throwable message]
193+
(swap! log (fnil conj []) (log-entry-fn logger-ns level throwable message))
194+
this))))
195+
196+
197+
198+
(defn logger-factory
199+
"Returns a LoggerFactory that will append log entries to stateful-log. Levels
200+
are enabled when (enabled-pred logger-ns level) is true."
201+
[stateful-log enabled-pred]
202+
(reify impl/LoggerFactory
203+
(name [_] "clojure.tools.logging.test/logger-factory")
204+
(get-logger [_ logger-ns]
205+
(reify impl/Logger
206+
(enabled? [_ level] (enabled-pred logger-ns level))
207+
(write! [_ level throwable message]
208+
(append! stateful-log logger-ns level throwable message)
209+
nil)))))
210+
211+
212+
213+
(def ^:dynamic
214+
^{:doc "The instance of StatefulLog used by with-log. By default unbound."}
215+
*stateful-log*)
216+
217+
218+
219+
(defn the-log
220+
"Returns the vector of current log entries.
221+
222+
Must be invoked within a context where *stateful-log* is bound to an instance
223+
of StatefulLog containing MatchableLogEntry items (e.g., inside with-log)."
224+
[]
225+
(entries *stateful-log*))
226+
227+
228+
229+
(defn matches
230+
"Returns matching log entries, otherwise nil.
231+
232+
Must be invoked within a context where *stateful-log* is bound to an instance
233+
of StatefulLog containing MatchableLogEntry items (e.g., inside with-log)."
234+
([logger-ns level message]
235+
(matches logger-ns level nil message))
236+
([logger-ns level throwable message]
237+
(seq (filter #(matches? % logger-ns level throwable message) (the-log)))))
238+
239+
240+
241+
(defn logged?
242+
"Returns true if the log contains matching entries.
243+
244+
Must be invoked within a context where *stateful-log* is bound to an instance
245+
of StatefulLog containing MatchableLogEntry items (e.g., inside with-log)."
246+
([logger-ns level message]
247+
(boolean (matches logger-ns level message)))
248+
([logger-ns level throwable message]
249+
(boolean (matches logger-ns level throwable message))))
250+
251+
252+
253+
(defmacro with-log [& body]
254+
"Evaluates body within a context where logging is collected.
255+
256+
See logged?, matches, the-log."
257+
`(let [stateful-log# (atomic-log ->LogEntry)
258+
logger-factory# (logger-factory stateful-log# (constantly true))]
259+
(binding [*stateful-log* stateful-log#
260+
*logger-factory* logger-factory#]
261+
~@body)))
262+
263+
264+

0 commit comments

Comments
 (0)