Skip to content

Commit

Permalink
Redact fully-matched data-structures from output (#214)
Browse files Browse the repository at this point in the history
* 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))]}] ...]
```
  • Loading branch information
philomates authored Sep 1, 2023
1 parent 5799704 commit 3df9b17
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 30 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ pom.xml.asc
.hg/
.clj-kondo/*
.shadow-cljs/*
.cpcache/*
.lsp/*
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ All notable changes to this project will be documented in this file. This
change log follows the conventions of
[keepachangelog.com](http://keepachangelog.com/).

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

Expand Down
2 changes: 2 additions & 0 deletions src/clj/matcher_combinators/clj_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
:actual '~form}))))))

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

(defmethod clojure.test/assert-expr 'match-roughly? [msg form]
;; DEPRECATED
(let [directive (first form)
delta (second form)
the-rest (rest (rest form))
Expand Down
19 changes: 10 additions & 9 deletions src/cljc/matcher_combinators/ansi_color.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@
*use-color*
true)

(defn- set-use-color! [v]
#?(:clj (alter-var-root #'*use-color* (constantly v))
:cljs (set! *use-color* v)))

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

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

(def ANSI-CODES
{:reset "[0m"
Expand Down
47 changes: 47 additions & 0 deletions src/cljc/matcher_combinators/config.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
(ns matcher-combinators.config
"Global output behavior configurations"
(:require [matcher-combinators.ansi-color :as ansi-color]))

;; Abbreviating match results to only include mismatched data in the output
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def ^{:dynamic true
:doc "thread-local way to control, via `binding`, the abbreviation of fully-matched data-structures in the matcher-combinator output"}
*use-abbreviation*
false)

(defn- set-use-abbreviation!
"internal function, use matcher-combinators.config/{enable|disable}-abbreviation!"
[v]
#?(:clj (alter-var-root #'*use-abbreviation* (constantly v))
:cljs (set! *use-abbreviation* v)))

(defn enable-abbreviation!
"**Experimental, subject to change**
Thread-global way to enable the abbreviation of fully-matched data-structures in matcher-combinator output."
[]
(set-use-abbreviation! true))

(defn disable-abbreviation!
"**Experimental, subject to change**
Thread-global way to disable the abbreviation of fully-matched data-structures in matcher-combinator output."
[]
(set-use-abbreviation! false))


;; Disable special ANSI color characters in output
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn- set-use-color! [v]
#?(:clj (alter-var-root #'ansi-color/*use-color* (constantly v))
:cljs (set! ansi-color/*use-color* v)))

(defn enable-ansi-color!
"Thread-global way to enable the usage of ANSI color codes in matcher-combinator output."
[]
(set-use-color! true))

(defn disable-ansi-color!
"Thread-global way to disable the usage of ANSI color codes in matcher-combinator output."
[]
(set-use-color! false))
20 changes: 18 additions & 2 deletions src/cljc/matcher_combinators/core.cljc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns matcher-combinators.core
(:require [clojure.math.combinatorics :as combo]
[clojure.pprint]
[clojure.string :as string]
[matcher-combinators.model :as model]
[matcher-combinators.result :as result]
[matcher-combinators.utils :as utils]))
Expand Down Expand Up @@ -169,6 +170,15 @@
nil)
[key (match matcher (get actual key ::missing))]))

(defn- with-mismatch-meta
"Tags element with data that allows abbreviation of matched data-structures
in test output when desired"
[elem mismatch-meta]
(if #?(:clj (instance? clojure.lang.IMeta elem)
:cljs (satisfies? IMeta elem))
(with-meta elem {:mismatch mismatch-meta})
elem))

(defn- compare-maps [expected actual unexpected-handler allow-unexpected?]
(let [entry-results (->> expected
(map (partial match-kv actual))
Expand All @@ -191,7 +201,7 @@
(reduce (fn [acc-weight result] (+ acc-weight (::result/weight result)))
(if allow-unexpected? 0 (count unexpected-entries))))]
{::result/type :mismatch
::result/value mismatch-val
::result/value (with-mismatch-meta mismatch-val :mismatch-map)
::result/weight weight}))))

(def ^:private map-like?
Expand Down Expand Up @@ -288,7 +298,9 @@
match-results (take match-size match-results')]
(if (some (complement indicates-match?) match-results)
{::result/type :mismatch
::result/value (type-preserving-mismatch (empty actual) (map ::result/value match-results))
::result/value (with-mismatch-meta
(type-preserving-mismatch (empty actual) (map ::result/value match-results))
:mismatch-sequence)
::result/weight (->> match-results
(map ::result/weight)
(reduce + 0))}
Expand Down Expand Up @@ -588,3 +600,7 @@
(value-match (.toString expected)
(.toString actual))))
(-base-name [_] 'equals))

(defn non-internal-record? [v]
(and (record? v)
(not (string/starts-with? (-> v type str) "class matcher_combinators.core"))))
45 changes: 28 additions & 17 deletions src/cljc/matcher_combinators/matchers.cljc
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
(ns matcher-combinators.matchers
(:require #?(:cljs [matcher-combinators.core :as core :refer [Matcher Regex Value Absent PredMatcher]]
:clj [matcher-combinators.core :as core])
[clojure.string :as string]
[matcher-combinators.utils :as utils])
#?(:clj (:import [matcher_combinators.core Matcher])))

(defn- non-internal-record? [v]
(and (record? v)
(not (string/starts-with? (-> v type str) "class matcher_combinators.core"))))

(defn equals
"Matcher that will match when the given value is exactly the same as the
`expected`.
Expand All @@ -21,12 +16,20 @@
for example, continue using their default matcher. If you want to do a deep
match, consider using `match-with` instead."
[expected]
(cond
(sequential? expected) (core/->EqualsSeq expected)
(set? expected) (core/->SetEquals expected false)
(non-internal-record? expected) (core/->EqualsRecord expected)
(map? expected) (core/->EqualsMap expected)
:else (core/->Value expected)))
(cond (sequential? expected)
(core/->EqualsSeq expected)

(set? expected)
(core/->SetEquals expected false)

(core/non-internal-record? expected)
(core/->EqualsRecord expected)

(map? expected)
(core/->EqualsMap expected)

:else
(core/->Value expected)))

(defn seq-of
"Matcher that will match when given a sequence where every element matches
Expand Down Expand Up @@ -54,12 +57,20 @@
matched with an element in the provided set. There may be more
elements in the provided set than there are matchers."
[expected]
(cond
(sequential? expected) (core/->EmbedsSeq expected)
(set? expected) (core/->SetEmbeds expected false)
(non-internal-record? expected) (core/->EqualsRecord expected)
(map? expected) (core/->EmbedsMap expected)
:else (core/->InvalidType expected "embeds" "seq, set, map")))
(cond (sequential? expected)
(core/->EmbedsSeq expected)

(set? expected)
(core/->SetEmbeds expected false)

(core/non-internal-record? expected)
(core/->EqualsRecord expected)

(map? expected)
(core/->EmbedsMap expected)

:else
(core/->InvalidType expected "embeds" "seq, set, map")))

(defn set-embeds
"Matches a set in the way `(embeds some-set)` would, but accepts sequences
Expand Down
61 changes: 60 additions & 1 deletion src/cljc/matcher_combinators/printer.cljc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
(ns matcher-combinators.printer
(:refer-clojure :exclude [print])
(:require [clojure.pprint :as pprint]
[matcher-combinators.config :as config]
[matcher-combinators.core :as core]
#?(:clj [matcher-combinators.model]
:cljs [matcher-combinators.model :refer [ExpectedMismatch
Mismatch
Expand All @@ -9,6 +11,7 @@
TypeMismatch
InvalidMatcherContext
InvalidMatcherType]])
[clojure.walk :as walk]
[matcher-combinators.ansi-color :as ansi-color])
#?(:clj
(:import [matcher_combinators.model ExpectedMismatch Mismatch Missing
Expand Down Expand Up @@ -66,10 +69,66 @@
(colorized-print markup)
(pprint/simple-dispatch markup))))

(defrecord EllisionMarker [])
(defmethod markup-expression EllisionMarker [_] '...)
(def ellision-marker (EllisionMarker.))

(defrecord EmptyMarker [])
(defmethod markup-expression EmptyMarker [_] (symbol ""))
(def empty-marker (EmptyMarker.))

(defn with-ellision-marker
"Include `...` in mismatch data-structure to show that the match output has
been abbreviated"
[expr]
(cond (or (sequential? expr)
(set? expr))
(conj expr ellision-marker)

(and (map? expr) (not (core/non-internal-record? expr)))
(assoc expr ellision-marker empty-marker)

:else
expr))

(defn- mismatch? [expr]
(or (instance? EllisionMarker expr)
(instance? EmptyMarker expr)
(instance? Mismatch expr)
(instance? Missing expr)
(instance? Unexpected expr)
(instance? InvalidMatcherType expr)
(instance? InvalidMatcherContext expr)
(instance? TypeMismatch expr)))

(defn- mismatch+? [x]
(or (mismatch? x)
(= :mismatch-map (:mismatch (meta x)))
(= :mismatch-sequence (:mismatch (meta x)))))

(defn abbreviated [expr]
(walk/prewalk (fn [x]
(cond (mismatch? x)
x

(= :mismatch-map (:mismatch (meta x)))
;; keep only mismatched data from the mismatched map
(into {} (filter (fn [[_k v]] (mismatch+? v))) x)

(= :mismatch-sequence (:mismatch (meta x)))
;; keep only mismatched data from the sequence
(#'core/type-preserving-mismatch (empty x) (filter mismatch+? x))

:else
x))
expr))

(defn pretty-print [expr]
(pprint/with-pprint-dispatch
print-diff-dispatch
(pprint/pprint expr)))
(pprint/pprint (if config/*use-abbreviation*
((comp abbreviated with-ellision-marker) expr)
expr))))

(defn as-string [value]
(with-out-str
Expand Down
46 changes: 46 additions & 0 deletions test/clj/matcher_combinators/config_test.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
(ns matcher-combinators.config-test
(:require [clojure.test :refer [deftest is testing use-fixtures]]
[colorize.core :as colorize]
[matcher-combinators.config :as config]
[matcher-combinators.core :as c]
[matcher-combinators.printer :as printer]
matcher-combinators.test))

(defn set-config-defaults! []
(config/enable-ansi-color!)
(config/disable-abbreviation!))

(use-fixtures :each
(fn [t]
(set-config-defaults!)
(t)
(set-config-defaults!)))

(deftest ansi-color-test
(testing "with color"
(is (= (str "(unexpected " (colorize/red 1) ")\n")
(printer/as-string (list 'unexpected (printer/->ColorTag :red 1))))))
(testing "disable coloring"
(config/disable-ansi-color!)
(is (= (str "(unexpected 1)\n")
(printer/as-string (list 'unexpected (printer/->ColorTag :red 1)))))))

(deftest abbreviated-matched-output-test
(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")
(printer/as-string
(:matcher-combinators.result/value
(c/match [1 2 {:a 2 :b [4 5] :c [2 [3 4]]}]
[1 2 {:a 2 :b [4 6] :c [2 [3 4]]}])))))

(config/enable-abbreviation!)
(is (= (str "{:stuff [{:b [(mismatch (expected " (colorize/yellow 5) ") (actual " (colorize/red 6) "))]}],\n ... }\n")
(printer/as-string
(:matcher-combinators.result/value
(c/match {:stuff [1 2 {:a 2 :b [4 5] :c [2 [3 4]]}]}
{:stuff [1 2 {:a 2 :b [4 6] :c [2 [3 4]]}]})))))

(is (= (str "[{:b [(mismatch (expected " (colorize/yellow 5) ") (actual " (colorize/red 6) "))]} ...]\n")
(printer/as-string
(:matcher-combinators.result/value
(c/match [1 2 {:a 2 :b [4 5] :c [2 [3 4]]}]
[1 2 {:a 2 :b [4 6] :c [2 [3 4]]}]))))))

0 comments on commit 3df9b17

Please sign in to comment.