Previously (here) I sketched out a design that went some way to being a coverage tool for Clojure code. The result was a macro that when used to define a function, resulted in an instrumented function that tallied every time the code was called. This time I am going to sketch the beginnings of a design that doesn’t use a macro.
To start with here is a slight re-write of the structure I used last time to record the coverage:
(def coverage-records (ref nil))
(defn add-record [fn-name record]
(dosync (alter coverage-records assoc (str fn-name) record)))
(defn get-record [fn-name]
(get @coverage-records (str fn-name)))
(defn covering [form fn-name]
(dosync (alter (get-record fn-name) assoc form 0)))
(defn- inc-map [map key]
(if (contains? map key)
(assoc (dissoc map key) key (inc (get map key)))
map))
(defn inc-coverage [form fn-name]
(dosync (alter (get-record fn-name) inc-map form)))
This code uses a single map to store a coverage record, another map, for each function that is being measured. Functions are provided for adding records and incrementing the coverage.
I am using the same set of functions to wrap and instrument the s-expressions that define the function, with a few minor modifications. I am indexing the s-expressions with a number so as to distinguish between any identical s-expressions within the function. This set of functions is still incomplete since there are lots of Clojure code that will get wrapped incorrectly, I will be rectifying this sometime in the near future.
(declare wrap-seq)
(defn wrap
[form idx fn-name]
(cond
(seq? form) (
let [key (str idx ":" form)]
(covering key fn-name)
(list ‘do `(inc-coverage ~key ~(str fn-name)) (wrap-seq form idx fn-name)))
:else form))
(defn- indexed [coll] (map vector (iterate inc 0) coll))
(defn wrap-seq [coll count fn-name]
(for [[idx elt] (indexed coll)] (wrap elt (+ count idx) fn-name)))
The previous article used a macro to generate Clojure code that was instrumented, this time I will load the source code for the function, wrap it and then evaluate it. This is done by the get-source function in clojure.contrib.repl-utils combined with a call to format and load-string, My code that interprets the source code is fragile and needs more work, it won’t handle functions with multiple bodies for example, but it demonstrates the principle. I am using a structure to hold the instrumented function and the coverage record for this function.
(defstruct wrapper :wrapped-fn :coverage-record)
(defn wrap-fn
[f cv-rec]
(let [s (read-string (get-source f))
fn-name (first (drop 1 s))
args (first (drop 2 s))
body (last s)]
(add-record fn-name cv-rec)
(load-string (format “(fn %s %s)” args (wrap body 0 fn-name)))))
(defn wrap-function [f]
(let [coverage-record (ref nil)]
(struct wrapper (wrap-fn f coverage-record) coverage-record)))
So now we can try the code out at the REPL:
user=> (def x (wrap-function 'test1))
#'user/x
user=> x
{:wrapped-fn #, :coverage-record #}
Here I have wrapped the function test1 and we can see the coverage structure returned, consisting of the wrapped function and the coverage record, keyed by the single s-expression with a count of 0. If I now use the wrapped function like this and examine the coverage structure the count should increment to 1.
user=> ((:wrapped-fn x) 1 2)
-1
user=> x
{:wrapped-fn #, :coverage-record #}
So we now have a partially functional coverage tool and no macros have needed to be written. To complete this tool I just need to tidy up the wrapping and source code reading functions and provide some sort of binding macro so that we can call the fn with
(test1 1 2)instead of
((:wrapped-fn x) 1 2). Hopefully, in my next blog entry I will have completed it!
You are currently browsing the LShift Ltd. blog archives for June, 2010.