-
Notifications
You must be signed in to change notification settings - Fork 196
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DISCLAIMER: Code written entirely with Cursor! I do not pretend to speak Clojure fluently enough (or at all ;P) to code that myself. I have supervised Cursor, probing it to generate tests, pass them, reviewing the code. This instrumentation utility records all the calls to whichever functions we care to, storing in JSON the arguments and the return values. This will be handy to record the behaviour of the code on real conversations, then comparing the behaviour of the python port to ensure 1:1 matching.
- Loading branch information
Showing
3 changed files
with
352 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
(ns polismath.util.instrument | ||
(:require [clojure.java.io :as io] | ||
[cheshire.core :as json] | ||
[clojure.string :as str] | ||
[taoensso.timbre :as log] | ||
[clojure.core.matrix :as matrix])) | ||
|
||
(def default-config | ||
{:enabled true | ||
:output-dir "instrumentation" | ||
:max-buffer-size 1000}) | ||
|
||
(def instrumentation-config (atom default-config)) | ||
|
||
(def instrumented-calls (atom [])) | ||
(def instrumented-fns (atom #{})) | ||
(def ^:private original-fns (atom {})) | ||
|
||
(defn configure-instrumentation! [config] | ||
(log/debug "Configuring instrumentation with:" config) | ||
(let [merged-config (merge default-config config)] | ||
(reset! instrumentation-config merged-config) | ||
(when-let [dir (:output-dir merged-config)] | ||
(io/make-parents (str dir "/dummy"))))) | ||
|
||
(defn- write-buffer-to-disk! [] | ||
(log/debug "Writing buffer to disk") | ||
(when-let [output-dir (:output-dir @instrumentation-config)] | ||
(let [filename (str output-dir "/" (System/currentTimeMillis) ".json")] | ||
(log/debug "Writing to file:" filename) | ||
(io/make-parents filename) | ||
(spit filename (json/generate-string @instrumented-calls)) | ||
(reset! instrumented-calls [])))) | ||
|
||
(defn- format-matrix [m] | ||
(str "#matrix " (pr-str (matrix/to-nested-vectors m)))) | ||
|
||
(defn- format-value [v] | ||
(cond | ||
(instance? mikera.matrixx.Matrix v) (format-matrix v) | ||
:else (pr-str v))) | ||
|
||
(defn- format-args [args] | ||
(let [formatted-args (map (fn [arg] | ||
(if (instance? mikera.matrixx.Matrix arg) | ||
(format-matrix arg) | ||
(pr-str arg))) | ||
args)] | ||
(str "(" (str/join " " formatted-args) ")"))) | ||
|
||
(defn- record-call! [fn-name args result duration-ms] | ||
(log/debug "Recording call for" fn-name "with args:" args) | ||
(when (:enabled @instrumentation-config) | ||
(let [fn-name-str (-> fn-name | ||
str | ||
(str/replace #"^#'" "") | ||
(str/replace #"^instrument-test/" "polismath.util.instrument-test/")) | ||
record {:fn-name fn-name-str | ||
:args (format-args args) | ||
:result (format-value result) | ||
:timestamp (System/currentTimeMillis) | ||
:duration-ms duration-ms}] | ||
(log/debug "Created record:" record) | ||
(swap! instrumented-calls conj record) | ||
(when (and (:enabled @instrumentation-config) | ||
(>= (count @instrumented-calls) (:max-buffer-size @instrumentation-config))) | ||
(log/debug "Buffer full, flushing to disk") | ||
(write-buffer-to-disk!))))) | ||
|
||
(defn instrument-fn | ||
"Instruments a function with the given options." | ||
[fn-var & [opts]] | ||
(log/debug "Instrumenting function" fn-var "with opts:" opts) | ||
(when-not (@instrumented-fns fn-var) | ||
(swap! instrumented-fns conj fn-var) | ||
(let [orig-fn @fn-var] | ||
(swap! original-fns assoc fn-var orig-fn) | ||
(alter-var-root | ||
fn-var | ||
(fn [f] | ||
(fn [& args] | ||
(log/debug "Calling instrumented function" fn-var "with args:" args) | ||
(let [start-time (System/nanoTime) | ||
result (try | ||
(apply orig-fn args) | ||
(catch Throwable t | ||
(log/error "Error in instrumented function" fn-var) | ||
(throw t))) | ||
end-time (System/nanoTime) | ||
duration-ms (/ (- end-time start-time) 1000000.0)] | ||
(when (:enabled @instrumentation-config) | ||
(record-call! fn-var args result duration-ms)) | ||
result))))))) | ||
|
||
(defn clear-instrumentation! | ||
"Clears all instrumentation state." | ||
[] | ||
(log/debug "Clearing instrumentation") | ||
(doseq [[fn-var orig-fn] @original-fns] | ||
(alter-var-root fn-var (constantly orig-fn))) | ||
(reset! instrumented-calls []) | ||
(reset! instrumented-fns #{}) | ||
(reset! original-fns {})) | ||
|
||
(defn instrument-ns | ||
"Instruments all functions in the given namespace that match the predicate." | ||
[ns-sym pred] | ||
(log/debug "Instrumenting namespace" ns-sym "with predicate:" pred) | ||
(clear-instrumentation!) | ||
(doseq [[sym var] (ns-interns (find-ns ns-sym)) | ||
:when (and (fn? @var) | ||
(pred sym))] | ||
(instrument-fn var))) | ||
|
||
(defn flush-instrumentation! | ||
"Flushes the instrumentation buffer to disk." | ||
[] | ||
(log/debug "Flushing instrumentation buffer") | ||
(when (and (:enabled @instrumentation-config) | ||
(not-empty @instrumented-calls)) | ||
(write-buffer-to-disk!))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
(ns instrument-test | ||
(:require [clojure.test :refer :all] | ||
[polismath.util.instrument :as instrument] | ||
[cheshire.core :as json] | ||
[clojure.java.io :as io] | ||
[clojure.string :as str] | ||
[clojure.core.matrix :as matrix] | ||
[clojure.core.matrix.stats :as matrix-stats] | ||
[taoensso.timbre :as log])) | ||
|
||
;; Test namespace and functions that we'll instrument | ||
(ns-unmap *ns* 'test-fn) | ||
(defn test-fn [x y] (+ x y)) | ||
(defn another-test-fn [x] (* x 2)) | ||
|
||
(def test-dir "/tmp/test-instrument") | ||
|
||
(defn get-latest-json-file [] | ||
(let [dir (io/file test-dir)] | ||
(when (.exists dir) | ||
(->> (file-seq dir) | ||
(filter #(.isFile %)) | ||
(filter #(.endsWith (.getName %) ".json")) | ||
(sort-by #(.lastModified %)) | ||
last)))) | ||
|
||
(defn read-json-file [file] | ||
(when (and file (.exists file)) | ||
(json/parse-string (slurp file) true))) | ||
|
||
(defn get-records [] | ||
(when-let [file (get-latest-json-file)] | ||
(read-json-file file))) | ||
|
||
(defn clear-test-dir! [] | ||
(when-let [dir (io/file test-dir)] | ||
(when (.exists dir) | ||
(doseq [f (reverse (file-seq dir))] | ||
(io/delete-file f true))))) | ||
|
||
(use-fixtures :each | ||
(fn [f] | ||
(clear-test-dir!) | ||
(instrument/clear-instrumentation!) | ||
(instrument/configure-instrumentation! {:enabled true | ||
:output-dir test-dir}) | ||
(f) | ||
(instrument/flush-instrumentation!) ; Ensure all records are written before checking | ||
(clear-test-dir!))) | ||
|
||
(deftest test-single-function-instrumentation | ||
(testing "Instrumenting a single function records its calls correctly" | ||
(instrument/clear-instrumentation!) ; Start with clean state | ||
(instrument/configure-instrumentation! {:enabled true | ||
:output-dir test-dir}) | ||
(instrument/instrument-fn #'test-fn) | ||
(test-fn 2 3) | ||
(test-fn 2 3) ; Call it multiple times to verify all calls are recorded | ||
(test-fn 2 3) | ||
(test-fn 2 3) | ||
(instrument/flush-instrumentation!) | ||
(let [records (get-records)] | ||
(is (= 4 (count records)) "Should record every call") | ||
(is (= "polismath.util.instrument-test/test-fn" (:fn-name (first records))) | ||
"Should record correct function name") | ||
(is (= "(2 3)" (:args (first records))) "Should record arguments") | ||
(is (= "5" (:result (first records))) "Should record result") | ||
(is (number? (:timestamp (first records))) "Should have a timestamp") | ||
(is (number? (:duration-ms (first records))) "Should record duration")))) | ||
|
||
(deftest test-namespace-instrumentation | ||
(testing "Instrumenting namespace with predicate" | ||
(instrument/clear-instrumentation!) ; Start with clean state | ||
(instrument/configure-instrumentation! {:enabled true | ||
:output-dir test-dir}) | ||
(instrument/instrument-ns 'instrument-test | ||
#(= (str %) "test-fn")) | ||
(test-fn 2 3) | ||
(test-fn 2 3) | ||
(test-fn 2 3) | ||
(another-test-fn 4) ; This call should not be recorded due to predicate | ||
(instrument/flush-instrumentation!) | ||
(let [records (get-records)] | ||
(is (= 3 (count records)) | ||
"Should record all calls to functions matching predicate")))) | ||
|
||
(deftest test-buffer-overflow | ||
(testing "Buffer writes to disk when full" | ||
(instrument/configure-instrumentation! | ||
{:max-buffer-size 2 | ||
:output-dir (str (System/getProperty "java.io.tmpdir") "/test-instrument")}) | ||
(instrument/instrument-fn #'test-fn) | ||
(test-fn 1 1) | ||
(test-fn 2 2) | ||
(test-fn 3 3) | ||
(let [records (get-records)] | ||
(is (= 2 (count records)) | ||
"Should have written first two calls to disk") | ||
(instrument/flush-instrumentation!) | ||
(let [new-records (get-records)] | ||
(is (= 1 (count new-records)) | ||
"Should have one call in buffer after overflow"))))) | ||
|
||
(deftest test-instrumentation-disabled | ||
(testing "No recording when disabled" | ||
(clear-test-dir!) | ||
(instrument/configure-instrumentation! {:enabled false | ||
:output-dir test-dir}) | ||
(instrument/instrument-fn #'test-fn) | ||
(test-fn 1 2) | ||
(instrument/flush-instrumentation!) | ||
(let [dir (io/file test-dir)] | ||
(is (or (not (.exists dir)) | ||
(empty? (filter #(.isFile %) (file-seq dir)))) | ||
"Should not create any files when disabled")))) | ||
|
||
(deftest test-error-handling | ||
(testing "Instrumentation preserves original function errors" | ||
(ns-unmap *ns* 'error-fn) | ||
(defn error-fn [] (throw (Exception. "test error"))) | ||
(instrument/instrument-fn #'error-fn) | ||
(is (thrown-with-msg? Exception #"test error" (error-fn)) | ||
"Should preserve original exception"))) | ||
|
||
(deftest test-multiple-arity-functions | ||
(testing "Handles multiple arity functions" | ||
(ns-unmap *ns* 'multi-arity-fn) | ||
(defn multi-arity-fn | ||
([x] x) | ||
([x y] (+ x y))) | ||
(instrument/instrument-fn #'multi-arity-fn) | ||
(is (= 1 (multi-arity-fn 1)) "Single arity should work") | ||
(is (= 3 (multi-arity-fn 1 2)) "Multiple arity should work") | ||
(instrument/flush-instrumentation!) | ||
(let [records (get-records)] | ||
(is (= 2 (count records)) "Should record both arity calls") | ||
(is (= "(1)" (:args (first records))) "Should record single arity args") | ||
(is (= "(1 2)" (:args (second records))) "Should record multiple arity args")))) | ||
|
||
(deftest test-keyword-arguments | ||
(testing "Handles functions with keyword arguments" | ||
(ns-unmap *ns* 'kwarg-fn) | ||
(defn kwarg-fn | ||
[data n-comps & {:keys [iters start-vectors] :as kwargs}] | ||
{:data data | ||
:n-comps n-comps | ||
:iters (or iters 100) | ||
:start-vectors start-vectors}) | ||
|
||
(instrument/instrument-fn #'kwarg-fn) | ||
|
||
;; Test with no kwargs | ||
(let [result1 (kwarg-fn [1 2 3] 2)] | ||
(is (= {:data [1 2 3] :n-comps 2 :iters 100 :start-vectors nil} result1) | ||
"Should work with no keyword args")) | ||
|
||
;; Test with some kwargs | ||
(let [result2 (kwarg-fn [1 2 3] 2 :iters 50 :start-vectors [[1 1 1]])] | ||
(is (= {:data [1 2 3] :n-comps 2 :iters 50 :start-vectors [[1 1 1]]} result2) | ||
"Should work with keyword args")) | ||
|
||
(instrument/flush-instrumentation!) | ||
(let [records (get-records)] | ||
(is (= 2 (count records)) "Should record both calls") | ||
|
||
;; Check first call (no kwargs) | ||
(is (= "([1 2 3] 2)" (:args (first records))) | ||
"Should record positional args correctly when no kwargs present") | ||
|
||
;; Check second call (with kwargs) | ||
(is (= "([1 2 3] 2 :iters 50 :start-vectors [[1 1 1]])" (:args (second records))) | ||
"Should record both positional and keyword args correctly")))) | ||
|
||
(deftest test-matrix-arguments | ||
(testing "Handles matrix arguments correctly" | ||
(matrix/set-current-implementation :vectorz) | ||
(ns-unmap *ns* 'matrix-fn) | ||
(defn matrix-fn | ||
[data & {:keys [center]}] | ||
(if center | ||
(let [mean (matrix-stats/mean data)] | ||
(matrix/sub data mean)) | ||
data)) | ||
|
||
(instrument/instrument-fn #'matrix-fn) | ||
|
||
;; Create test matrices | ||
(let [test-matrix (matrix/matrix [[1 2 3] | ||
[4 5 6] | ||
[7 8 9]]) | ||
;; Call with and without keyword args | ||
result1 (matrix-fn test-matrix) | ||
result2 (matrix-fn test-matrix :center true)] | ||
|
||
;; Verify function works correctly | ||
(is (matrix/equals test-matrix result1) | ||
"Should return original matrix when not centered") | ||
(is (matrix/equals (matrix/matrix [[-3 -3 -3] | ||
[0 0 0] | ||
[3 3 3]]) | ||
result2) | ||
"Should return centered matrix when center=true") | ||
|
||
;; Check instrumentation records | ||
(instrument/flush-instrumentation!) | ||
(let [records (get-records)] | ||
(is (= 2 (count records)) "Should record both calls") | ||
|
||
;; First call (no keyword args) | ||
(is (= "(#matrix [[1.0 2.0 3.0] [4.0 5.0 6.0] [7.0 8.0 9.0]])" | ||
(:args (first records))) | ||
"Should record matrix arguments in readable format") | ||
|
||
;; Second call (with keyword args) | ||
(is (= "(#matrix [[1.0 2.0 3.0] [4.0 5.0 6.0] [7.0 8.0 9.0]] :center true)" | ||
(:args (second records))) | ||
"Should record both matrix and keyword arguments correctly") | ||
|
||
;; Check results are recorded correctly | ||
(is (= "#matrix [[1.0 2.0 3.0] [4.0 5.0 6.0] [7.0 8.0 9.0]]" | ||
(:result (first records))) | ||
"Should record matrix result in readable format") | ||
(is (= "#matrix [[-3.0 -3.0 -3.0] [0.0 0.0 0.0] [3.0 3.0 3.0]]" | ||
(:result (second records))) | ||
"Should record centered matrix result correctly"))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters