Ghostwheel
Hassle-free inline clojure.spec with semi-automatic generative testing and side effect detection
Install / Use
/learn @gnl/GhostwheelREADME
https://github.com/gnl/ghostwheel/blob/master/STATUS.adoc[Project update July 2023: DO NOT PANIC – this is not a eulogy]
TL;DR: Ghostwheel's core functionality has been mostly split into independent (but compatible) projects, which also improve on it in various ways:
- Inline gspec definitions and runtime spec-checking – https://github.com/fulcrologic/guardrails[fulcrologic/guardrails]
- Evaluation tracing – https://github.com/gnl/playback[gnl/playback]
- Generative testing and side-effect detection – TBD
Robustness and Observability Without the Pain
:linkattrs: :toc: :toc-placement!: :hardbreaks: :sectanchors: ifndef::env-github,env-cljdoc[] :imagesdir: ../gnl.gitlab.io/public/images endif::[] ifdef::env-github,env-cljdoc[] :imagesdir: https://gnl.gitlab.io/images :tip-caption: :bulb: :note-caption: :information_source: :important-caption: :heavy_exclamation_mark: :caution-caption: :fire: :warning-caption: :warning: endif::[]
[quote, 'Roger Zelazny, Trumps of Doom'] Random let me get through half a cup of coffee before he said, “Tell me about the Ghostwheel.“ “It's a kind of para-physical surveillance device and library.“ Random put down his cup and cocked his head to one side. “Could you be more specific?“ he said. “In other words, I had to locate a shadow environment where the operations would remain pretty much invariant but where the physical construct, all of the peripherals, the programming techniques and the energy inputs would be of a different nature.“ “You've lost me already.“
{zwsp}
image:https://img.shields.io/clojars/v/gnl/ghostwheel.svg[link=https://clojars.org/gnl/ghostwheel] image:https://cljdoc.xyz/badge/gnl/ghostwheel[link=https://cljdoc.xyz/jump/release/gnl/ghostwheel] image:license.svg[link=https://choosealicense.com/licenses/epl-2.0] image:https://circleci.com/gh/gnl/ghostwheel.svg?style=shield["CircleCI", link="https://circleci.com/gh/gnl/ghostwheel"]
toc::[]
Introduction
Ghostwheel makes using clojure.spec easy, minimises the need for unit tests, detects unexpected side effects at compile time, and helps you see what your code is doing so that you can play, explore and refactor fearlessly.
It's about getting the mundane, frustrating stuff out of the way in order to let you focus on the creative side of building software and maybe even get some link:https://gnl.gitlab.io/images/clojurian-using-ghostwheel.gif[quality hammock time^] without cryptic stack traces invading your dreams.
Here are some buzzwords and pictures:
// TODO: Add notes on runtime and compile time performance impact
[[gspec-comparison]]
- Inline fspec definitions with a concise syntax for single- and multi-arity functions for improved readability and minimal effort spec writing and refactoring
...so instead of writing specced functions like this: + image::image-1.png[,700] + ...you can write them like this: + image::image-2.png[,700] + ...or using the alternative symbolic operators (with ligatures): + image::image-3.png[,700]
- Automagical generative testing – off by default – of specced, side-effect-free functions on namespace reload, with human-readable expound-powered reporting and support for spec instrumentation of internal and external namespaces, including experimental specs for most of clojure.core
image::image-6-1.png[,700] + image::image-6.png[,700]
- Explicit side-effect annotations with heuristic compile-time validation (= making sure you stick to naming your unsafe functions with a bang)
image::image-7.png[,700] + image::image-8.png[,700]
[[tracing-overview]]
- Comprehensive tracing of function I/O, bindings and all threading macros for smooth debugging and exploratory programming
ClojureScript only at the moment. + image::image-9.png[,700] + [[tracing-screenshot]] image::image-10.png[,700]
- Effortless spec-based stub generation in nil-body functions for rapider prototyping
image::image-11.png[,700]
- Easy instrumentation of individual functions and namespaces with cljs.spec.test or orchestra on namespace reload
image::image-12.png[,700]
- Experimental automatic generation of Google Closure type annotations from fspec definitions
WIP, ClojureScript only. + image::image-13.png[,700] + image::image-14.png[,700]
Walkthrough
[quote, 'James S.A. Corey, Nemesis Games, The Expanse series'] “There was a button,“ Holden said. “I pushed it.“ + “Jesus Christ. That really is how you go through life, isn't it?“
{zwsp}
How to Read a Readme
It's the age of smartphone notifications, cat videos and Twitter. You are not unlikely to have the attention span of a sleep-deprived parakeet and this walkthrough looks terrifyingly long (it's just the pictures, really). Here's your personal read-it/skim-it guide:
Definitely read:
CAUTION: <- Danger zone.
WARNING: Read this or strange things might happen that'll freak you out.
Stuff you simply need to know in order to use Ghostwheel effectively is written as regular text, like this.
Better read:
TIP: Tips and tricks to make the most of Ghostwheel. Not critical but highly recommended.
Maybe skim:
NOTE: This is additional information on how and/or why something works the way it does. Read if you are curious or intend to open an issue and aren't certain if it's Ghostwheel's fault. Otherwise non-essential so feel free to skip or skim it. I'll be silently judging you.
Getting Started
. Install
+
.Main artifact
image:https://img.shields.io/clojars/v/gnl/ghostwheel.svg[link=https://clojars.org/gnl/ghostwheel]
+
.Production stubs
image:https://img.shields.io/clojars/v/gnl/ghostwheel.stubs.svg[link=https://clojars.org/gnl/ghostwheel.stubs]
+
Both provide the same public namespaces and API, but the stubs don't do anything other than strip any Ghostwheel-specific code.
+
.ClojureScript only:
link:https://github.com/binaryage/cljs-devtools[Setup CLJS DevTools]
+
WARNING: Make sure you use Clojure 1.10.0/ClojureScript 1.10.520 or higher; if you're using figwheel-main, use 0.2.1-SNAPSHOT or higher.
+
[TIP]
To set up the stubs, you can use link:https://clojure.org/reference/deps_and_cli#_aliases[deps.edn aliases] with the :extra-deps clause or link:https://github.com/technomancy/leiningen/blob/master/doc/PROFILES.md[Leiningen profiles], depending on your build setup, in order to depend on gnl/ghostwheel in your dev / testing builds, and on gnl/ghostwheel.stubs in your production builds.
[NOTE]
You can disable Ghostwheel by building with the JVM system property -Dghostwheel.enabled=false
When disabled, Ghostwheel doesn't generate any extra code whatsoever other than simply passing through the plain, unchanged defn s.
That being said, it's recommended to use the stubs artifact in production as it does the same and has the additional advantage of having no external dependencies. This is a significantly more reliable way to reduce build size than dead code elimination.
. Configure
+
Ghostwheel's behaviour is determined individually for each function by merging the configuration maps:
+
ghostwheel.utils/ghostwheel-default-config -> ghostwheel.edn config file in the project root -> {:external-config {:ghostwheel {...}} compiler options (ClojureScript only) -> namespace metadata -> function metadata.
+
See the link:https://github.com/gnl/ghostwheel/blob/master/src/ghostwheel/utils.cljc#L29[default configuration map] for a description of the options – unless explicitly stated otherwise, each one can be overridden on any level.
+
[TIP]
Note that Ghostwheel uses ghostwheel.core-qualified keywords for its configuration, except in the top level configuration (ghostwheel.edn or compiler options). To minimise verbosity you can use namespaced maps for the namespace metadata like this:
(ns test-chamber.one
#:ghostwheel.core{:check true
:num-tests 10}
...)
There's no need for this in the function metadata – if you alias Ghostwheel with [ghostwheel.core :as g] you can just reference the options as ::g/check.
. Use +
(:require [ghostwheel.core :as g :refer [>defn >defn- >fdef => | <- ? |> tr]])
[NOTE]
Depending on how you intend to use Ghostwheel only some of these will be required:
>defn, >defn-, >fdef – main Ghostwheel macros, described in detail in the next section
\=>, |, \<-, ? – optional shortcuts to help write function specs using the <<gspec-syntax, gspec syntax>>.
|>, tr – println on steroids – wrappers for easy <<adhoc-tracing, ad-hoc code evaluation tracing>>.
Staying Sane with Function Specs and Generative Testing
[quote, 'Neil Gaiman & Terry Pratchett, Good Omens: The Nice and Accurate Prophecies of Agnes Nutter, Witch']
25 And the Lord spake unto the Angel that guarded the eastern gate, saying "Where is the flaming sword that was given unto thee?" +
26 And the Angel said, "I had it here only a moment ago, I must have put it down somewhere, forget my own head next." +
27 And the Lord did not ask him again.
{zwsp}
Function specs are generally defined inline using the >defn macro, except when defining them for functions in external namespaces – mainly for instrumentation – in which case >fdef is used.
>defn is almost identical to defn, except that the first body form must be an inline spec definition using the gspec syntax (to be explained in detail in the <<gspec-syntax,next section>>):
[[gspec-example]]
(>defn ranged-rand
"I was lifted straight from the clojure.spec guide"
[start end]
[int? int? | #(< start end)
=> int? | #(>= % start) #(< % end)]
(+ start (long (rand (- end start)))))
TIP: Leave out the function body or set it to nil and you get an automatically generated, spec-instrumented stub, which, when passed the correct arguments, returns random data according to the spec.
TIP: The gspec can be set to nil – in which case no s/fdef block is generated – but it cannot be left out.
[NOTE]
Note that the actual
