-
-
Save mourjo/c7fc03e59eb96f8a342dfcabd350a927 to your computer and use it in GitHub Desktop.
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
| ;; Commentary ;; | |
| ;; ;; | |
| ;; The goal for writing this started with the idea to have tests run in ;; | |
| ;; parallel using the leiningen plugin eftest ;; | |
| ;; https://github.com/weavejester/eftest. ;; | |
| ;; ;; | |
| ;; With tests using with-redefs, it was not possible to run them in ;; | |
| ;; parallel if they were changing the root binding of the same ;; | |
| ;; vars. Here, we are binding the root of the var to one function that ;; | |
| ;; respects per-thread rebindings, if any exist. ;; | |
| ;; ;; | |
| ;; Known caveats: ;; | |
| ;; - This per-therad rebinding will only work with clojure concurrency ;; | |
| ;; primitives which copy per-thread bindings to newly spawned threads, ;; | |
| ;; eg, using clojure futures. But will not work for, say a ;; | |
| ;; java.lang.Thread. ;; | |
| ;; - As of now this only supports functions being bound and not other ;; | |
| ;; vars which store values, say (def x 19) for example. ;; | |
| ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |
| (def ^:dynamic local-redefinitions {}) | |
| (defn current->original-definition | |
| [v] | |
| (when (var? v) | |
| (get (meta v) ::original))) | |
| (defn redefiniton-fn | |
| [a-var] | |
| (fn [& args] | |
| (let [current-f (get local-redefinitions | |
| a-var | |
| (current->original-definition a-var))] | |
| (apply current-f args)))) | |
| (defn dynamic-redefs | |
| [vars func] | |
| (let [un-redefs (remove #(::already-bound? (meta %)) vars)] | |
| (doseq [a-var un-redefs] | |
| (locking a-var | |
| (when-not (::already-bound? (meta a-var)) | |
| (let [old-val (.getRawRoot ^clojure.lang.Var a-var)] | |
| (.bindRoot ^clojure.lang.Var a-var | |
| (redefiniton-fn a-var)) | |
| (alter-meta! a-var | |
| (fn [m] | |
| (assoc m | |
| ::already-bound? true | |
| ::original old-val)))))))) | |
| (func)) | |
| (defn xs->map | |
| [xs] | |
| (reduce (fn [acc [k v]] (assoc acc `(var ~k) v)) | |
| {} | |
| (partition 2 xs))) | |
| (defmacro with-dynamic-redefs | |
| [bindings & body] | |
| ;; @TODO: Add support for non-functions | |
| (let [map-bindings (xs->map bindings)] | |
| `(let [old-rebindings# local-redefinitions] | |
| (binding [local-redefinitions (merge old-rebindings# ~map-bindings)] | |
| (dynamic-redefs ~(vec (keys map-bindings)) | |
| (fn [] ~@body)))))) | |
| (comment ;; for testing | |
| (defn funk [& args] {:original-args args}) | |
| (dotimes [i 1000] | |
| (let [f1 (future (with-dynamic-redefs [funk (constantly -100)] | |
| (Thread/sleep (rand-int 10)) | |
| {:100 (funk) :t (.getName (Thread/currentThread))})) | |
| f2 (future (with-dynamic-redefs [funk (constantly -200)] | |
| (Thread/sleep (rand-int 1000)) | |
| {:200 (funk 9) :t (.getName (Thread/currentThread))})) | |
| f3 (future (do | |
| (Thread/sleep (rand-int 1000)) | |
| {:orig (funk 9) :t (.getName (Thread/currentThread))}))] | |
| (when (or (not= (:100 @f1) -100) | |
| (not= (:200 @f2) -200) | |
| (not= (:orig @f3) {:original-args '(9)})) | |
| (println "FAIL") | |
| (prn @f1) | |
| (prn @f2) | |
| (println "----------------\n\n"))))) |
Got it, thank you!
This gist is a bit unfortunate since both of you (@filipesilva, @mourjo) have a way to fix a lot of complexity we are finding on the Clojure services created out there. The fact that with-redefs is not thread-safe influenced (for wrong reasons) the creation of a lot of indirections. I wish the Clojure community would embrace what you have created.
@KingMob wrote something great about it: https://modulolotus.net/blog/2022-06-22-tidd/
I wish all of this had a bit more visibility.
I wish all of this had a bit more visibility.
Me too 😄
Hey all! Sorry it took me so long to get to this - it would be really great to combine the two implementations
@filipesilva would you be okay if I blatantly copy how you are handling metadata in your implementation? Or if you prefer a pull request, that'd be great too.
I wish we would be end up with a single with-dynamic-redefs library.
@mourjo go for it :D
We just removed it and did not do parallel tests. They were a bit tricky to get right, and weren't worth the complexity. If doing it again, I'd probably try sharding the test suite at the process level instead.