|
| 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