diff --git a/README.md b/README.md index 9fd4ad6..d98db42 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,16 @@ Namespaced keywords can be used to group cassettes in the filesystem. Each var that is recorded can be customized with options: +- `:arg-transformer`: A function with the same argument signature as the + recorded function, which returns a vector of possibly transformed arguments. + During recording/playback, the original arguments to the function call are + passed through this transformer, and the transformed arguments are passed to + `arg-key-fn`, `recordable?` and the recorded function. This can be useful for + replacing an argument would be destructively consumed (e.g. a mutable + `InputStream`) with an indestructible substitute. The transformed arguments + ought to be equivalent to the original arguments for the purpose of the code + under test. The default is `clojure.core/vector`, which just passes along + the original arguments. - `:arg-key-fn`: A function with the same argument signature as the recorded function, which returns a value for "fingerprinting" the arguments to each call. During recording, the value returned by this function will be saved diff --git a/src/vcr_clj/core.clj b/src/vcr_clj/core.clj index a515e36..0fee8b1 100644 --- a/src/vcr_clj/core.clj +++ b/src/vcr_clj/core.clj @@ -21,22 +21,24 @@ true) (defn ^:private build-wrapped-fn - [record-fn {:keys [var arg-key-fn recordable? return-transformer] - :or {arg-key-fn vector + [record-fn {:keys [var arg-transformer arg-key-fn recordable? return-transformer] + :or {arg-transformer vector + arg-key-fn vector recordable? (constantly true) return-transformer identity}}] (let [orig-fn (deref var) the-var-name (var-name var) wrapped (fn [& args] - (if-not (and *recording?* (apply recordable? args)) - (apply orig-fn args) - (let [res (binding [*recording?* false] - (return-transformer (apply orig-fn args))) - call {:var-name the-var-name - :arg-key (apply arg-key-fn args) - :return res}] - (record-fn call) - res)))] + (let [args* (apply arg-transformer args)] + (if-not (and *recording?* (apply recordable? args*)) + (apply orig-fn args*) + (let [res (binding [*recording?* false] + (return-transformer (apply orig-fn args*))) + call {:var-name the-var-name + :arg-key (apply arg-key-fn args*) + :return res}] + (record-fn call) + res))))] (add-meta-from wrapped orig-fn))) ;; TODO: add the ability to configure whether out-of-order @@ -91,17 +93,19 @@ (let [the-playbacker (playbacker cassette :key) redeffings (into {} - (for [{:keys [var arg-key-fn recordable?] - :or {arg-key-fn vector + (for [{:keys [var arg-transformer arg-key-fn recordable?] + :or {arg-transformer vector + arg-key-fn vector recordable? (constantly true)}} specs :let [orig (deref var) the-var-name (var-name var) wrapped (fn [& args] - (if (apply recordable? args) - (let [k (apply arg-key-fn args)] - (:return (the-playbacker the-var-name k))) - (apply orig args)))]] + (let [args* (apply arg-transformer args)] + (if (apply recordable? args*) + (let [k (apply arg-key-fn args*)] + (:return (the-playbacker the-var-name k))) + (apply orig args*))))]] [var (add-meta-from wrapped orig)]))] (with-redefs-fn redeffings func))) @@ -133,6 +137,14 @@ ;; the rest are optional + :arg-transformer + a function with the same arg signature as the var, + which is expected to returns a vector of equivalent + arguments. During recording/playback, the original + arguments to the function call are passed through this + transformer, and the transformed arguments are passed + to arg-key-fn, recordable? and the recorded function. + Defaults to clojure.core/vector. :arg-key-fn a function of the same arguments as the var that is expected to return a value that can be stored and compared for equality to the expected call. Defaults diff --git a/test/vcr_clj/test/core.clj b/test/vcr_clj/test/core.clj index e2f3d6a..3391f4d 100644 --- a/test/vcr_clj/test/core.clj +++ b/test/vcr_clj/test/core.clj @@ -12,6 +12,7 @@ ;; some test fns (defn plus [a b] (+ a b)) (defn increment [x] (inc x)) +(defn put-foo [m v] (assoc m :foo v)) (deftest basic-test (with-spy [plus] @@ -87,6 +88,27 @@ :arg-key-fn #(mod % 2)}] (is (= 42 (increment 29))))) +(deftest arg-transformer-test + (with-spy [put-foo] + (with-cassette :shebang + [{:var #'put-foo + :arg-transformer (fn [m v] + (is (= [{} 42] [m v])) + [(vary-meta m assoc :arg-transformer true) v]) + :recordable? (fn [m v] + (is (= [{} 42] [m v])) + (is (:arg-transformer (meta m))) + true) + :arg-key-fn (fn [m v] + (is (= [{} 42] [m v])) + (is (:arg-transformer (meta m))) + [m v])}] + (is (= {:foo 42} (put-foo {} 42))) + (is (= 1 (count (calls put-foo)))) + (is (= [{} 42] + (:args (first (calls put-foo))))) + (is (-> (calls put-foo) first :args first meta :arg-transformer))))) + (defn self-caller "Multi-arity function that calls itself when called with one argument" ([x]