Skip to content

Commit 3df9b17

Browse files
authored
Redact fully-matched data-structures from output (#214)
* attach meta to seq/maps that don't match * use mismatch metadata to elide matched results when printing * add enabling/disabling of output redaction of matched data-structures users can call `(matcher-combinators.config/enable-redaction!)` in their test startup to enable redaction * create matcher-combinators.config and move ansi & redact controls there * include ... ellision element when printing redacted results e.g. for maps: ``` {:a [{:b [(mismatch (expected 5) (actual 6))]}] ... } ``` and for sequences: ``` [{:b [(mismatch (expected 5) (actual 6))]}] ...] ```
1 parent 5799704 commit 3df9b17

File tree

9 files changed

+219
-30
lines changed

9 files changed

+219
-30
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ pom.xml.asc
1212
.hg/
1313
.clj-kondo/*
1414
.shadow-cljs/*
15+
.cpcache/*
16+
.lsp/*

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ All notable changes to this project will be documented in this file. This
33
change log follows the conventions of
44
[keepachangelog.com](http://keepachangelog.com/).
55

6-
## 3.8.7 / 2023-08-31
6+
## 3.8.7 / 2023-09-01
7+
- introduce `matcher-combinators.config` namespace to toggle use of ansi color
8+
codes and the new output abbreviation mode.
9+
- [Experimental] add `(matcher-combinators.config/enable-abbreviation!)`, an
10+
experimental feature to print only the mismatched parts of a data-structure
11+
while elliding the matched parts.
712
- fix more issues when using non-composite matchers (`m/regex`, `m/pred`, etc)
813
inside `match-with`.
914

src/clj/matcher_combinators/clj_test.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@
204204
:actual '~form}))))))
205205

206206
(defmethod clojure.test/assert-expr 'match-equals? [msg form]
207+
;; DEPRECATED
207208
(build-match-assert 'match-equals?
208209
{clojure.lang.IPersistentMap matchers/equals}
209210
msg
@@ -212,6 +213,7 @@
212213
" Use (match? (matchers/match-with [map? matchers/equals] <expected>) <actual>) instead.")))
213214

214215
(defmethod clojure.test/assert-expr 'match-roughly? [msg form]
216+
;; DEPRECATED
215217
(let [directive (first form)
216218
delta (second form)
217219
the-rest (rest (rest form))

src/cljc/matcher_combinators/ansi_color.cljc

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@
77
*use-color*
88
true)
99

10-
(defn- set-use-color! [v]
11-
#?(:clj (alter-var-root #'*use-color* (constantly v))
12-
:cljs (set! *use-color* v)))
13-
14-
(defn enable!
15-
"Thread-global way to enable the usage of ANSI color codes in matcher-combinator output."
10+
(defn
11+
^{:deprecated true
12+
:doc "DEPRECATED! Use matcher-combinators.config/enable-ansi-color!"}
13+
enable!
1614
[]
17-
(set-use-color! true))
15+
#?(:clj (alter-var-root #'*use-color* (constantly true))
16+
:cljs (set! *use-color* true)))
1817

1918
(defn disable!
20-
"Thread-global way to disable the usage of ANSI color codes in matcher-combinator output."
19+
^{:deprecated true
20+
:doc "DEPRECATED! Use matcher-combinators.config/disable-ansi-color!"}
2121
[]
22-
(set-use-color! false))
22+
#?(:clj (alter-var-root #'*use-color* (constantly false))
23+
:cljs (set! *use-color* false)))
2324

2425
(def ANSI-CODES
2526
{:reset "[0m"
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
(ns matcher-combinators.config
2+
"Global output behavior configurations"
3+
(:require [matcher-combinators.ansi-color :as ansi-color]))
4+
5+
;; Abbreviating match results to only include mismatched data in the output
6+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
7+
8+
(def ^{:dynamic true
9+
:doc "thread-local way to control, via `binding`, the abbreviation of fully-matched data-structures in the matcher-combinator output"}
10+
*use-abbreviation*
11+
false)
12+
13+
(defn- set-use-abbreviation!
14+
"internal function, use matcher-combinators.config/{enable|disable}-abbreviation!"
15+
[v]
16+
#?(:clj (alter-var-root #'*use-abbreviation* (constantly v))
17+
:cljs (set! *use-abbreviation* v)))
18+
19+
(defn enable-abbreviation!
20+
"**Experimental, subject to change**
21+
Thread-global way to enable the abbreviation of fully-matched data-structures in matcher-combinator output."
22+
[]
23+
(set-use-abbreviation! true))
24+
25+
(defn disable-abbreviation!
26+
"**Experimental, subject to change**
27+
Thread-global way to disable the abbreviation of fully-matched data-structures in matcher-combinator output."
28+
[]
29+
(set-use-abbreviation! false))
30+
31+
32+
;; Disable special ANSI color characters in output
33+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
34+
35+
(defn- set-use-color! [v]
36+
#?(:clj (alter-var-root #'ansi-color/*use-color* (constantly v))
37+
:cljs (set! ansi-color/*use-color* v)))
38+
39+
(defn enable-ansi-color!
40+
"Thread-global way to enable the usage of ANSI color codes in matcher-combinator output."
41+
[]
42+
(set-use-color! true))
43+
44+
(defn disable-ansi-color!
45+
"Thread-global way to disable the usage of ANSI color codes in matcher-combinator output."
46+
[]
47+
(set-use-color! false))

src/cljc/matcher_combinators/core.cljc

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns matcher-combinators.core
22
(:require [clojure.math.combinatorics :as combo]
33
[clojure.pprint]
4+
[clojure.string :as string]
45
[matcher-combinators.model :as model]
56
[matcher-combinators.result :as result]
67
[matcher-combinators.utils :as utils]))
@@ -169,6 +170,15 @@
169170
nil)
170171
[key (match matcher (get actual key ::missing))]))
171172

173+
(defn- with-mismatch-meta
174+
"Tags element with data that allows abbreviation of matched data-structures
175+
in test output when desired"
176+
[elem mismatch-meta]
177+
(if #?(:clj (instance? clojure.lang.IMeta elem)
178+
:cljs (satisfies? IMeta elem))
179+
(with-meta elem {:mismatch mismatch-meta})
180+
elem))
181+
172182
(defn- compare-maps [expected actual unexpected-handler allow-unexpected?]
173183
(let [entry-results (->> expected
174184
(map (partial match-kv actual))
@@ -191,7 +201,7 @@
191201
(reduce (fn [acc-weight result] (+ acc-weight (::result/weight result)))
192202
(if allow-unexpected? 0 (count unexpected-entries))))]
193203
{::result/type :mismatch
194-
::result/value mismatch-val
204+
::result/value (with-mismatch-meta mismatch-val :mismatch-map)
195205
::result/weight weight}))))
196206

197207
(def ^:private map-like?
@@ -288,7 +298,9 @@
288298
match-results (take match-size match-results')]
289299
(if (some (complement indicates-match?) match-results)
290300
{::result/type :mismatch
291-
::result/value (type-preserving-mismatch (empty actual) (map ::result/value match-results))
301+
::result/value (with-mismatch-meta
302+
(type-preserving-mismatch (empty actual) (map ::result/value match-results))
303+
:mismatch-sequence)
292304
::result/weight (->> match-results
293305
(map ::result/weight)
294306
(reduce + 0))}
@@ -588,3 +600,7 @@
588600
(value-match (.toString expected)
589601
(.toString actual))))
590602
(-base-name [_] 'equals))
603+
604+
(defn non-internal-record? [v]
605+
(and (record? v)
606+
(not (string/starts-with? (-> v type str) "class matcher_combinators.core"))))

src/cljc/matcher_combinators/matchers.cljc

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
(ns matcher-combinators.matchers
22
(:require #?(:cljs [matcher-combinators.core :as core :refer [Matcher Regex Value Absent PredMatcher]]
33
:clj [matcher-combinators.core :as core])
4-
[clojure.string :as string]
54
[matcher-combinators.utils :as utils])
65
#?(:clj (:import [matcher_combinators.core Matcher])))
76

8-
(defn- non-internal-record? [v]
9-
(and (record? v)
10-
(not (string/starts-with? (-> v type str) "class matcher_combinators.core"))))
11-
127
(defn equals
138
"Matcher that will match when the given value is exactly the same as the
149
`expected`.
@@ -21,12 +16,20 @@
2116
for example, continue using their default matcher. If you want to do a deep
2217
match, consider using `match-with` instead."
2318
[expected]
24-
(cond
25-
(sequential? expected) (core/->EqualsSeq expected)
26-
(set? expected) (core/->SetEquals expected false)
27-
(non-internal-record? expected) (core/->EqualsRecord expected)
28-
(map? expected) (core/->EqualsMap expected)
29-
:else (core/->Value expected)))
19+
(cond (sequential? expected)
20+
(core/->EqualsSeq expected)
21+
22+
(set? expected)
23+
(core/->SetEquals expected false)
24+
25+
(core/non-internal-record? expected)
26+
(core/->EqualsRecord expected)
27+
28+
(map? expected)
29+
(core/->EqualsMap expected)
30+
31+
:else
32+
(core/->Value expected)))
3033

3134
(defn seq-of
3235
"Matcher that will match when given a sequence where every element matches
@@ -54,12 +57,20 @@
5457
matched with an element in the provided set. There may be more
5558
elements in the provided set than there are matchers."
5659
[expected]
57-
(cond
58-
(sequential? expected) (core/->EmbedsSeq expected)
59-
(set? expected) (core/->SetEmbeds expected false)
60-
(non-internal-record? expected) (core/->EqualsRecord expected)
61-
(map? expected) (core/->EmbedsMap expected)
62-
:else (core/->InvalidType expected "embeds" "seq, set, map")))
60+
(cond (sequential? expected)
61+
(core/->EmbedsSeq expected)
62+
63+
(set? expected)
64+
(core/->SetEmbeds expected false)
65+
66+
(core/non-internal-record? expected)
67+
(core/->EqualsRecord expected)
68+
69+
(map? expected)
70+
(core/->EmbedsMap expected)
71+
72+
:else
73+
(core/->InvalidType expected "embeds" "seq, set, map")))
6374

6475
(defn set-embeds
6576
"Matches a set in the way `(embeds some-set)` would, but accepts sequences

src/cljc/matcher_combinators/printer.cljc

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
(ns matcher-combinators.printer
22
(:refer-clojure :exclude [print])
33
(:require [clojure.pprint :as pprint]
4+
[matcher-combinators.config :as config]
5+
[matcher-combinators.core :as core]
46
#?(:clj [matcher-combinators.model]
57
:cljs [matcher-combinators.model :refer [ExpectedMismatch
68
Mismatch
@@ -9,6 +11,7 @@
911
TypeMismatch
1012
InvalidMatcherContext
1113
InvalidMatcherType]])
14+
[clojure.walk :as walk]
1215
[matcher-combinators.ansi-color :as ansi-color])
1316
#?(:clj
1417
(:import [matcher_combinators.model ExpectedMismatch Mismatch Missing
@@ -66,10 +69,66 @@
6669
(colorized-print markup)
6770
(pprint/simple-dispatch markup))))
6871

72+
(defrecord EllisionMarker [])
73+
(defmethod markup-expression EllisionMarker [_] '...)
74+
(def ellision-marker (EllisionMarker.))
75+
76+
(defrecord EmptyMarker [])
77+
(defmethod markup-expression EmptyMarker [_] (symbol ""))
78+
(def empty-marker (EmptyMarker.))
79+
80+
(defn with-ellision-marker
81+
"Include `...` in mismatch data-structure to show that the match output has
82+
been abbreviated"
83+
[expr]
84+
(cond (or (sequential? expr)
85+
(set? expr))
86+
(conj expr ellision-marker)
87+
88+
(and (map? expr) (not (core/non-internal-record? expr)))
89+
(assoc expr ellision-marker empty-marker)
90+
91+
:else
92+
expr))
93+
94+
(defn- mismatch? [expr]
95+
(or (instance? EllisionMarker expr)
96+
(instance? EmptyMarker expr)
97+
(instance? Mismatch expr)
98+
(instance? Missing expr)
99+
(instance? Unexpected expr)
100+
(instance? InvalidMatcherType expr)
101+
(instance? InvalidMatcherContext expr)
102+
(instance? TypeMismatch expr)))
103+
104+
(defn- mismatch+? [x]
105+
(or (mismatch? x)
106+
(= :mismatch-map (:mismatch (meta x)))
107+
(= :mismatch-sequence (:mismatch (meta x)))))
108+
109+
(defn abbreviated [expr]
110+
(walk/prewalk (fn [x]
111+
(cond (mismatch? x)
112+
x
113+
114+
(= :mismatch-map (:mismatch (meta x)))
115+
;; keep only mismatched data from the mismatched map
116+
(into {} (filter (fn [[_k v]] (mismatch+? v))) x)
117+
118+
(= :mismatch-sequence (:mismatch (meta x)))
119+
;; keep only mismatched data from the sequence
120+
(#'core/type-preserving-mismatch (empty x) (filter mismatch+? x))
121+
122+
:else
123+
x))
124+
expr))
125+
69126
(defn pretty-print [expr]
70127
(pprint/with-pprint-dispatch
71128
print-diff-dispatch
72-
(pprint/pprint expr)))
129+
(pprint/pprint (if config/*use-abbreviation*
130+
((comp abbreviated with-ellision-marker) expr)
131+
expr))))
73132

74133
(defn as-string [value]
75134
(with-out-str
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
(ns matcher-combinators.config-test
2+
(:require [clojure.test :refer [deftest is testing use-fixtures]]
3+
[colorize.core :as colorize]
4+
[matcher-combinators.config :as config]
5+
[matcher-combinators.core :as c]
6+
[matcher-combinators.printer :as printer]
7+
matcher-combinators.test))
8+
9+
(defn set-config-defaults! []
10+
(config/enable-ansi-color!)
11+
(config/disable-abbreviation!))
12+
13+
(use-fixtures :each
14+
(fn [t]
15+
(set-config-defaults!)
16+
(t)
17+
(set-config-defaults!)))
18+
19+
(deftest ansi-color-test
20+
(testing "with color"
21+
(is (= (str "(unexpected " (colorize/red 1) ")\n")
22+
(printer/as-string (list 'unexpected (printer/->ColorTag :red 1))))))
23+
(testing "disable coloring"
24+
(config/disable-ansi-color!)
25+
(is (= (str "(unexpected 1)\n")
26+
(printer/as-string (list 'unexpected (printer/->ColorTag :red 1)))))))
27+
28+
(deftest abbreviated-matched-output-test
29+
(is (= (str "[1\n 2\n {:a 2,\n :b [4 (mismatch (expected " (colorize/yellow 5) ") (actual " (colorize/red 6) "))],\n :c [2 [3 4]]}]\n")
30+
(printer/as-string
31+
(:matcher-combinators.result/value
32+
(c/match [1 2 {:a 2 :b [4 5] :c [2 [3 4]]}]
33+
[1 2 {:a 2 :b [4 6] :c [2 [3 4]]}])))))
34+
35+
(config/enable-abbreviation!)
36+
(is (= (str "{:stuff [{:b [(mismatch (expected " (colorize/yellow 5) ") (actual " (colorize/red 6) "))]}],\n ... }\n")
37+
(printer/as-string
38+
(:matcher-combinators.result/value
39+
(c/match {:stuff [1 2 {:a 2 :b [4 5] :c [2 [3 4]]}]}
40+
{:stuff [1 2 {:a 2 :b [4 6] :c [2 [3 4]]}]})))))
41+
42+
(is (= (str "[{:b [(mismatch (expected " (colorize/yellow 5) ") (actual " (colorize/red 6) "))]} ...]\n")
43+
(printer/as-string
44+
(:matcher-combinators.result/value
45+
(c/match [1 2 {:a 2 :b [4 5] :c [2 [3 4]]}]
46+
[1 2 {:a 2 :b [4 6] :c [2 [3 4]]}]))))))

0 commit comments

Comments
 (0)