From 68bfc66fad18b2457ffff95ae885889e1cf839f6 Mon Sep 17 00:00:00 2001 From: Roman Scherer Date: Sat, 20 Aug 2022 14:48:39 +0000 Subject: [PATCH] Add datafy section to inspector --- CHANGELOG.md | 12 + README.md | 2 +- doc/inspector.org | 648 +++++++++++++++++++++++++++++ src/orchard/inspect.clj | 338 ++++++++++----- src/orchard/misc.clj | 28 ++ test/orchard/inspect_test.clj | 759 +++++++++++++++++++++++++++++----- test/orchard/misc_test.clj | 17 + 7 files changed, 1618 insertions(+), 186 deletions(-) create mode 100644 doc/inspector.org diff --git a/CHANGELOG.md b/CHANGELOG.md index 95864e3a..8c99610b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ ## master (unreleased) * [#158](https://github.com/clojure-emacs/orchard/issues/158): Make classpath-namespaces resilient to faulty ns declarations. +### Changes + +* [#161](https://github.com/clojure-emacs/orchard/pull/161): Add Datafy section to inspector and align section headers + * Add a `Datafy` section to the inspector. For more details, take a + look at the + [Datafiable](https://github.com/clojure-emacs/orchard/blob/master/doc/inspector.org#datafiable) + and + [Navigable](https://github.com/clojure-emacs/orchard/blob/master/doc/inspector.org#navigable) + sections of the Orchard inspector + [docs](https://github.com/clojure-emacs/orchard/blob/master/doc/inspector.org). + * Align all section headers to start with `---`. + ## 0.9.2 (2022-02-22) * Guard against OOMs in `orchard.java/member-info`. diff --git a/README.md b/README.md index 3c6df588..ead91781 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Right now `orchard` provides functionality like: * enhanced apropos * classpath utils (alternative for `java.classpath`) -* value inspector +* value [inspector](https://github.com/clojure-emacs/orchard/blob/master/doc/inspector.org) * Java class handling utilities * Utilities for dealing with metadata * Namespace utilities diff --git a/doc/inspector.org b/doc/inspector.org new file mode 100644 index 00000000..a808c31f --- /dev/null +++ b/doc/inspector.org @@ -0,0 +1,648 @@ +* The Orchard Inspector + +The Orchard Inspector provides functionality to inspect Clojure and +Java objects and is a useful tool for debugging large data +structures. It is inspired by the [[https://slime.common-lisp.dev/doc/html/Inspector.html][Slime Inspector]] for Common LISP and +is used in the [[https://github.com/clojure-emacs/cider-nrepl][Cider NREPL middleware]] to power the [[https://docs.cider.mx/cider/debugging/inspector.html][Cider Inspector]]. + +** Usage + +The Orchard inspector is a Clojure map that implements a stack and +holds information about an inspected object. The =orchard.inspect= +namespace provides functions to create the inspector data structure +and to manipulate it, such as drilling down into an object, moving up +and down the stack, configuring the inspector and paginating large +collections. + +#+begin_src clojure :exports code :results silent + (require '[orchard.inspect :as inspect]) +#+end_src + +*** The inspector data structure + +To create the inspector data structure we can use the =inspect/fresh= +function. It returns an empty inspector initialized with the value +=nil= that looks like this: + +#+begin_src clojure :exports both :results pp :wrap example + (inspect/fresh) +#+end_src + +#+RESULTS: +#+begin_example + {:path [], + :index [], + :pages-stack [], + :value nil, + :page-size 32, + :counter 0, + :rendered ("nil" (:newline)), + :stack [], + :indentation 0, + :current-page 0} +#+end_example + +The map contains the following keys: + +- =:path= is a vector that contains a symbolic path to the currently + inspected object. + +- =:index= is a vector that holds the inspect-able objects that were + rendered at the current stack level. These are the objects into + which we can drill down to. + +- =:pages-stack= is a vector of page numbers, which is used to + paginate collections. + +- =:value= is the currently inspected object. The empty inspector has + this value always set to =nil=. + +- =:page-size= controls how many elements should be rendered per page. + +- =:counter= is a number that gets incremented when a new object is + added to the =index=. + +- =:rendered= is a list of instructions on how to render the currently + inspected object on the client (e.g in the Cider inspector). Here + the list =("nil" (:newline))= instructs the client to render the + string =nil= followed by a newline. + +- =:stack= is the stack of inspected objects. When drilling down into + an object the new object will be pushed onto that stack. + +- =:indentation= is the number of spaces used for padding on the left + side and is used by some render functions. + +- =:current-page= is the current page number used when paginating + collections. + +*** Inspecting an object + +Let's inspect a more interesting object. To start the inspection we +can use the =inspect/start= function with the inspector and the object +we want to inspect as its arguments. This will modify the inspector +data structure in the following way: + +#+begin_src clojure :exports both :results pp :wrap example + (-> (inspect/fresh) + (inspect/start {:a {:b 1}})) +#+end_src + +#+RESULTS: +#+begin_example +{:path [], + :index [clojure.lang.PersistentArrayMap :a {:b 1}], + :pages-stack [], + :value {:a {:b 1}}, + :page-size 32, + :counter 3, + :rendered + ("Class" ": " (:value "clojure.lang.PersistentArrayMap" 0) + (:newline) + (:newline) + "--- Contents:" + (:newline) + " " (:value ":a" 1) " = " (:value "{ :b 1 }" 2) + (:newline)), + :stack [], + :indentation 0, + :current-page 0} +#+end_example + +The inspected object ={:a {:b 1}}= has been added to the =:value= +key. The objects, into which we can drill down to, have been added to +the =:index=, and the =:counter= has been increased by the sum of +those objects. + +Since we are inspecting a Clojure map, the inspector dispatched to a +render function that knows how to render a Clojure map. The inspector +has render functions for different kinds of objects, and what is added +to the =:index= depends on those render functions. + +The render function for Clojure maps has been implemented in a way +that shows the class of the map and the its keys and values. When +displayed on a client this will look like this: + +#+begin_example +Class: clojure.lang.PersistentArrayMap + +--- Contents: + :a = { :b 1 } +#+end_example + +The class of the map, their keys and their values are inspect-able +objects by themselves. The instructions under the =:rendered= key now +contain lists starting with a =:value= keyword, such as =(:value +"clojure.lang.PersistentArrayMap" 0)=. These lists represent objects, +into which can be drilled down to. The first element of those lists +tells the client that the element should be rendered as an +inspect-able object. The 2nd element is it's textual representation +and the 3rd element is the position of the object in the =:index=. + +*** Drilling into an object + +To drill down into an object we can use the =inspect/down= +function. After inspecting the object ={:a {:b 1}}= the =:index= is +set to =[clojure.lang.PersistentArrayMap :a {:b 1}]=. Passing =2= (the +position in the index) as the argument to the =inspect/down= function +means the next object that is going to be inspected is ={:b 1}=. + +#+begin_src clojure :exports both :results pp :wrap example + (-> (inspect/fresh) + (inspect/start {:a {:b 1}}) + (inspect/down 2)) +#+end_src + +#+RESULTS: +#+begin_example +{:path [:a], + :index [clojure.lang.PersistentArrayMap :b 1], + :pages-stack [0], + :value {:b 1}, + :page-size 32, + :counter 3, + :rendered + ("Class" ": " (:value "clojure.lang.PersistentArrayMap" 0) + (:newline) + (:newline) + "--- Contents:" + (:newline) + " " (:value ":b" 1) " = " (:value "1" 2) + (:newline) + (:newline) + "--- Path:" + (:newline) + " " ":a"), + :stack [{:a {:b 1}}], + :indentation 0, + :current-page 0} +#+end_example + +We can see that the inspected object ={:b 1}= has been added to the +=:value= key and got rendered under the =:rendered= key. The previous +object has been pushed onto the =:stack=, and =:path= has been updated +with the instructions that describe how to get from the original +object to the object we drilled down to. =:counter= is again set to +=3= because the inspector dispatched to the render function for maps, +which renders the class of the map and it's keys and values as +inspected-able objects. + +** Spec + +The following section describes how the Orchard Inspector renders +different kinds of objects. + +*** Class + +Classes are rendered with their name, the implemented interfaces, the +available constructors, their fields and methods. In Clojure versions +>= 1.10 an optional =Datafy= section is added. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print Boolean) +#+end_src + +#+RESULTS: +#+begin_example +Class: java.lang.Class + +--- Interfaces: + java.io.Serializable + java.lang.Comparable + +--- Constructors: + public java.lang.Boolean(boolean) + public java.lang.Boolean(java.lang.String) + +--- Fields: + public static final java.lang.Boolean java.lang.Boolean.FALSE + public static final java.lang.Boolean java.lang.Boolean.TRUE + public static final java.lang.Class java.lang.Boolean.TYPE + +--- Methods: + public boolean java.lang.Boolean.booleanValue() + public static int java.lang.Boolean.compare(boolean,boolean) + public int java.lang.Boolean.compareTo(java.lang.Boolean) + public int java.lang.Boolean.compareTo(java.lang.Object) + public boolean java.lang.Boolean.equals(java.lang.Object) + public static boolean java.lang.Boolean.getBoolean(java.lang.String) + public final native java.lang.Class java.lang.Object.getClass() + public int java.lang.Boolean.hashCode() + public static int java.lang.Boolean.hashCode(boolean) + public static boolean java.lang.Boolean.logicalAnd(boolean,boolean) + public static boolean java.lang.Boolean.logicalOr(boolean,boolean) + public static boolean java.lang.Boolean.logicalXor(boolean,boolean) + public final native void java.lang.Object.notify() + public final native void java.lang.Object.notifyAll() + public static boolean java.lang.Boolean.parseBoolean(java.lang.String) + public java.lang.String java.lang.Boolean.toString() + public static java.lang.String java.lang.Boolean.toString(boolean) + public static java.lang.Boolean java.lang.Boolean.valueOf(boolean) + public static java.lang.Boolean java.lang.Boolean.valueOf(java.lang.String) + public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException + public final void java.lang.Object.wait() throws java.lang.InterruptedException + public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException + +--- Datafy: + :bases = #{ java.lang.Object java.lang.Comparable java.io.Serializable } + :flags = #{ :public :final } + :members = { FALSE [ { :name FALSE, :type java.lang.Boolean, :declaring-class java.lang.Boolean, :flags #{ :public :static :final } } ], TRUE [ { :name TRUE, :type java.lang.Boolean, :declaring-class java.lang.Boolean, :flags #{ :public :static :final } } ], TYPE [ { :name TYPE, :type java.lang.Class, :declaring-class java.lang.Boolean, :flags #{ :public :static :final } } ], booleanValue [ { :name booleanValue, :return-type boolean, :declaring-class java.lang.Boolean, :parameter-types [], :exception-types [], ... } ], compare [ { :name compare, :return-type int, :declaring-class java.lang.Boolean, :parameter-types [ boolean boolean ], :exception-types [], ... } ], ... } + :name = java.lang.Boolean +#+end_example + +*** Datafiable + +Objects implementing the [[https://github.com/clojure/clojure/blob/master/src/clj/clojure/core/protocols.clj#L182][Datafiable]] protocol are rendered with an +optional =Datafy= section. The section shows the result of calling the +=datafy= function on the object, navigating 1 level into the children +using =nav= and calling =datafy= again on them. + +Since the [[https://github.com/clojure/clojure/blob/master/src/clj/clojure/core/protocols.clj#L182][Datafiable]] protocol is implemented for every object, this +section will only be rendered if the datafy-ed version of the object +is different than the original object. + +Minimum requirement for this feature is a Clojure version >= 1.10. + +#+begin_src clojure :exports both :results output :wrap example + (-> {:name "John Doe"} + (with-meta {'clojure.core.protocols/datafy + (fn [x] (assoc x :class (.getSimpleName (class x))))}) + (inspect/inspect-print)) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.PersistentArrayMap + +--- Meta Information: + clojure.core.protocols/datafy = user$eval11033$fn__11034@213a1033 + +--- Contents: + :name = "John Doe" + +--- Datafy: + :name = "John Doe" + :class = "PersistentArrayMap" +#+end_example + +*** Keyword + +Clojure keywords are rendered with their class name, their printed +value, and the instance and static fields. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print :abc/def) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.Keyword +Value: ":abc/def" + +--- Fields: + "_str" = ":abc/def" + "hasheq" = -1043781166 + "sym" = abc/def + +--- Static fields: + "rq" = java.lang.ref.ReferenceQueue@7849a4c3 + "table" = { returns java.lang.ref.WeakReference@1b9ad0c1, dialect java.lang.ref.WeakReference@542db2ec, existing java.lang.ref.WeakReference@17d434cf, clojure.core.logic/unify-with-pmap* java.lang.ref.WeakReference@4d5b7e61, patch java.lang.ref.WeakReference@7239d9d7, ... } +#+end_example + +*** Number + +Numbers keywords are rendered with their class name, their printed +value, and the instance and static fields. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print 1) +#+end_src + +#+RESULTS: +#+begin_example +Class: java.lang.Long +Value: "1" + +--- Fields: + "value" = 1 + +--- Static fields: + "BYTES" = 8 + "MAX_VALUE" = 9223372036854775807 + "MIN_VALUE" = -9223372036854775808 + "SIZE" = 64 + "TYPE" = long + "serialVersionUID" = 4290774380558885855 +#+end_example + +*** List + +Lists are rendered with their class name, the number of list items +and the paginated items in tabular form. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print (list :a :b)) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.PersistentList + +--- Contents: + 0. :a + 1. :b +#+end_example + +*** Map + +Maps are rendered with their class name, the number of map entries +and the map entries in tabular form. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print {:a 1 :b 2}) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.PersistentArrayMap + +--- Contents: + :a = 1 + :b = 2 +#+end_example + +*** Metadata + +Objects that have metadata attached to them are rendered with a =Meta +Information= section that shows the metadata in tabular form. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print (with-meta [1 2] {:a 1})) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.PersistentVector + +--- Meta Information: + :a = 1 + +--- Contents: + 0. 1 + 1. 2 +#+end_example + +*** Namespace + +Clojure namespaces are rendered with their class name, their +printed value, the total number of mappings, and sections for the +=Refer from=, =Interns= and =Imports= mappings. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print (find-ns 'clojure.string)) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.Namespace +Count: 786 + +--- Refer from: + clojure.core = [ #'clojure.core/primitives-classnames #'clojure.core/+' #'clojure.core/decimal? #'clojure.core/restart-agent #'clojure.core/sort-by ... ] + +--- Imports: + { Enum java.lang.Enum, InternalError java.lang.InternalError, NullPointerException java.lang.NullPointerException, InheritableThreadLocal java.lang.InheritableThreadLocal, Class java.lang.Class, ... } + +--- Interns: + { ends-with? #'clojure.string/ends-with?, replace-first-char #'clojure.string/replace-first-char, capitalize #'clojure.string/capitalize, reverse #'clojure.string/reverse, join #'clojure.string/join, ... } + +--- Datafy: + :name = clojure.string + :publics = { blank? #'clojure.string/blank?, capitalize #'clojure.string/capitalize, ends-with? #'clojure.string/ends-with?, escape #'clojure.string/escape, includes? #'clojure.string/includes?, ... } + :imports = { AbstractMethodError java.lang.AbstractMethodError, Appendable java.lang.Appendable, ArithmeticException java.lang.ArithmeticException, ArrayIndexOutOfBoundsException java.lang.ArrayIndexOutOfBoundsException, ArrayStoreException java.lang.ArrayStoreException, ... } + :interns = { blank? #'clojure.string/blank?, capitalize #'clojure.string/capitalize, ends-with? #'clojure.string/ends-with?, escape #'clojure.string/escape, includes? #'clojure.string/includes?, ... } +#+end_example + +*** Navigable + +Objects implementing the [[https://github.com/clojure/clojure/blob/master/src/clj/clojure/core/protocols.clj#L194][Navigable]] protocol are rendered with an +optional =Datafy= section. The ='clojure.core.protocols/nav= function +of the object will be used for navigation, instead of the default +implementation declared on object. + +Minimum requirement for this feature is a Clojure version >= 1.10. + +#+begin_src clojure :exports both :results output :wrap example + (-> {:name "John Doe"} + (with-meta {'clojure.core.protocols/nav + (fn [coll k v] [k (get coll k v)])}) + (inspect/inspect-print)) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.PersistentArrayMap + +--- Meta Information: + clojure.core.protocols/nav = user$eval11043$fn__11044@4c605d88 + +--- Contents: + :name = "John Doe" + +--- Datafy: + :name = [ :name "John Doe" ] +#+end_example + +Collections that contain elements that implement the [[https://github.com/clojure/clojure/blob/master/src/clj/clojure/core/protocols.clj#L182][Datafiable]] and +[[https://github.com/clojure/clojure/blob/master/src/clj/clojure/core/protocols.clj#L194][Navigable]] protocols will be rendered with an additional =Datafy= +section that shows the datafy-ed version of the elements. + +#+begin_src clojure :exports both :results output :wrap example + (->> (iterate inc 0) + (map #(hash-map :x %)) + (map #(with-meta % + {'clojure.core.protocols/datafy (fn [x] (assoc x :class (.getSimpleName (class x)))) + 'clojure.core.protocols/nav (fn [coll k v] [k (get coll k v)])})) + (take 5) + (inspect/inspect-print)) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.LazySeq + +--- Contents: + 0. { :x 0 } + 1. { :x 1 } + 2. { :x 2 } + 3. { :x 3 } + 4. { :x 4 } + +--- Datafy: + 0. { :class "PersistentHashMap", :x 0 } + 1. { :class "PersistentHashMap", :x 1 } + 2. { :class "PersistentHashMap", :x 2 } + 3. { :class "PersistentHashMap", :x 3 } + 4. { :class "PersistentHashMap", :x 4 } +#+end_example + +*** Pagination + +Collections that have more than 32 elements are paginated. A paginated +collection can be navigated with the =inspect/next-page= and +=inspect/prev-page= functions. The size of each page can be adjusted +with the =inspect/set-page-size= function. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print (iterate inc 0)) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.Iterate + +--- Contents: + 0. 0 + 1. 1 + 2. 2 + 3. 3 + 4. 4 + 5. 5 + 6. 6 + 7. 7 + 8. 8 + 9. 9 + 10. 10 + 11. 11 + 12. 12 + 13. 13 + 14. 14 + 15. 15 + 16. 16 + 17. 17 + 18. 18 + 19. 19 + 20. 20 + 21. 21 + 22. 22 + 23. 23 + 24. 24 + 25. 25 + 26. 26 + 27. 27 + 28. 28 + 29. 29 + 30. 30 + 31. 31 + ... + +--- Page Info: + Page size: 32, showing page: 1 of ? +#+end_example + +*** Ref + +Clojure references are rendered with their class name, their +containing object in the =Contains= section, and an optional =Datafy= +section. + +The object rendered under the =Contains= section is indented by 2 +spaces to better distinguish it from enclosing reference object. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print (atom {:a 1})) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.Atom + +--- Contains: + Class: clojure.lang.PersistentArrayMap + + --- Contents: + :a = 1 + +--- Datafy: + 0. { :a 1 } +#+end_example + +*** String + +Strings are rendered with their class name, their value, and the +printed version of the string in the =Printed Value= section. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print "Hello world") +#+end_src + +#+RESULTS: +#+begin_example +Class: java.lang.String +Value: "Hello world" + +--- Print: + Hello world +#+end_example + +*** Symbol + +Clojure symbols are rendered with their class name, their printed +value, and their fields. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print 'abc/def) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.Symbol +Value: "abc/def" + +--- Fields: + "_hasheq" = 0 + "_meta" = + "_str" = "abc/def" + "name" = "def" + "ns" = "abc" +#+end_example + +*** Var + +Clojure vars are rendered with their class name, their value (if +bound), metadata information and an optional =Datafy= section. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print #'*assert*) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.Var +Value: true + +--- Meta Information: + :ns = clojure.core + :name = *assert* + +--- Datafy: + 0. true +#+end_example +*** Vector + +Vectors are rendered with their class name, the number of items and +the paginated items in tabular form. + +#+begin_src clojure :exports both :results output :wrap example + (inspect/inspect-print [1 2 3]) +#+end_src + +#+RESULTS: +#+begin_example +Class: clojure.lang.PersistentVector + +--- Contents: + 0. 1 + 1. 2 + 2. 3 +#+end_example diff --git a/src/orchard/inspect.clj b/src/orchard/inspect.clj index cc63600f..9bdd4178 100644 --- a/src/orchard/inspect.clj +++ b/src/orchard/inspect.clj @@ -9,11 +9,21 @@ Pretty wild, right?" (:require - [clojure.string :as s]) + [clojure.string :as s] + [orchard.misc :as misc]) (:import - (java.lang.reflect Field Modifier) - (java.util List Map) - clojure.lang.Seqable)) + clojure.lang.Seqable + (java.lang.reflect Constructor Field Method Modifier) + (java.util List Map))) + +;; Datafy and Nav are only available since Clojure 1.10 +(require 'clojure.core.protocols) + +(def ^:private datafy + (misc/call-when-resolved 'clojure.core.protocols/datafy)) + +(def ^:private nav + (misc/call-when-resolved 'clojure.core.protocols/nav)) ;; ;; Navigating Inspector State @@ -67,7 +77,7 @@ [inspector] (merge (reset-index inspector) {:value nil, :stack [], :path [], :pages-stack [], - :current-page 0, :rendered '()})) + :current-page 0, :rendered '(), :indentation 0})) (defn fresh "Return an empty inspector." @@ -209,7 +219,7 @@ (map? value) :map-long (and (vector? value) (short? value)) :vector (vector? value) :vector-long - (and (seq? value) (not (counted? value))) :lazy-seq + (misc/lazy-seq? value) :lazy-seq (and (seq? value) (short? value)) :list (seq? value) :list-long (and (set? value) (short? value)) :set @@ -290,6 +300,29 @@ (defn render-ln [inspector & values] (render-onto inspector (concat values '((:newline))))) +(defn- indent [inspector] + (update inspector :indentation + 2)) + +(defn- unindent [inspector] + (update inspector :indentation - 2)) + +(defn- padding [{:keys [indentation]}] + (when (and (number? indentation) (pos? indentation)) + (apply str (repeat indentation " ")))) + +(defn- render-indent [inspector & values] + (let [padding (padding inspector)] + (cond-> inspector + padding + (render padding) + (seq values) + (render-onto values)))) + +(defn- render-section-header [inspector section] + (-> (render-ln inspector) + (render (format "%s--- %s:" (or (padding inspector) "") (name section))) + (render-ln))) + (defn render-value [inspector value] (let [{:keys [counter]} inspector expr `(:value ~(inspect-value value) ~counter)] @@ -300,18 +333,21 @@ (defn render-labeled-value [inspector label value] (-> inspector - (render label ": ") + (render-indent label ": ") (render-value value) (render-ln))) +(defn- render-class-name [inspector obj] + (render-labeled-value inspector "Class" (class obj))) + (defn render-map-values [inspector mappable] (reduce (fn [ins [key val]] (-> ins - (render " ") + (render-indent) (render-value key) (render " = ") (render-value val) - (render '(:newline)))) + (render-ln))) inspector mappable)) @@ -321,37 +357,62 @@ (loop [ins inspector, obj (seq obj), idx idx-starts-from] (if obj (recur (-> ins - (render " " (str idx) ". ") + (render-indent (str idx) ". ") (render-value (first obj)) - (render '(:newline))) + (render-ln)) (next obj) (inc idx)) ins)))) +(defn- last-page [{:keys [current-page page-size]} obj] + (if (or (instance? clojure.lang.Counted obj) + ;; if there are no more items after the current page, + ;; we must have reached the end of the collection, so + ;; it's not infinite. + (empty? (drop (* (inc current-page) page-size) obj))) + (quot (dec (count obj)) page-size) + ;; possibly infinite + Integer/MAX_VALUE)) + +(defn- render-page-info [{:keys [current-page page-size] :as inspector} obj] + (if-not (sequential? obj) + inspector + (let [last-page (last-page inspector obj) + paginate? (not= last-page 0)] + (if-not paginate? + inspector + (-> (render-section-header inspector "Page Info") + (indent) + (render-indent (format "Page size: %d, showing page: %d of %s" + page-size (inc current-page) + (if (= last-page Integer/MAX_VALUE) + "?" (inc last-page)))) + (unindent)))))) + +(defn- current-page [{:keys [current-page] :as inspector} obj] + (let [last-page (last-page inspector obj)] + ;; current-page might contain an incorrect value, fix that + (cond (< current-page 0) 0 + (> current-page last-page) last-page + :else current-page))) + +(defn- chunk-to-display [{:keys [page-size] :as inspector} obj] + (let [start-idx (* (current-page inspector obj) page-size)] + (->> obj (drop start-idx) (take page-size)))) + (defn render-collection-paged "Render a single page of either an indexed or associative collection." [inspector obj] - (let [{:keys [current-page page-size]} inspector - last-page (if (or (instance? clojure.lang.Counted obj) - ;; if there are no more items after the current page, - ;; we must have reached the end of the collection, so - ;; it's not infinite. - (empty? (drop (* (inc current-page) page-size) obj))) - (quot (dec (count obj)) page-size) - Integer/MAX_VALUE) ;; possibly infinite - ;; current-page might contain an incorrect value, fix that - current-page (cond (< current-page 0) 0 - (> current-page last-page) last-page - :else current-page) + (let [{:keys [page-size]} inspector + last-page (last-page inspector obj) + current-page (current-page inspector obj) start-idx (* current-page page-size) - chunk-to-display (->> obj - (drop start-idx) - (take page-size)) + chunk-to-display (chunk-to-display inspector obj) paginate? (not= last-page 0)] (as-> inspector ins (if (> current-page 0) (-> ins - (render " ...") - (render '(:newline))) + (render-indent "...") + (render-ln)) ins) (if (or (map? obj) (instance? Map obj)) @@ -359,26 +420,66 @@ (render-indexed-values ins chunk-to-display start-idx)) (if (< current-page last-page) - (render ins " ...") + (-> (render-indent ins "...") + (render-ln)) ins) (if paginate? - (-> ins - (render '(:newline)) - (render (format " Page size: %d, showing page: %d of %s" - page-size (inc current-page) - (if (= last-page Integer/MAX_VALUE) - "?" (inc last-page)))) - (assoc :current-page current-page)) + (assoc ins :current-page current-page) ins)))) (defn render-meta-information [inspector obj] (if (seq (meta obj)) (-> inspector - (render-ln "Meta Information: ") - (render-map-values (meta obj))) + (render-section-header "Meta Information") + (indent) + (render-map-values (meta obj)) + (unindent)) inspector)) +(defn- nav-datafy-tx [obj] + (comp (map (fn [[k v]] (some->> (nav obj k v) datafy (vector k)))) (remove nil?))) + +(defn- nav-datafy [obj] + (let [data (datafy obj)] + (cond (map? data) + (into {} (nav-datafy-tx obj) data) + (or (sequential? data) (set? data)) + (map datafy data)))) + +(defn- render-datafy? [inspector obj] + (cond (not misc/datafy?) + false + (map? obj) + (not= obj (nav-datafy obj)) + (or (sequential? obj) (set? obj)) + (not= (chunk-to-display inspector obj) + (map datafy (chunk-to-display inspector obj))) + :else (not= obj (datafy obj)))) + +(declare inspect) + +(defn- render-datafy-content [inspector obj] + (let [contents (nav-datafy obj)] + (cond (map? contents) + (render-collection-paged inspector contents) + (sequential? contents) + (render-collection-paged inspector contents) + :else (-> (indent inspector) + (inspect contents) + (unindent))))) + +(defn- render-datafy [inspector obj] + ;; Only render the datafy section if the datafyed version of the object is + ;; different than object, since we don't want to show the same data twice to + ;; the user. + (if-not (render-datafy? inspector obj) + inspector + (-> (render-section-header inspector "Datafy") + (indent) + (render-datafy-content obj) + (unindent)))) + ;; Inspector multimethod (defn known-types [_ins obj] (cond @@ -405,35 +506,50 @@ (render-ln "nil"))) (defmethod inspect :coll [inspector obj] - (-> inspector - (render-labeled-value "Class" (class obj)) + (-> (render-class-name inspector obj) (render-meta-information obj) - (render-ln "Contents: ") - (render-collection-paged obj))) + (render-section-header "Contents") + (indent) + (render-collection-paged obj) + (unindent) + (render-datafy obj))) (defmethod inspect :array [inspector obj] - (-> inspector - (render-labeled-value "Class" (class obj)) + (-> (render-class-name inspector obj) (render-labeled-value "Count" (java.lang.reflect.Array/getLength obj)) ; avoid reflection warning from Clojure compiler (render-labeled-value "Component Type" (.getComponentType (class obj))) - (render-ln "Contents: ") - (render-collection-paged obj))) + (render-section-header "Contents") + (indent) + (render-collection-paged obj) + (unindent) + (render-datafy obj))) + +(defn- render-var-value [inspector ^clojure.lang.Var obj] + (if-not (.isBound obj) + inspector + (-> (render-indent inspector "Value: ") + (render-value (var-get obj)) + (render-ln)))) (defmethod inspect :var [inspector ^clojure.lang.Var obj] - (let [header-added - (-> inspector - (render-labeled-value "Class" (class obj)) - (render-meta-information obj))] - (if (.isBound obj) - (-> header-added - (render "Value: ") - (render-value (var-get obj))) - header-added))) + (-> (render-class-name inspector obj) + (render-var-value obj) + (render-meta-information obj) + (render-datafy obj))) + +(defn- render-indent-str-lines [inspector s] + (reduce #(-> (render-indent %1 (str %2)) + (render-ln)) + inspector (s/split-lines s))) (defmethod inspect :string [inspector ^java.lang.String obj] - (-> inspector - (render-labeled-value "Class" (class obj)) - (render "Value: " (pr-str obj)))) + (-> (render-class-name inspector obj) + (render "Value: " (pr-str obj)) + (render-ln) + (render-section-header "Print") + (indent) + (render-indent-str-lines obj) + (unindent))) (defmethod inspect :default [inspector obj] (let [class-chain (loop [c (class obj), res ()] @@ -466,59 +582,94 @@ (render-fields [inspector section-name fields] (if (seq fields) (-> inspector - (render-ln section-name) + (render-section-header section-name) + (indent) (render-map-values (->> fields (map (fn [f] [(field-name f) (field-val f)])) (into (sorted-map)))) - (render-ln)) + (unindent)) inspector))] (-> inspector (render-labeled-value "Class" (class obj)) (render-labeled-value "Value" (pr-str obj)) - (render-ln "---") - (render-fields "Fields:" non-static) - (render-fields "Static fields:" static))))) + (render-fields "Fields" non-static) + (render-fields "Static fields" static) + (render-datafy obj))))) -(defn- render-section [obj inspector section] +(defn- render-section [obj inspector [section sort-key-fn]] (let [method (symbol (str ".get" (name section))) elements (eval (list method obj))] - (if-not elements + (if-not (seq elements) inspector - (reduce (fn [ins elt] - (-> ins - (render " ") - (render-value elt) - (render-ln))) - (-> inspector - (render-ln) - (render-ln "--- " (name section) ": ")) - elements)))) + (unindent (reduce (fn [ins elt] + (-> ins + (render-indent) + (render-value elt) + (render-ln))) + (-> inspector + (render-section-header section) + (indent)) + (sort-by sort-key-fn elements)))))) (defmethod inspect :class [inspector ^Class obj] - (reduce (partial render-section obj) - (render-labeled-value inspector "Class" (class obj)) - [:Interfaces :Constructors :Fields :Methods])) + (-> (reduce (partial render-section obj) + (render-class-name inspector obj) + [[:Interfaces #(.getName ^Class %)] + [:Constructors #(.toGenericString ^Constructor %)] + [:Fields #(.getName ^Field %)] + [:Methods #(vector (.getName ^Method %) (.toGenericString ^Method %))]]) + (render-datafy obj))) (defmethod inspect :aref [inspector ^clojure.lang.ARef obj] - (-> inspector - (render-labeled-value "Class" (class obj)) - (render-ln "Contains:") - (render-ln) - (inspect (deref obj)))) + (-> (render-class-name inspector obj) + (render-section-header "Contains") + (indent) + (inspect (deref obj)) + (unindent) + (render-datafy obj))) (defn ns-refers-by-ns [^clojure.lang.Namespace ns] (group-by (fn [^clojure.lang.Var v] (.ns v)) (map val (ns-refers ns)))) +(defn- render-ns-refers [inspector obj] + (let [refers (ns-refers-by-ns obj)] + (if-not (seq refers) + inspector + (-> (render-section-header inspector "Refer from") + (indent) + (render-map-values refers) + (unindent))))) + +(defn- render-ns-imports [inspector obj] + (let [imports (ns-imports obj)] + (if-not (seq imports) + inspector + (-> (render-section-header inspector "Imports") + (indent) + (render-indent) + (render-value imports) + (unindent) + (render-ln))))) + +(defn- render-ns-interns [inspector obj] + (let [interns (ns-interns obj)] + (if-not (seq interns) + inspector + (-> (render-section-header inspector "Interns") + (indent) + (render-indent) + (render-value interns) + (unindent) + (render-ln))))) + (defmethod inspect :namespace [inspector ^clojure.lang.Namespace obj] - (-> inspector - (render-labeled-value "Class" (class obj)) + (-> (render-class-name inspector obj) (render-labeled-value "Count" (count (ns-map obj))) - (render-ln "---") - (render-ln "Refer from: ") - (render-map-values (ns-refers-by-ns obj)) - (render-labeled-value "Imports" (ns-imports obj)) - (render-labeled-value "Interns" (ns-interns obj)))) + (render-ns-refers obj) + (render-ns-imports obj) + (render-ns-interns obj) + (render-datafy obj))) ;; ;; Entry point to inspect a value and get the serialized rep @@ -535,10 +686,10 @@ (defn render-path [inspector] (let [path (:path inspector)] (if (and (seq path) (not-any? #(= % ') path)) - (-> inspector - (render '(:newline)) - (render (str " Path: " - (s/join " " (:path inspector))))) + (-> (render-section-header inspector "Path") + (indent) + (render-indent (s/join " " (:path inspector))) + (unindent)) inspector))) (defn inspect-render @@ -551,6 +702,7 @@ (assoc :value value) (render-reference) (inspect value) + (render-page-info value) (render-path))))) ;; Get a human readable printout of rendered sequence diff --git a/src/orchard/misc.clj b/src/orchard/misc.clj index deed9cc6..0d2ba324 100644 --- a/src/orchard/misc.clj +++ b/src/orchard/misc.clj @@ -6,6 +6,8 @@ [clojure.string :as str] [orchard.util.io :as util.io])) +(require 'clojure.core.protocols) + (defn os-windows? [] (.startsWith (System/getProperty "os.name") "Windows")) @@ -157,3 +159,29 @@ (require ns) (catch Exception _ nil))) (some-> sym find-var var-get))) + +(def datafy? + "True if Datafy and Nav (added in Clojure 1.10) are supported, + otherwise false." + (some? (resolve 'clojure.core.protocols/datafy))) + +(defn call-when-resolved + "Return a fn that calls the fn resolved through `var-sym` with it's + own arguments. `var-sym` will be resolved once. If `var-sym` can't + be resolved the function always returns nil." + [var-sym] + (let [resolved-var (resolve var-sym)] + (fn [& args] + (when resolved-var + (apply resolved-var args))))) + +(defn lazy-seq? + "Return true if `x` is a lazy seq, otherwise false." + [x] + (and (seq? x) (not (counted? x)))) + +(defn safe-count + "Call `clojure.core/count` on `x` if it is a collection, but not a lazy seq." + [x] + (when (and (coll? x) (not (lazy-seq? x))) + (count x))) diff --git a/test/orchard/inspect_test.clj b/test/orchard/inspect_test.clj index 30e889d3..688967be 100644 --- a/test/orchard/inspect_test.clj +++ b/test/orchard/inspect_test.clj @@ -1,35 +1,160 @@ (ns orchard.inspect-test (:require - [clojure.test :refer [deftest is are testing]] - [orchard.inspect :as inspect])) - -(def nil-result ["(\"nil\" (:newline))"]) - -(def var-result ["(\"Class\" \": \" (:value \"clojure.lang.Var\" 0) (:newline) \"Meta Information: \" (:newline) \" \" (:value \":ns\" 1) \" = \" (:value \"clojure.core\" 2) (:newline) \" \" (:value \":name\" 3) \" = \" (:value \"*assert*\" 4) (:newline) \"Value: \" (:value \"true\" 5))"]) + [clojure.data :as data] + [clojure.walk :as walk] + [clojure.edn :as edn] + [clojure.java.io :as io] + [clojure.java.shell :as shell] + [clojure.pprint :as pprint] + [clojure.string :as str] + [clojure.test :as t :refer [deftest is are testing]] + [orchard.inspect :as inspect] + [orchard.misc :refer [datafy? java-api-version]]) + (:import java.io.File)) + +(defn- demunge-str [s] + (str/replace s #"(?i)\$([a-z-]+)__([0-9]+)(@[a-f0-9]+)?" "\\$$1")) + +(defn- demunge + ([rendered] + (demunge rendered demunge-str)) + ([rendered demunge-fn] + (walk/prewalk (fn [form] + (if (string? form) + (demunge-fn form) + form)) + rendered))) + +(defn- render-plain [x] + (cond (and (seq? x) (keyword? (first x))) + (let [[type value & args] x] + (case type + :newline "\n" + :value (format "%s <%s>" value (str/join "," args)))) + (seq? x) + (str/join "" (map render-plain x)) + :else (str x))) + +(defn- diff-text [expected actual] + (when (zero? (:exit (shell/sh "git" "--version"))) + (let [actual-file (File/createTempFile "actual" ".txt") + expected-file (File/createTempFile "expected" ".txt")] + (spit actual-file (render-plain actual)) + (spit expected-file (render-plain expected)) + (try (let [{:keys [exit out err] :as result} + (shell/sh "git" "diff" + (if (= "dumb" (System/getenv "TERM")) "--no-color" "--color") + "--minimal" + "--no-index" + (str actual-file) (str expected-file))] + (case exit + (0 1) out + (ex-info "Failed to call diff" result))) + (finally + (io/delete-file actual-file) + (io/delete-file expected-file)))))) + +(defn- test-message [msg expected actual] + (let [expected-text (render-plain expected) + actual-text (render-plain actual)] + (with-out-str + (println (format "Inspect test failed" (when msg (str ": " msg)))) + (let [diff (diff-text expected actual)] + (when-not (str/blank? diff) + (println) + (println "=== Text Diff ===\n") + (println diff))) + (let [[only-in-expected only-in-actual both] (data/diff expected actual)] + (when (seq only-in-expected) + (println) + (println "=== Expected data diff ===\n") + (pprint/pprint only-in-expected)) + (when (seq only-in-actual) + (println) + (println "=== Actual data diff ===\n") + (pprint/pprint only-in-actual))) + (when-not (= expected-text actual-text) + (when expected + (println) + (println "=== Expected text ===\n") + (println expected-text)) + (when actual + (println) + (println "=== Actual text ===\n") + (println actual-text) + (println)))))) + +(defmethod t/assert-expr 'match? [msg form] + `(let [expected# ~(nth form 1) + actual# ~(nth form 2) + result# (= expected# actual#)] + (t/do-report + {:type (if result# :pass :fail) + :message (test-message ~msg expected# actual#) + :expected expected# + :actual actual#}) + result#)) + +(def nil-result + '("nil" (:newline))) (def code "(sorted-map :a {:b 1} :c \"a\" :d 'e :f [2 3])") (def eval-result (eval (read-string code))) -(def inspect-result ["(\"Class\" \": \" (:value \"clojure.lang.PersistentTreeMap\" 0) (:newline) \"Contents: \" (:newline) \" \" (:value \":a\" 1) \" = \" (:value \"{ :b 1 }\" 2) (:newline) \" \" (:value \":c\" 3) \" = \" (:value \"\\\"a\\\"\" 4) (:newline) \" \" (:value \":d\" 5) \" = \" (:value \"e\" 6) (:newline) \" \" (:value \":f\" 7) \" = \" (:value \"[ 2 3 ]\" 8) (:newline))"]) +(def inspect-result + '("Class" + ": " + (:value "clojure.lang.PersistentTreeMap" 0) + (:newline) + (:newline) + "--- Contents:" + (:newline) + " " (:value ":a" 1) " = " (:value "{ :b 1 }" 2) + (:newline) + " " (:value ":c" 3) " = " (:value "\"a\"" 4) + (:newline) + " " (:value ":d" 5) " = " (:value "e" 6) + (:newline) + " " (:value ":f" 7) " = " (:value "[ 2 3 ]" 8) + (:newline))) + +(-> (inspect/fresh) + (inspect/start {:a {:b 1}})) + +(-> (inspect/fresh) + (inspect/start {:a {:b 1}}) + (inspect/down 1) + (inspect/down 1)) -(def push-result ["(\"Class\" \": \" (:value \"clojure.lang.PersistentArrayMap\" 0) (:newline) \"Contents: \" (:newline) \" \" (:value \":b\" 1) \" = \" (:value \"1\" 2) (:newline) (:newline) \" Path: :a\")"]) +(def long-sequence (range 70)) +(def long-vector (vec (range 70))) +(def long-map (zipmap (range 70) (range 70))) +(def long-nested-coll (vec (map #(range (* % 10) (+ (* % 10) 80)) (range 200)))) +(def truncated-string (str "\"" (apply str (repeat 146 "a")) "...")) -(def inspect-result-with-nil ["(\"Class\" \": \" (:value \"clojure.lang.PersistentVector\" 0) (:newline) \"Contents: \" (:newline) \" \" \"0\" \". \" (:value \"1\" 1) (:newline) \" \" \"1\" \". \" (:value \"2\" 2) (:newline) \" \" \"2\" \". \" (:value \"nil\" 3) (:newline) \" \" \"3\" \". \" (:value \"3\" 4) (:newline))"]) +(defn- section? [name rendered] + (when (string? rendered) + (re-matches (re-pattern (format "--- %s:" name)) rendered))) -(def inspect-result-configure-length ["(\"Class\" \": \" (:value \"clojure.lang.PersistentVector\" 0) (:newline) \"Contents: \" (:newline) \" \" \"0\" \". \" (:value \"[ 1... 2222 333 ... ]\" 1) (:newline))"]) +(defn- section [name rendered] + (->> rendered + (drop-while #(not (section? name %))) + (take-while #(or (section? name %) + (not (section? ".*" %)))))) -(def eval-and-inspect-result ["(\"Class\" \": \" (:value \"java.lang.String\" 0) (:newline) \"Value: \" \"\\\"1001\\\"\")"]) +(defn- datafy-section [rendered] + (section "Datafy" rendered)) -(def java-hashmap-inspect-result ["(\"Class\" \": \" (:value \"java.util.HashMap\" 0) (:newline) \"Contents: \" (:newline) \" \" (:value \":b\" 1) \" = \" (:value \"2\" 2) (:newline) \" \" (:value \":c\" 3) \" = \" (:value \"3\" 4) (:newline) \" \" (:value \":a\" 5) \" = \" (:value \"1\" 6) (:newline))"]) +(defn- header [rendered] + (take-while #(not (and (string? %) + (re-matches #".*---.*" %))) rendered)) -(def tagged-literal-inspect-result ["(\"Class\" \": \" (:value \"clojure.lang.TaggedLiteral\" 0) (:newline) \"Value\" \": \" (:value \"\\\"#foo ()\\\"\" 1) (:newline) \"---\" (:newline) \"Fields:\" (:newline) \" \" (:value \"\\\"form\\\"\" 2) \" = \" (:value \"()\" 3) (:newline) \" \" (:value \"\\\"tag\\\"\" 4) \" = \" (:value \"foo\" 5) (:newline) (:newline) \"Static fields:\" (:newline) \" \" (:value \"\\\"FORM_KW\\\"\" 6) \" = \" (:value \":form\" 7) (:newline) \" \" (:value \"\\\"TAG_KW\\\"\" 8) \" = \" (:value \":tag\" 9) (:newline) (:newline))"]) +(defn- extend-datafy-class [m] + (vary-meta m assoc 'clojure.core.protocols/datafy (fn [x] (assoc x :class (.getSimpleName (class x)))))) -(def long-sequence (range 70)) -(def long-vector (vec (range 70))) -(def long-map (zipmap (range 70) (range 70))) -(def long-nested-coll (vec (map #(range (* % 10) (+ (* % 10) 80)) (range 200)))) -(def truncated-string (str "\"" (apply str (repeat 146 "a")) "...")) +(defn- extend-nav-vector [m] + (vary-meta m assoc 'clojure.core.protocols/nav (fn [coll k v] [k (get coll k v)]))) (defn inspect [value] @@ -37,75 +162,107 @@ (defn render [inspector] - (vector (pr-str (:rendered inspector)))) + (:rendered inspector)) (deftest nil-test (testing "nil renders correctly" - (is (= nil-result - (-> nil - inspect - render))))) + (is (match? nil-result + (-> nil + inspect + render))))) (deftest pop-empty-test (testing "popping an empty inspector renders nil" - (is (= nil-result - (-> (inspect/fresh) - inspect/up - render))))) + (is (match? nil-result + (-> (inspect/fresh) + inspect/up + render))))) (deftest pop-empty-idempotent-test (testing "popping an empty inspector is idempotent" - (is (= nil-result - (-> (inspect/fresh) - inspect/up - inspect/up - render))))) + (is (match? nil-result + (-> (inspect/fresh) + inspect/up + inspect/up + render))))) (deftest push-empty-test (testing "pushing an empty inspector index renders nil" - (is (= nil-result - (-> (inspect/fresh) - (inspect/down 1) - render))))) + (is (match? nil-result + (-> (inspect/fresh) + (inspect/down 1) + render))))) (deftest push-empty-idempotent-test (testing "pushing an empty inspector index is idempotent" - (is (= nil-result - (-> (inspect/fresh) - (inspect/down 1) - (inspect/down 1) - render))))) + (is (match? nil-result + (-> (inspect/fresh) + (inspect/down 1) + (inspect/down 1) + render))))) (deftest inspect-var-test - (testing "rendering a var" - (is (= var-result - (-> #'*assert* - inspect - render))))) + (testing "inspecting a var" + (let [rendered (-> #'*assert* inspect render)] + (testing "renders the header" + (is (match? '("Class" + ": " + (:value "clojure.lang.Var" 0) + (:newline) + "Value: " + (:value "true" 1) + (:newline) + (:newline)) + (header rendered)))) + (testing "renders the meta information section" + (is (match? (cond-> '("--- Meta Information:" + (:newline) + " " (:value ":ns" 2) " = " (:value "clojure.core" 3) + (:newline) + " " (:value ":name" 4) " = " (:value "*assert*" 5) + (:newline)) + datafy? (concat ['(:newline)])) + (section "Meta Information" rendered)))) + (when datafy? + (testing "renders the datafy section" + (is (match? '("--- Datafy:" + (:newline) + " " "0" ". " (:value "true" 6) + (:newline)) + (datafy-section rendered)))))))) (deftest inspect-expr-test (testing "rendering an expr" - (is (= inspect-result - (-> eval-result - inspect - render))))) + (is (match? inspect-result + (-> eval-result + inspect + render))))) (deftest push-test (testing "pushing a rendered expr inspector idx" - (is (= push-result - (-> eval-result - inspect - (inspect/down 2) - render))))) + (is (match? '("Class" + ": " (:value "clojure.lang.PersistentArrayMap" 0) + (:newline) + (:newline) + "--- Contents:" + (:newline) + " " (:value ":b" 1) " = " (:value "1" 2) + (:newline) + (:newline) + "--- Path:" + (:newline) + " " + ":a") + (-> eval-result inspect (inspect/down 2) render))))) (deftest pop-test (testing "popping a rendered expr inspector" - (is (= inspect-result - (-> eval-result - inspect - (inspect/down 2) - inspect/up - render))))) + (is (match? inspect-result + (-> eval-result + inspect + (inspect/down 2) + inspect/up + render))))) (deftest pagination-test (testing "big collections are paginated" @@ -120,7 +277,7 @@ inspect :rendered ^String (last) - (.startsWith " Page size:")))) + (.startsWith "Page size:")))) (testing "small collections are not paginated" (is (= '(:newline) (-> (range 10) @@ -138,7 +295,7 @@ :rendered last)))) (testing "uncounted collections have their size determined on the last page" - (is (= " Page size: 32, showing page: 2 of 2" + (is (= "Page size: 32, showing page: 2 of 2" (-> (range 50) inspect inspect/next-page @@ -186,13 +343,22 @@ (deftest eval-and-inspect-test (testing "evaluate expr in the context of currently inspected value" - (is (= eval-and-inspect-result - (-> eval-result - inspect - (inspect/down 2) - (inspect/down 2) - (inspect/eval-and-inspect "(str (+ v 1000))") - render))))) + (is (match? '("Class" + ": " (:value "java.lang.String" 0) + (:newline) + "Value: " "\"1001\"" + (:newline) + (:newline) + "--- Print:" + (:newline) + " " "1001" + (:newline)) + (-> eval-result + inspect + (inspect/down 2) + (inspect/down 2) + (inspect/eval-and-inspect "(str (+ v 1000))") + render))))) (deftest def-value-test (testing "define var with the currently inspected value" @@ -205,13 +371,13 @@ (deftest path-test (testing "inspector tracks the path in the data structure" - (is (-> long-map inspect (inspect/down 39) render ^String (first) (.endsWith "\" Path: (find 50) key\")"))) - (is (-> long-map inspect (inspect/down 40) render ^String (first) (.endsWith "\" Path: (get 50)\")"))) - (is (-> long-map inspect (inspect/down 40) (inspect/down 0) render ^String (first) (.endsWith "\" Path: (get 50) class\")")))) + (is (= "(find 50) key" (-> long-map inspect (inspect/down 39) render last))) + (is (= "(get 50)" (-> long-map inspect (inspect/down 40) render last))) + (is (= "(get 50) class" (-> long-map inspect (inspect/down 40) (inspect/down 0) render last)))) (testing "doesn't show path if unknown navigation has happened" - (is (-> long-map inspect (inspect/down 40) (inspect/down 0) (inspect/down 1) render ^String (first) (.endsWith "(:newline))")))) + (is (= '(:newline) (-> long-map inspect (inspect/down 40) (inspect/down 0) (inspect/down 1) render last)))) (testing "doesn't show the path in the top level" - (is (-> [1 2 3] inspect render ^String (first) (.endsWith "(:newline))"))))) + (is (= '(:newline) (-> [1 2 3] inspect render last))))) (defprotocol IMyTestType (^String get-name [this])) @@ -225,7 +391,7 @@ (deftest inspect-val-test (testing "inspect-value print types" - (are [result form] (= result (inspect/inspect-value form)) + (are [result form] (match? result (inspect/inspect-value form)) "1" 1 "\"2\"" "2" truncated-string (apply str (repeat 300 \a)) @@ -251,7 +417,7 @@ (testing "inspect-value adjust length and size" (binding [inspect/*max-atom-length* 6 inspect/*max-coll-size* 2] - (are [result form] (= result (inspect/inspect-value form)) + (are [result form] (match? result (inspect/inspect-value form)) "1" 1 "nil" nil "\"2\"" "2" @@ -266,36 +432,146 @@ "{ :a { ( 0 1 ... ) \"ab..., 2 3, ... } }" {:a {(range 10) "abcdefg", 2 3, 4 5, 6 7, 8 9, 10 11}} "java.lang.Long[] { 0, 1, ... }" (into-array Long (range 10)))) (binding [inspect/*max-coll-size* 6] - (are [result form] (= result (inspect/inspect-value form)) + (are [result form] (match? result (inspect/inspect-value form)) "[ ( 1 1 1 1 1 1 ... ) ]" [(repeat 1)] "{ :a { ( 0 1 2 3 4 5 ... ) 1, 2 3, 4 5, 6 7, 8 9, 10 11 } }" {:a {(range 10) 1, 2 3, 4 5, 6 7, 8 9, 10 11}})))) +(deftest inspect-class-fields-test + (testing "inspecting a class with fields renders correctly" + (is (match? (case java-api-version + (8 11) + '("--- Fields:" + (:newline) + " " (:value "public static final java.lang.Boolean java.lang.Boolean.FALSE" 5) + (:newline) + " " (:value "public static final java.lang.Boolean java.lang.Boolean.TRUE" 6) + (:newline) + " " (:value "public static final java.lang.Class java.lang.Boolean.TYPE" 7) + (:newline) + (:newline)) + '("--- Fields:" + (:newline) + " " (:value "public static final java.lang.Boolean java.lang.Boolean.FALSE" 6) + (:newline) + " " (:value "public static final java.lang.Boolean java.lang.Boolean.TRUE" 7) + (:newline) + " " (:value "public static final java.lang.Class java.lang.Boolean.TYPE" 8) + (:newline) + (:newline))) + (->> Boolean inspect render (section "Fields"))))) + (testing "inspecting a class without fields renders correctly" + (is (-> Object inspect render (section "Fields") empty?)))) + (deftest inspect-coll-test (testing "inspect :coll prints contents of the coll" - (is (= inspect-result-with-nil - (render (inspect/start (inspect/fresh) [1 2 nil 3])))))) + (is (match? '("Class" + ": " (:value "clojure.lang.PersistentVector" 0) + (:newline) + (:newline) + "--- Contents:" + (:newline) + " " "0" ". " (:value "1" 1) + (:newline) + " " "1" ". " (:value "2" 2) + (:newline) + " " "2" ". " (:value "nil" 3) + (:newline) + " " "3" ". " (:value "3" 4) + (:newline)) + (render (inspect/start (inspect/fresh) [1 2 nil 3])))))) + +(deftest inspect-coll-nav-test + (testing "inspecting a collection extended with the Datafiable and Navigable protocols" + (let [rendered (-> (->> (iterate inc 0) + (map #(hash-map :x %)) + (map extend-datafy-class) + (map extend-nav-vector)) + inspect (inspect/set-page-size 2) render)] + (testing "renders the content section" + (is (match? '("--- Contents:" + (:newline) + " " "0" ". " (:value "{ :x 0 }" 1) + (:newline) + " " "1" ". " (:value "{ :x 1 }" 2) + (:newline) + " " "..." + (:newline) + (:newline)) + (section "Contents" rendered)))) + (when datafy? + (testing "renders the datafy section" + (is (match? '("--- Datafy:" + (:newline) + " " "0" ". " (:value "{ :class \"PersistentHashMap\", :x 0 }" 3) + (:newline) + " " "1" ". " (:value "{ :class \"PersistentHashMap\", :x 1 }" 4) + (:newline) + " " "..." + (:newline) + (:newline)) + (datafy-section rendered))))) + (testing "renders the page info section" + (is (match? '("--- Page Info:" + (:newline) + " " "Page size: 2, showing page: 1 of ?") + (section "Page Info" rendered))))))) (deftest inspect-configure-length-test (testing "inspect respects :max-atom-length and :max-coll-size configuration" - (is (= inspect-result-configure-length - (render (-> (inspect/fresh) - (assoc :max-atom-length 4 - :max-coll-size 3) - (inspect/start [[111111 2222 333 44 5]]))))))) + (is (match? '("Class" + ": " + (:value "clojure.lang.PersistentVector" 0) + (:newline) + (:newline) + "--- Contents:" + (:newline) + " " "0" ". " (:value "[ 1... 2222 333 ... ]" 1) + (:newline)) + (render (-> (inspect/fresh) + (assoc :max-atom-length 4 + :max-coll-size 3) + (inspect/start [[111111 2222 333 44 5]]))))))) (deftest inspect-java-hashmap-test (testing "inspecting java.util.Map descendendants prints a key-value coll" (let [^java.util.Map the-map {:a 1, :b 2, :c 3}] - (is (= java-hashmap-inspect-result - (-> (inspect/fresh) - (inspect/start (java.util.HashMap. the-map)) - render)))))) + (is (match? '("Class" + ": " + (:value "java.util.HashMap" 0) + (:newline) + (:newline) + "--- Contents:" + (:newline) + " " (:value ":b" 1) " = " (:value "2" 2) + (:newline) + " " (:value ":c" 3) " = " (:value "3" 4) + (:newline) + " " (:value ":a" 5) " = " (:value "1" 6) + (:newline)) + (-> (inspect/fresh) + (inspect/start (java.util.HashMap. the-map)) + render)))))) (deftest inspect-java-object-test (testing "inspecting any Java object prints its fields" - (is (= tagged-literal-inspect-result - (render (inspect/start (inspect/fresh) - (clojure.lang.TaggedLiteral/create 'foo ()))))))) + (is (match? '("Class" + ": " + (:value "clojure.lang.TaggedLiteral" 0) + (:newline) + "Value" ": " (:value "\"#foo ()\"" 1) + (:newline) + (:newline) + "--- Fields:" + (:newline) " " (:value "\"form\"" 2) " = " (:value "()" 3) + (:newline) " " (:value "\"tag\"" 4) " = " (:value "foo" 5) + (:newline) + (:newline) + "--- Static fields:" + (:newline) " " (:value "\"FORM_KW\"" 6) " = " (:value ":form" 7) + (:newline) " " (:value "\"TAG_KW\"" 8) " = " (:value ":tag" 9) + (:newline)) + (render (inspect/start (inspect/fresh) + (clojure.lang.TaggedLiteral/create 'foo ()))))))) (deftest inspect-path (testing "inspector keeps track of the path in the inspected structure" @@ -319,3 +595,302 @@ (:path (-> inspector (inspect/down 0))))) (is (= '[:a (nth 2) :b :c (nth 73) (find :foo) key class ] (:path (-> inspector (inspect/down 0) (inspect/down 1)))))))) + +(deftest inspect-object-class-test + (testing "inspecting the java.lang.Object class" + (let [rendered (-> Object inspect render)] + (testing "renders the header section" + (is (match? '("Class" ": " (:value "java.lang.Class" 0) (:newline) (:newline)) + (header rendered)))) + (testing "renders the constructors section" + (is (match? '("--- Constructors:" + (:newline) + " " (:value "public java.lang.Object()" 1) + (:newline) + (:newline)) + (section "Constructors" rendered)))) + (testing "renders the methods section" + (is (match? (cond-> '("--- Methods:" + (:newline) + " " (:value "public boolean java.lang.Object.equals(java.lang.Object)" 2) + (:newline) + " " (:value "public final native java.lang.Class java.lang.Object.getClass()" 3) + (:newline) + " " (:value "public native int java.lang.Object.hashCode()" 4) + (:newline) + " " (:value "public final native void java.lang.Object.notify()" 5) + (:newline) + " " (:value "public final native void java.lang.Object.notifyAll()" 6) + (:newline) + " " (:value "public java.lang.String java.lang.Object.toString()" 7) + (:newline) + " " (:value "public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException" 8) + (:newline) + " " (:value "public final void java.lang.Object.wait() throws java.lang.InterruptedException" 9) + (:newline) + " " (:value "public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException" 10) + (:newline)) + datafy? (concat ['(:newline)])) + (section "Methods" rendered)))) + (when datafy? + (testing "renders the datafy section" + (is (match? `("--- Datafy:" + (:newline) + " " (:value ":flags" 11) " = " (:value "#{ :public }" 12) + (:newline) + " " (:value ":members" 13) " = " + (:value ~(str "{ clone [ { :name clone, :return-type java.lang.Object, :declaring-class java.lang.Object, " + ":parameter-types [], :exception-types [ java.lang.CloneNotSupportedException ], ... } ], equals " + "[ { :name equals, :return-type boolean, :declaring-class java.lang.Object, :parameter-types " + "[ java.lang.Object ], :exception-types [], ... } ], finalize [ { :name finalize, :return-type void, " + ":declaring-class java.lang.Object, :parameter-types [], :exception-types [ java.lang.Throwable ], " + "... } ], getClass [ { :name getClass, :return-type java.lang.Class, :declaring-class java.lang.Object, " + ":parameter-types [], :exception-types [], ... } ], hashCode [ { :name hashCode, :return-type int, " + ":declaring-class java.lang.Object, :parameter-types [], :exception-types [], ... } ], ... }") 14) + (:newline) + " " (:value ":name" 15) " = " (:value "java.lang.Object" 16) + (:newline)) + (datafy-section rendered)))))))) + +(deftest inspect-atom-test + (testing "inspecting an atom" + (let [rendered (-> (atom {:a 1}) inspect render)] + (testing "renders the header section" + (is (match? '("Class" + ": " + (:value "clojure.lang.Atom" 0) + (:newline) + (:newline)) + (header rendered)))) + (testing "renders the contains section" + (is (match? (cond-> '("--- Contains:" + (:newline) + " " "Class" ": " (:value "clojure.lang.PersistentArrayMap" 1) + (:newline) + (:newline) + " --- Contents:" + (:newline) + " " (:value ":a" 2) " = " (:value "1" 3) + (:newline)) + datafy? (concat ['(:newline)])) + (section "Contains" rendered)))) + (when datafy? + (testing "renders the datafy section" + (is (match? '("--- Datafy:" + (:newline) + " " "0" ". " (:value "{ :a 1 }" 4) + (:newline)) + (datafy-section rendered)))))))) + +(deftest inspect-atom-infinite-seq-test + (testing "inspecting an atom holding an infinite seq" + (let [rendered (-> (atom (repeat 1)) inspect (inspect/set-page-size 3) render)] + (testing "renders the header section" + (is (match? '("Class" + ": " + (:value "clojure.lang.Atom" 0) + (:newline) + (:newline)) + (header rendered)))) + (testing "renders the contains section" + (is (match? (cond-> '("--- Contains:" + (:newline) + " " "Class" ": " (:value "clojure.lang.Repeat" 1) + (:newline) + (:newline) + " --- Contents:" + (:newline) + " " "0" ". " (:value "1" 2) + (:newline) + " " "1" ". " (:value "1" 3) + (:newline) + " " "2" ". " (:value "1" 4) + (:newline) + " " "..." + (:newline)) + datafy? (concat ['(:newline)])) + (section "Contains" rendered)))) + (when datafy? + (testing "renders the datafy section" + (is (match? '("--- Datafy:" + (:newline) + " " "0" ". " (:value "( 1 1 1 1 1 ... )" 5) + (:newline)) + (datafy-section rendered)))))))) + +(deftest inspect-clojure-string-namespace-test + (testing "inspecting the clojure.string namespace" + (let [result (-> (find-ns 'clojure.string) inspect render)] + (testing "renders the header" + (is (match? `("Class" + ": " + (:value "clojure.lang.Namespace" 0) + (:newline) + "Count" + ": " + (:value ~(case (:minor *clojure-version*) + 8 "748" + 9 "778" + 10 "786" + "799") + 1) + (:newline) + (:newline)) + (header result)))) + (testing "renders the refer from section" + (is (match? `("--- Refer from:" + (:newline) + " " + (:value "clojure.core" 2) + " = " + (:value ~(str "[ #'clojure.core/primitives-classnames #'clojure.core/+' #'clojure.core/decimal? " + "#'clojure.core/restart-agent #'clojure.core/sort-by ... ]") 3) + (:newline) + (:newline)) + (section "Refer from" result)))) + (testing "renders the imports section" + (is (match? `("--- Imports:" + (:newline) + " " (:value ~(str "{ Enum java.lang.Enum, " + "InternalError java.lang.InternalError, " + "NullPointerException java.lang.NullPointerException, " + "InheritableThreadLocal java.lang.InheritableThreadLocal, " + "Class java.lang.Class, ... }") 4) + (:newline) + (:newline)) + (section "Imports" result)))) + (testing "renders the interns section" + (is (match? (cond-> `("--- Interns:" + (:newline) + " " (:value ~(str "{ ends-with? #'clojure.string/ends-with?, " + "replace-first-char #'clojure.string/replace-first-char, " + "capitalize #'clojure.string/capitalize, " + "reverse #'clojure.string/reverse, join #'clojure.string/join, ... }") 5) + (:newline)) + datafy? (concat ['(:newline)])) + (section "Interns" result)))) + (when datafy? + (testing "renders the datafy from section" + (is (match? `("--- Datafy:" + (:newline) + " " (:value ":name" 6) " = " (:value "clojure.string" 7) + (:newline) + " " (:value ":publics" 8) " = " + (:value ~(str "{ blank? #'clojure.string/blank?, capitalize " + "#'clojure.string/capitalize, ends-with? #'clojure.string/ends-with?, " + "escape #'clojure.string/escape, includes? #'clojure.string/includes?, ... }") 9) + (:newline) + " " (:value ":imports" 10) " = " + (:value ~(str "{ AbstractMethodError java.lang.AbstractMethodError, Appendable java.lang.Appendable, " + "ArithmeticException java.lang.ArithmeticException, ArrayIndexOutOfBoundsException " + "java.lang.ArrayIndexOutOfBoundsException, ArrayStoreException java.lang.ArrayStoreException, ... }") 11) + (:newline) + " " (:value ":interns" 12) " = " + (:value ~(str "{ blank? #'clojure.string/blank?, capitalize #'clojure.string/capitalize, ends-with? #'clojure.string/ends-with?, " + "escape #'clojure.string/escape, includes? #'clojure.string/includes?, ... }") 13) + (:newline)) + (datafy-section result)))))))) + +(deftest inspect-datafiable-metadata-extension-test + (testing "inspecting a map extended with the Datafiable protocol" + (let [rendered (-> (extend-datafy-class {:name "John Doe"}) inspect render)] + (testing "renders the header" + (is (match? '("Class" + ": " + (:value "clojure.lang.PersistentArrayMap" 0) + (:newline) + (:newline)) + (header rendered)))) + (testing "renders the meta information section" + (is (match? '("--- Meta Information:" + (:newline) + " " + (:value "clojure.core.protocols/datafy" 1) + " = " + (:value "orchard.inspect_test$extend_datafy_class$fn" 2) + (:newline) + (:newline)) + (demunge (section "Meta Information" rendered))))) + (when datafy? + (testing "renders the datafy section" + (is (match? '("--- Datafy:" + (:newline) + " " (:value ":name" 5) " = " (:value "\"John Doe\"" 6) + (:newline) + " " (:value ":class" 7) " = " (:value "\"PersistentArrayMap\"" 8) + (:newline)) + (datafy-section rendered)))))))) + +(deftest inspect-navigable-metadata-extension-test + (testing "inspecting a map extended with the Navigable protocol" + (let [rendered (-> (extend-nav-vector {:name "John Doe"}) inspect render)] + (testing "renders the header" + (is (match? '("Class" + ": " + (:value "clojure.lang.PersistentArrayMap" 0) + (:newline) + (:newline)) + (header rendered)))) + (testing "renders the meta information section" + (is (match? '("--- Meta Information:" + (:newline) + " " (:value "clojure.core.protocols/nav" 1) + " = " (:value "orchard.inspect_test$extend_nav_vector$fn" 2) + (:newline) + (:newline)) + (demunge (section "Meta Information" rendered))))) + (when datafy? + (testing "renders the datafy section" + (is (match? '("--- Datafy:" + (:newline) + " " (:value ":name" 5) " = " (:value "[ :name \"John Doe\" ]" 6) + (:newline)) + (datafy-section rendered)))))))) + +(deftest inspect-throwable-test + (testing "inspecting a throwable" + (let [rendered (-> (doto ^Throwable (ex-info "BOOM" {}) + (.setStackTrace (into-array StackTraceElement []))) + inspect render)] + (testing "renders the header" + (is (match? `("Class" + ": " (:value "clojure.lang.ExceptionInfo" 0) + (:newline) + "Value" ": " + (:value + ~(if (= 8 (:minor *clojure-version*)) + (str (str "\"#error {\\n :cause \\\"BOOM\\\"\\n :data {}\\n " + ":via\\n [{:type clojure.lang.ExceptionInfo\\n " + ":message \\\"BOOM\\\"\\n " + ":data {}\\n :at nil}]\\n :trace\\n []}\"")) + (str "\"#error {\\n :cause \\\"BOOM\\\"\\n :data {}\\n :via\\n " + "[{:type clojure.lang.ExceptionInfo\\n " + ":message \\\"BOOM\\\"\\n :data {}}]\\n :trace\\n []}\"")) 1) + (:newline) + (:newline)) + (header rendered)))) + (when datafy? + (testing "renders the datafy section" + (is (match? (case java-api-version + (11 16 17) + '("--- Datafy:" + (:newline) + " " (:value ":via" 34) " = " (:value "[ { :type clojure.lang.ExceptionInfo, :message \"BOOM\", :data {} } ]" 35) + (:newline) + " " (:value ":trace" 36) " = " (:value "[]" 37) + (:newline) + " " (:value ":cause" 38) " = " (:value "\"BOOM\"" 39) + (:newline) + " " (:value ":data" 40) " = " (:value "{}" 41) + (:newline)) + '("--- Datafy:" + (:newline) + " " (:value ":via" 30) " = " (:value "[ { :type clojure.lang.ExceptionInfo, :message \"BOOM\", :data {} } ]" 31) + (:newline) + " " (:value ":trace" 32) " = " (:value "[]" 33) + (:newline) + " " (:value ":cause" 34) " = " (:value "\"BOOM\"" 35) + (:newline) + " " (:value ":data" 36) " = " (:value "{}" 37) + (:newline))) + (datafy-section rendered)))))))) diff --git a/test/orchard/misc_test.clj b/test/orchard/misc_test.clj index a0be30a7..1a96ce13 100644 --- a/test/orchard/misc_test.clj +++ b/test/orchard/misc_test.clj @@ -64,3 +64,20 @@ (deftest directory? (is (misc/directory? (.toURL (.toURI (java.io.File. "")))))) + +(deftest call-when-resolved + (let [f (misc/call-when-resolved 'clojure.core/identity)] + (is (= 1 (f 1)))) + (let [f (misc/call-when-resolved 'com.example/unknown)] + (is (nil? (f 1))))) + +(deftest lazy-seq? + (is (misc/lazy-seq? (repeat 1))) + (is (not (misc/lazy-seq? nil))) + (is (not (misc/lazy-seq? [1 2 3]))) + (is (not (misc/lazy-seq? :not-a-seq)))) + +(deftest safe-count + (is (= 3 (misc/safe-count [1 2 3]))) + (is (nil? (misc/safe-count (repeat 1)))) + (is (nil? (misc/safe-count :not-a-seq))))