ghostwheel

Hassle-free clojure.spec, automatic generative testing, side effect detection, and evaluation tracing for Clojure(-Script) – https://github.com/gnl/ghostwheel
genekim 2018-08-05T15:19:30.000029Z

@clojurians.net — I’m delighted to be finally giving ghostwheel a try for an app that has both CLJ and CLJS component. I’m starting with server-side CLJ, just because it’s something I was writing. Delighted that setup was so easy — changes to project.clj and clj file was very straightforward, and wrote my first (>defn) which compiled. But when I passed in an incorrect parameter, it didn’t generate an error. Can you help me figure out what I did wrong? Thank you!!!

(ns trello-workflow.mongodb
  #:ghostwheel.core{:check     true
                    :num-tests 0}
  (:require [monger.core :as mg]
            [monger.collection :as mc]
            [trello-workflow.env :as tenv]
            [ghostwheel.core :as g
             :refer [>defn >defn- >fdef => | <- ?]])

(>defn update-user-record
  " given trello user name, update the board record in database "
  [username record]
  [string? map?
    => map?]
  (mc/insert-and-return (:db db)  "documents" {:name username :hotkeys record}))

genekim 2018-08-05T15:20:55.000093Z

REPL interaction

; second argument should be a map
(update-user-record "genekim" "abc")
; expected an error, but none generated...
{:_id #object[org.bson.types.ObjectId 0x2aeae19f "5b67144fe6136b81f10e7d78"], :name "genekim", :hotkeys "abc"}

gnl 2018-08-05T16:31:54.000009Z

@genekim – with that configuration Ghostwheel will generate the fspec/fdef and test code, but no actual checking of any sort will be performed. You can go about this two ways: - Add the ::g/instrument or ::g/outstrument metadata to enable instrumentation with cljs.test or orchestra respectively - Set ::g/num-tests to > 0 and call (g/check) at the bottom of the namespace to do generative testing. However this will only work in ClojureScript for now, test execution isn't implemented for Clojure yet (test generation is though, so you can call the tests with a test-runner of your choice). Plus this looks like a side-effectful function so generative testing isn't a great idea anyway. Something's in the works for that, but it's still in an early prototype phase. I recommend you make sure to suffix the function name with ! according to the Clojure guidelines for impure/unsafe functions like this one. This will also ensure that if you enable generative testing on the namespace or higher level, it won't run for functions that do I/O. Hope this helps!

gnl 2018-08-05T16:37:45.000037Z

Also check out the re-frame guide on how to write pure event handlers: https://github.com/Day8/re-frame/blob/master/docs/EffectfulHandlers.md Even if you aren't using re-frame (or ClojureScript for that matter), I think the principles described can be implemented to some degree in any code base with relatively little effort.

gnl 2018-08-05T16:43:01.000013Z

Also keep in mind that the compiler-level configuration is only implemented for ClojureScript at the moment so there's no way to disable Ghostwheel in production on Clojure. That's fine as long as you just generate the fdef and deftest code with Ghostwheel, but if you do instrumentation like this it would end up in production which may or may not be what you want.

genekim 2018-08-05T16:47:39.000009Z

Wonderful, @clojurians.net — thanks for all the advice! Actually, the functionality I’m looking for right away, even before generative testing, is getting the equivalent of:

(defn display-notification
  [msg msgtype]
  {:pre [(s/valid? string? msg) (s/valid? keyword? msgtype)]})))
In other words, I’d just love some run-time errors when I pass in the wrong types. How can I enable that? PS: Even for that one function, I found writing the types in Ghostwheel to be a joy. I’ve made it a habit to write s/valid`` checks for my parameters, and I love your treatment much better. Thx again for your wonderful work!

gnl 2018-08-05T17:00:22.000068Z

@genekim In that case what you're looking for is spec instrumentation with the orchestra library, which is enabled with the ::g/outstrument metadata like this: (>defn ^::g/outstrument update-user-record! ...) And thank you!

gnl 2018-08-05T17:03:41.000018Z

You can do this on the namespace level as well of course, but checking the input and output of every single function at runtime would definitely have a noticeable performance impact.

gnl 2018-08-05T17:05:30.000056Z

Alternatively you can just temporarily instrument stuff in the REPL with (st/instrument fn-name) if you've defined the specs with Ghostwheel without needing the extra metadata, but that wouldn't persist between namespace reloads.

genekim 2018-08-05T17:26:07.000026Z

That’s super, @clojurians.net. Thanks! FWIW, I make enough parameter errors that I’ll likely default to enabling at namespace level, and maybe selectively turning off some functions. (Uh, how would one turn off outstrument on a per-function basis?) I can’t wait to try this tonight, and I’ll be sure to write up something about it — I’m excited about your work!

gnl 2018-08-05T17:50:43.000033Z

@genekim You just need to override the Ghostwheel metadata on the function level, see step 5 here: https://github.com/gnl/ghostwheel#getting-started In this case you can do (>defn ^{::g/outstrument false} fn-name ...) or (>defn fn-name {::g/outstrument false} ...)

👍 1
🎉 1