Giter Site home page Giter Site logo

gnl / ghostwheel Goto Github PK

View Code? Open in Web Editor NEW
600.0 19.0 14.0 462 KB

Hassle-free inline clojure.spec with semi-automatic generative testing and side effect detection

License: Eclipse Public License 2.0

Clojure 100.00%
clojurescript clojure spec clojure-spec tracing debugging testing side-effects

ghostwheel's Introduction

TL;DR: Ghostwheel’s core functionality has been mostly split into independent (but compatible) projects, which also improve on it in various ways:

Robustness and Observability Without the Pain

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.“
— Roger Zelazny, Trumps of Doom

ghostwheel ghostwheel license CircleCI

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 quality hammock time without cryptic stack traces invading your dreams.

Here are some buzzwords and pictures:

  • 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 1

    …​you can write them like this:

    image 2

    …​or using the alternative symbolic operators (with ligatures):

    image 3
  • 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 6 1
    image 6
  • Explicit side-effect annotations with heuristic compile-time validation (= making sure you stick to naming your unsafe functions with a bang)

    image 7
    image 8
  • Comprehensive tracing of function I/O, bindings and all threading macros for smooth debugging and exploratory programming

    ClojureScript only at the moment.

    image 9
    image 10
  • Effortless spec-based stub generation in nil-body functions for rapider prototyping

    image 11
  • Easy instrumentation of individual functions and namespaces with cljs.spec.test or orchestra on namespace reload

    image 12
  • Experimental automatic generation of Google Closure type annotations from fspec definitions

    WIP, ClojureScript only.

    image 13
    image 14

Walkthrough

“There was a button,“ Holden said. “I pushed it.“
“Jesus Christ. That really is how you go through life, isn’t it?“
— James S.A. Corey, Nemesis Games, The Expanse series

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:

🔥
← Danger zone.
⚠️
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:

💡
Tips and tricks to make the most of Ghostwheel. Not critical but highly recommended.

Maybe skim:

ℹ️
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

  1. Install

    Main artifact

    ghostwheel

    Production stubs

    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:

    Setup CLJS DevTools

    ⚠️
    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.
    💡

    To set up the stubs, you can use deps.edn aliases with the :extra-deps clause or 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.

    ℹ️

    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.

  2. Configure

    Ghostwheel’s behaviour is determined individually for each function by merging the configuration maps:

    ghostwheel.utils/ghostwheel-default-configghostwheel.edn config file in the project root → {:external-config {:ghostwheel {…​}} compiler options (ClojureScript only) → namespace metadata → function metadata.

    See the default configuration map for a description of the options – unless explicitly stated otherwise, each one can be overridden on any level.

    💡

    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.

  3. Use

    (:require [ghostwheel.core :as g :refer [>defn >defn- >fdef => | <- ? |> tr]])
    ℹ️

    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.
    |>, trprintln on steroids – wrappers for easy ad-hoc code evaluation tracing.

Staying Sane with Function Specs and Generative Testing

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.
— Neil Gaiman & Terry Pratchett, Good Omens: The Nice and Accurate Prophecies of Agnes Nutter, Witch

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 next section):

(>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)))))
💡
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.
💡
The gspec can be set to nil – in which case no s/fdef block is generated – but it cannot be left out.
ℹ️

Note that the actual parameter symbols are used in the anonymous predicates instead of (-> % :args :start), which is not only shorter, but also lets you do quick and clean rename refactorings in your IDE instead of having to hunt down non-namespaced keywords in multiple nested forms.

From the point of view of the programmer and the editor, the function arguments are bound to their respective symbols and can be freely referenced in any expression as expected, including the gspec which is considered just another body form inside the function.

In fact you can even use argument destructuring with this, except if you go too crazy with it (= more than one level of nesting) things can break due to an imperfect workaround for Clojure bugs CLJ-2003 and CLJ-2021.

>fdef is pretty much the same, except for the missing body forms:

(>fdef ranged-rand
   [start end]
   [int? int? | #(< start end)
    => int? | #(>= % start) #(< % end)])
💡

If you’re using Cursive IDE, it’s probably a good idea to use IntelliJ’s intention actions to tell Cursive to resolve >defn and >fdef as defn, and >defn- as defn- – this way you get proper highlighting, formatting, error handling, structural navigation, symbol resolution, and refactoring support.

Just place the cursor on >defn, click on the light bulb that appears (or press Alt+Enter) and select Resolve as…​defn.

Specs for multi-arity functions are defined in a similar way. For example, this is what a spec for clojure.core/drop would look like:

(>fdef clojure.core/drop
  ([n]
   [nat-int? => fn?])
  ([n coll]
   [nat-int? (s/nilable seqable?) => seq?]))

Same principle when using >defn with multi-arity functions, just add the function bodies.

ℹ️
Multi-arity functions where the return value specs vary between the different arities are handled correctly using the :fn fspec clause – macroexpand-1 a >defn or >fdef form for details.

Sometimes you need to register an fspec under a keyword in the spec registry for use as part of another spec using (s/def ::keyword (s/fspec …​)).

Ghostwheel handles this by simply passing a qualified keyword to >fdef instead of a symbol:

(>fdef ::nested-fspec
   [i s]
   [int? string? => string?])

The Gspec Syntax

[arg-specs* (| arg-preds+)? => ret-spec (| fn-preds+)? (<- generator-fn)?]

| = :st – such that
=> = :ret – return value, same as in fspec
<- = :gen – generator, same as in fspec

ℹ️
Throughout this guide the symbolic gspec operators =>, | and <- will be used instead of the equivalent keyword-based :ret, :st and :gen. The two sets are perfectly interchangeable and can even be freely mixed within the same gspec.

The number of arg-specs must match the number of function arguments, including a possible variadic argument – Ghostwheel will shout at you if it doesn’t.

arg-specs for variadic arguments are defined as one would expect from standard fspec:

(>fdef clojure.core/max
  [x & more]
  [number? (s/* number?) => number?])
ℹ️

The arg-preds, if defined, are s/and-wrapped together with the arg-specs when desugared.

The fn-preds are equivalent to (and desugar to) spec’s :fn predicates, except that the anonymous function parameter is the ret, and the args are referenced using their symbols. That’s because in the gspec syntax spec’s :fn is simply considered a 'such that' clause on the ret.

? can be used as a shorthand for s/nilable:

(>fdef clojure.core/empty?
  [coll]
  [(? seqable?) => boolean?])

Nested gspecs are defined using the exact same syntax:

(>fdef clojure.core/map-indexed
  ([f]
   [[nat-int? any? => any?] => fn?])
  ([f coll]
   [[nat-int? any? => any?] (? seqable?) => seq?]))

In the rare cases when a nilable gspec is needed ? is put in a vector rather than a list:

(>fdef clojure.core/set-validator!
  [a f]
  [atom? [? [any? => any?]] => any?])
💡
For nested gspecs there’s no way to reference the args in the arg-preds or fn-preds by symbol. The recommended approach here is to register the required gspec separately by using >fdef with a keyword as described in the previous section.
💡
The ghostwheel.specs.clojure.core namespace contains specs for many of the functions in clojure.core. It’s not recommended that you try and instrument it as a whole at this point – there’s a number of ways in which that’s likely to blow up in your face – but it can serve as a good reference on how to write different types of gspecs correctly.
ℹ️
Nested gspecs with one or more any? argspecs desugar to ifn?, so as not to mess up generative testing. This can be overridden by passing a generator – even an empty one, that is simply adding <- or :gen to the gspec – in which case the gspec will desugar exactly as specified.

The assumption here is that any? does not imply that the function can in fact handle any type of argument.

You should still write out nested gspecs, even if they are as simple as [any? => any?] – this is useful as succinct documentation that this particular function receives exactly one argument.
ℹ️

The gspec syntax has a number of advantages:

  • It’s much more concise and easier to write and read (see the comparison in the introduction section above)

  • It’s inline, so you can see at a glance what kind of data a function expects and returns right under the docstring and arg list, for example when previewing the function definition in your editor

  • Renaming/refactoring parameters is a breeze – just use your IDE’s symbol rename functionality and all references in the predicate functions will be handled correctly.

  • You can reliably bypass Ghostwheel temporarily by simply changing >defn to defn - the minimal performance impact of evaluating the gspec vector as the first body form aside, nothing will break because >defn syntax is valid defn syntax.

Testing Specced Functions

Set ::g/check and ::g/num-tests to enable generative testing…​

(ns re-frame-playground.ghostwheel
  #:ghostwheel.core{:check     true
                    :num-tests 10}
  ...)

…​and define a simple function:

(>defn addition
  [a b]
  [pos-int? pos-int? => int? | #(> % a) #(> % b)]
  (- a b))

This will generate the defn, fdef, and testing code for addition, but it won’t actually run the test. Open the Chrome DevTools console, put (g/check) at the bottom of your namespace and save the file.

If you have hot-reloading set up correctly and didn’t get too overzealous fixing bugs in the example code before you were told to, you should get something resembling this:

image 15

Yay! Ghostwheel is already proving invaluable. Fix it by changing (- a b) to (+ a b), save the file, go back to the console, and rejoice:

image 16
💡

You can test any single or multiple functions or namespaces in the REPL or your test-runner by passing the quoted symbols or namespace regex to (g/check). When checking as part of a test-suite make sure to enable ::g/extensive-tests and set ::g/num-tests-ext to a high number.

💡

You can make re-rendering in a ClojureScript hot-reloading workflow dependent on successful test completion. If you’re using Shadow CLJS you can set the after-load hook like this:
:devtools {:after-load-async ghostwheel.core/post-check-async}
And use metadata on the re-render function to add it to the queue:
(defn ^:dev/after-load mount-root [] …​)

The Ghostwheel hook will short-circuit the hook queue if a test fails in any namespace and no re-render will be triggered.

ℹ️
In multi-arity functions each arity is tested as a separate function to ensure adequate test coverage, so a function with 3 arities and ::g/num-tests 5 will have 15 spec checks run against it.

Performance Considerations or How Much Generative Testing Is Enough

Depending on the number and kind of functions in a namespace as well as the dependencies between namespaces, even basic testing on every reload could take long enough to make your fancy hot-reloading workflow useless. The general idea here is to keep ::g/num-tests low enough that the tests complete in a reasonable amount of time, but high enough that you still catch a relatively large number of errors on every run.

ℹ️
Keep in mind that the tests are only executed per namespace reload – whenever (g/check) is called – so if you’re working on some view and hot-reloading its namespace, only the tests defined there (if any) would run. If you change something deep down in a namespace that’s heavily depended on, more namespaces will be reloaded and more tests will run.

Either way – you should not be relying on this alone, especially for functions with complex input and a larger number of parameters. Setup a separate test build config just like you would when writing unit tests, enable ::g/extensive-tests, set ::g/num-tests-ext as high as possible without making your test times unacceptable, and run the whole thing in a CI environment or manually on a regular basis – before coffee breaks, merges to master, releases, etc.

Tweak the ::g/num-tests and ::g/num-tests-ext numbers on a global, namespace and function level as needed and feel free to share what worked for you, so the defaults and recommendations can be improved based on more real world data.

Keeping Track of Side Effects

“These bridges are made from natural light that I pump in from the surface. If you rubbed your cheek on one, it would be like standing outside with the sun shining on your face. It would also set your hair on fire, so don’t actually do it.“
— Erik Wolpaw and Jay Pinkerton, Portal 2

By default functions are considered pure and during compile time Ghostwheel will do its magic to detect potential side effects in any function defined with >defn – calling functions with an ! at the end, do blocks, multiple-form when, let and defn/fn, known unsafe operations, stuff like that – and store the evidence so that it can politely inform you of your transgressions during testing.

It won’t run any automatic generative tests if a function is found to be unsafe, whether it’s due to detected side effects or explicit annotation.

ℹ️
Ghostwheel assumes functions to be (STM- and test-) safe by default, that is – not having unsafe/permanent side effects, which isn’t necessarily the same thing as pure. For the purpose of this guide we will however use the terms interchangeably, to the absolute horror of purists everywhere.

You can disable side effect detection with the ::g/ignore-fx option in which case Ghostwheel will simply trust the name of the function (…​! = unsafe) and behave accordingly.

🔥
If you set ::g/ignore-fx true for an actually unsafe function that has been incorrectly named as safe, and have ::g/check enabled, ::g/num-tests set to > 0 as well as a valid gspec and a call to (g/check) at the bottom of the namespace, generative testing will be performed, side effects and all. This could be bad.
🔥
Side effect detection is a heuristic and in no way fail-safe operation, relying heavily on the assumption that you’re not actively trying to shoot yourself in the foot. That being said, so far it seems to work pretty great in practice, and where it occasionally fails, the likelihood of false positives is significantly higher than that of false negatives so the chances of side effects actually seeping through the cracks and setting your hair on fire are relatively low.

This is pretty much the gist of it – read on for a more detailed description of what all this looks like in practice.

Getting Your Hands Dirty with Side Effects

Let’s take the function we defined in the previous section and map it over a collection of numbers, but make sure you have ::g/check and ::g/num-tests set correctly first.

(>defn addition
  [a b]
  [pos-int? pos-int? => int? | #(> % a) #(> % b)]
  (+ a b))

(>defn increase-numbers
  [increment numbers]
  [int? (s/coll-of int?) => (s/coll-of int?)]
  (map (partial addition increment) numbers))

(g/check)

The two should check out fine:

image 17

We will then decide that it’s a good idea to send an email every time two numbers are added together and modify addition accordingly:

(>defn addition
  [a b]
  [pos-int? pos-int? => int? | #(> % a) #(> % b)]
  (let [result (+ a b)]
    (println "Sending mail with" result "(not really)")
    result))
image 18

So that didn’t go too well. Both addition and its caller increase-numbers fail their checks – addition because of the detected side effects, and increase-numbers because it’s calling the former, the body of which is now replaced with exception-throwing code until the whole messy situation is remedied.

ℹ️
The whole "replaced with exception-throwing code" thing does sound kinda scary, admittedly, but it’s necessary – otherwise, while addition may fail its side effect checks and thus be excluded from testing, increase-numbers would still be happily passing its own, calling addition and sending out mails.

If you’re serious about the impurity, traitor to the Church of Functional Programming that you are, you can make Ghostwheel shut up by renaming your function to suffix it with a ! thus officially marking it as unsafe. Use your IDE to rename addition to addition! now.

image 19

Okay, so it doesn’t quite shut up yet, but it’s for your own good. Even though Ghostwheel is now happy about addition! being correctly marked as unsafe, the infestation of impurity is still actively spreading to its callers!

Worry not – Ghostwheel will help you nip this insidious corruption in the bud. Correctly naming an unsafe function will cause all the previously innocent pure functions, which were calling the now branded offender in good faith, to fail their purity inspections as well and be given a chance for redemption. Go ahead and rename increase-numbers to increase-numbers!.

image 17

Don’t be too quick to breathe a sigh of relief. The checks are fine, but that’s just because all the side-effectful stuff is out in the open – as mentioned above, no generative testing is being done so whether your impure functions are doing what you think they’re doing is anyone’s guess. Not great, but that’s what you get for messing with the dark side.

ℹ️
That being said, some work’s being done to make the testing and stubbing of side-effectful functions easy as well, but we ain’t there yet.

Having recognised the error of your ways, please go ahead and remove the side effect from addition!:

(>defn addition!
  [a b]
  [pos-int? pos-int? => int? | #(> % a) #(> % b)]
  (let [result (+ a b)]
    result))
image 20

To preserve the balance in the universe, purity can spread just as efficiently as its sinister counterpart – if you remove side effects from a function, Ghostwheel will warn you if it’s still marked as unsafe and as soon as you rename it to remove the bang, it will now show the same warning for its potentially purified callers, and so on, until harmony is restored. Once you’ve renamed increase-numbers! as well, this should be the result:

image 17

This is nice. You can relax now. If any false positives/negatives come up, just add ::g/ignore-fx true to the function metadata to disable side effect detection and open an issue on github to help improve it.

Evaluation Tracing and Program Observability

In fact, the mere act of opening the box will determine the state of the cat, although in this case there were three determinate states the cat could be in: these being Alive, Dead, and Bloody Furious.
— Terry Pratchett, Lords and Ladies

Specs are all nice and good, but often enough we want to take a peek at what’s going on under the hood while it’s going. Set the ::g/trace option to anything from 0 to 5 to determine the trace verbosity and performance impact, and you’re good to go.

Table 1. Ghostwheel trace levels
Trace level What gets traced (additive)

0

Nothing

1

Function call without any data

2

Function I/O

3

Local bindings

4

Threading macros

5

Named anonymous functions

6

All anonymous functions

💡
Use |> or tr (they are aliases) to wrap any expression for more granular tracing or if you are only interested in Ghostwheel’s tracing functionality. They work standalone, default to trace level 5 and will generally do the right thing depending on the type of wrapped form – functions and threading macros get proper in-depth tracing, while other expressions are pretty-printed and returned.
💡
::g/trace true is equivalent to ::g/trace 5, so you can just add the ^::g/trace metadata to the function name.
💡

A great workflow for working on a function is enabling the trace and passing a callback to g/check to have the function automatically called on namespace reload after the checks have completed successfully:

(>defn ^::g/trace foo
  [a b]
  [int? int? => (s/coll-of number? :kind vector?)]
  (let [c (inc b)
        d (-> a (* 2) (- 20))]
    [(+ a b) (+ c d)]))

(g/after-check #(foo 2 4))
(g/check `foo)

For this to work you’ll also have to set your build system hooks correctly.

This way you immediately see data flowing through the function on file save after every change (or the check results, if you messed up). Take a moment to zen out and revel in the intoxicating sense of power.

ℹ️
If you don’t like the painstakingly selected default shade of blue-violet, you can change it with the ::g/trace-color option. Philistine.

Contributions and Roadmap

“I USHERED SOULS INTO THE NEXT WORLD. I WAS THE GRAVE OF ALL HOPE. I WAS THE ULTIMATE REALITY. I WAS THE ASSASSIN AGAINST WHOM NO LOCK WOULD HOLD.“
“Yes, point taken, but do you have any particular skills?“
— Terry Pratchett, Mort

The blood of generations of LISPers is coursing through your veins? You’ve howled naked at the moon in arcane rituals ordained by the dark forces you summoned in order to gain your abilities? At this point you don’t even see the parens?

Or maybe you just like breaking things and telling people about it. Either way, there’s enough work to go around. First and foremost:

  • Put it to use and report any issues you run into

  • Submit PRs with gspecs for external libraries to the ghostwheel.specs repo

Other than that, here’s the rather loose roadmap, not necessarily sorted by priority or particularly rich on detail. PRs are welcome if anything should tickle your fancy (or annoy the hell out of you), but if you are planning on doing anything bigger maybe open an issue first so we can discuss it.

  • Automatically re-run failed gen-tests with tracing enabled.

  • Enable spec-based generation of safe stubs for gen-testing unsafe functions

  • Solve miscellaneous issues around fully instrumenting the ghostwheel.specs.clojure.core namespace during testing

  • Get the Closure type annotations working properly

  • Integrate bhauman/spell-spec

Hacking on Ghostwheel

The quickest and easiest way is probably to use Shadow CLJS and copy an external namespace into the src directory with the correct directory structure – it will then override whatever’s on the classpath. See the Shadow CLJS guide for details.

For a more solid environment - setup a playground project with something like re-frame, link your Ghostwheel repo under the checkouts folder and add checkouts/ghostwheel/src to the source path. This way Shadow CLJS will watch the Ghostwheel namespaces for changes as well and hot-reload accordingly.

A similar setup should be possible with Figwheel as well – feel free to contribute documentation for that if you’re using it.

For debugging the code generating functions in ghostwheel.core there’s a code block at the bottom of the namespace which you can use to trace them at runtime in ClojureScript. Some of the symbol generation here and there can trip it up, but it generally works quite well.

FAQ

  • Q: Can I trust Ghostwheel not to break my code?

    A: Every build is extensively tested with a combination of manually written and generated tests for a large number of configuration option combinations (including all tracing levels) with production and development build configurations in three environments - Clojure, node, and headless Chrome. The generated code is evaluated to make sure it behaves exactly like the code that went in and the fspec generation is tested with a number of convoluted gspecs to make sure everything desugars as expected.

    In production mode with Ghostwheel disabled, the gspec vectors are simply stripped from the >defn blocks and a plain defn is generated, independent of any other configuration. There are less than 20 lines of Ghostwheel code involved in this scenario and they are also unit-tested to ensure that the produced defn is identical to the >defn minus the gspecs. >fdef and g/check simply output nil.

    Test coverage for the somewhat less critical parts (testing, instrumentation, etc.) is not yet 100% but getting there.

    Purely cosmetic bugs in tracing and reporting are more difficult to test and thus more likely.

    All that being said, Ghostwheel is Alpha software and you should proceed with care, especially on Clojure where it’s even more Alpha.

  • Q: Can I use Ghostwheel for test generation with existing fspecs defined with s/fdef?

    A: Adding support for this was considered and ultimately decided against for the time being. It would add complexity and a maintenance workload in order to enable the use of a small subset of Ghostwheel’s functionality in a subpar manner, because there’s a number of checks and validations Ghostwheel cannot perform without the inline gspecs, and some aspects of its behaviour would change as well.

    You can still use Ghostwheel with nil gspecs and take advantage of the side effect detection, tracing functionality and easy instrumentation. Extracting the test generation code into a separate library may be considered further down the line.

    What is on the roadmap however, is the ability to convert fspecs to gspecs for an easier migration of existing code bases.

  • Q: What does tracing have to do with testing and why is it not a separate project?

    A: Primarily because tracing needs to be aware of the automated testing so as not to interfere with it. That aside, I rely heavily on both spec-checking and tracing in my own workflow and like having the UI tightly integrated like this.

    If you’re only interested in tracing you can use >defn with nil gspecs and the default configuration plus a per function ^::g/trace. There are also some vague plans in the works to involve tracing in the testing process, but that’s still taking shape.

  • Q: How are >defn and >fdef pronounced in conversation?

    A: Ghostwheel-def-n / g-def-n and ghostwheel-f-def / g-f-def respectively.

  • Q: Why not use a statically typed language?

    A: Not touching that one with a ten foot pole.

Acknowledgements, Inspiration and Prior Art

Some other projects and people without which/whom it likely wouldn’t exist in its current form or at all, in no particular order:

plumatic’s schema for offering a glimpse into the future of generative testing for quite some time before spec was introduced;

Bruce Hauman’s Figwheel and Thomas Heller’s Shadow CLJS – for providing robust hot-reloading which is essential to ClojureScript development and to the Ghostwheel experience in particular;

BinaryAge’s CLJS DevTools, without which ClojureScript tracing and data inspection would be a lot less fun;

Philip Kamenarsky for introducing me to Clojure and Haskell, providing valuable feedback during the development of Ghostwheel, and many insightful conversations about some of the concepts that inspired it;

David Nolen for his initial work and documentation on integrating Google Closure type checking and his work on ClojureScript in general;

And last but not least, our cherished BDFL and his minions, working tirelessly to bestow upon us the magic of Clojure, without which Ghostwheel would be somewhere between significantly more difficult to write and plain impossible.

Rationale

The demon hath return’d from the darkest depths of the underworld, whither he was banish’d when he dared raise his crooked hand against the Macro. His soul – wrapp’d in shadows, his mind – clouded, full of evil and despair. He is the AntiLISP. He speaketh with a twisted tongue and casteth confusion with his words – sweet and cunning – about types and proofs.

Hearken! Raise your armies! Sharpen your parens and gather your bravest heroes! War is upon us.
— Unknown historian, The Clojure Chronicles

Clojure is beautiful. The simplicity, clarity, flexibility and immediacy of it; immutable data, macros, the powerful REPL, paredit/parinfer, STM, the list goes on.

On the other hand, the lack of easy, comprehensive type verification before spec came along, sometimes meant frustrating time spent hunting down pointless runtime exceptions – with stack traces and error messages ranging from not particularly helpful to openly mocking – and less willingness to do major refactoring for justified fear of breaking something not immediately visible and painful to debug.

When making changes to any medium sized codebase, one could, despite being careful and having the best of intentions, end up with a less than stellar experience.

In this context spec is a huge step forward and elevates Clojure onto a whole new level of robustness, maintainability, and painless usability. And it does so the Clojure way - by providing simple and powerful tools, making their application easy and natural, and getting out of the way, leaving it up to the programmer to decide how and to what extent they want to use them.

When it comes to defining function specs, however, it’s quite verbose and the actual day to day usage is a little rough around the edges. The gspec syntax was born as a solution, taking inspiration from some static type systems and mathematical notation, while staying Clojure-like enough to be a seamless fit.

Generating the nil-body stubs and automatically defining the tests naturally followed from there, which in turn inspired the heuristic side effect detection to serve as a safeguard against inadvertently doing I/O during testing and to provide additional insight into the code by helping keep unsafe operations explicit.

It is my hope that Ghostwheel will help lower the barrier to using spec and contribute to its wider adoption, which will reduce the need for writing unit tests to a minimum and generally do wonders for overall Clojure code quality.

While spec provides the ability to quickly track down many type and logic errors, it doesn’t remove the need to observe the function in operation, as a tool for both debugging and thinking. Common techniques for achieving this include:

  • running code fragments in the REPL for parts of a function that one wants to see in action, which can force one to create intermediate mock data (which may or may not be an accurate representation of the original environment) or to break functions and bindings apart beyond the point where it would make sense from a complexity/readability perspective;

  • interspersing logging statements throughout the code and sometimes forgetting them there or breaking something along the way and introducing weird bugs, not to mention the hassle of adding and removing them while trying to zero in on the point of interest;

  • setting breakpoints and using a step-by-step debugger, which can be quite akin to trying to take in a landscape through a straw.

Compared to these, seeing the data as it flows through each part of a function at a glance in a tree of evaluated expressions is quite a bit more efficient, enjoyable, and conducive to thinking about the operations and architecture involved on a higher level.

Essentially, Ghostwheel is about reaching a higher state of flow by removing the barriers between your mind and your code, and taking a lot of pedestrian busywork off your shoulders to put it where it belongs – with the computer.

Now go forth and create, fellow maker. Use your new-found powers for good.

Copyright (c) 2019 George Lipov
Licensed under the Eclipse Public License 2.0

ghostwheel's People

Contributors

gnl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ghostwheel's Issues

:check-coverage reporting false-positive for declared >defn

declare calls seem to trip up coverage checking. When compiled, the following reports that "a" is not declared with >defn.

(ns example.core
  (:require [ghostwheel.core :as g, :refer [>defn =>]]))

(declare a)

(>defn a [x] [int? => int?]
  (* x 2))

(g/check)

inline test spec

would there be a way to write inline tests with ghostwheel?

as an example, there's a clojureverse post of someone using defn-spec for inline tests; here's the code snippet:

(defn abs
  "The absolute value of a number."
   {:test (fn []
            (is (= (Math/abs 5) 5))
            (is (= (Math/abs -2) 2))
            (is (= (Math/abs 0) 0)))}
  [x]
  {:pre [(number? x)]}
  (if (pos? x)
    x
    (- x)))

(though I'd personally prefer if the test spec came after the function body, rather than before it, if possible)

Does ghostwheel validate return values?

I'm having trouble validating the return value of a function when it's called.

I have a function like:

(>defn foo [x] [int? => int?]
  "foobar")

When I call it like this:

(foo "x")

I get spec error about the argument as expected. However if I make the argument conform:

(foo 1)

I don't get any error about the invalid return value. I do, however, get errors about the return value if I run the generative tests.

Is this expected behavior?

Ability to override config

It would be useful to override the configuration when running checks, as I would like to run checks as part of a test suite. Maybe some kind of dynamic var that is nil by default but gets merged in inside the merge-config file?

Non-conforming function call does not show reason

Wow, ghostwheel is really great, thanks!

One thing that I don't get to work is this:

(>defn add-positives
  [a b]
  [pos-int? pos-int? => int?]
  (+ a b))

Calling (add-positives -1 1) rightfully detects a non-conforming argument, but does not show which argument did not conform and why:

Error: Call to #'shadow-cljs-browser-template.core/add-positives did not conform to spec.
    at new cljs$core$ExceptionInfo (http://localhost:8080/cljs-runtime/cljs.core.js:37198:10)
    at Function.cljs$core$IFn$_invoke$arity$3 (http://localhost:8080/cljs-runtime/cljs.core.js:37259:9)
    at Function.cljs$core$IFn$_invoke$arity$2 (http://localhost:8080/cljs-runtime/cljs.core.js:37255:26)
    at http://localhost:8080/cljs-runtime/cljs.spec.test.alpha.js:140:25
    at http://localhost:8080/cljs-runtime/cljs.spec.test.alpha.js:170:8
    at G__48736__delegate (http://localhost:8080/cljs-runtime/cljs.spec.test.alpha.js:180:5)
    at G__48736 (http://localhost:8080/cljs-runtime/cljs.spec.test.alpha.js:200:27)
    at eval (eval at <anonymous> (http://localhost:8080/cljs-runtime/shadow.cljs.devtools.client.browser.js:823:8), <anonymous>:1:49)
    at http://localhost:8080/cljs-runtime/shadow.cljs.devtools.client.browser.js:823:8
    at Object.shadow$cljs$devtools$client$env$repl_call [as repl_call] (http://localhost:8080/cljs-runtime/shadow.cljs.devtools.client.env.js:141:108)

My namespace is declared like this:

(ns shadow-cljs-browser-template.core
  #:ghostwheel.core{:check     true
                    :num-tests 10
                    :instrument true
                    }
  (:require
    [ghostwheel.core :as g :refer [>defn >defn- >fdef => | <- ?]]
    [ghostwheel.tracer]
    [clojure.pprint :as pp]))

Am I missing something?

shadow-cljs -- Use of undeclared Var com.rpl.specter/java

When requiring ghostwheel.core, shadow-cljs prints out the following warning after compilation.

------ WARNING #1 --------------------------------------------------------------
 File: com/rpl/specter.cljc:1272:19
--------------------------------------------------------------------------------
1269 |           ns (namespace structure)]
1270 |       (cond (keyword? structure) (keyword ns new-name)
1271 |             (symbol? structure) (symbol ns new-name)
1272 |             :else (i/throw-illegal "NAME can only be used on symbols or keywords - " structure)
-------------------------^------------------------------------------------------
 Use of undeclared Var com.rpl.specter/java
--------------------------------------------------------------------------------
1273 |             ))))
1274 |
1275 | (defnav ^{:doc "Navigates to the namespace portion of the keyword or symbol"}
1276 |   NAMESPACE
--------------------------------------------------------------------------------

------ WARNING #2 --------------------------------------------------------------
 File: com/rpl/specter.cljc:1285:19
--------------------------------------------------------------------------------
1282 |           new-ns (next-fn (namespace structure))]
1283 |       (cond (keyword? structure) (keyword new-ns name)
1284 |             (symbol? structure) (symbol new-ns name)
1285 |             :else (i/throw-illegal "NAMESPACE can only be used on symbols or keywords - " structure)
-------------------------^------------------------------------------------------
 Use of undeclared Var com.rpl.specter/java
--------------------------------------------------------------------------------
1286 |             ))))
1287 |
1288 | (defdynamicnav
1289 |   ^{:doc "Adds the result of running select with the given path on the
--------------------------------------------------------------------------------

I assume it has something to do with redplanetlabs/specter#267.

Disable side effect checking for print statements

I sometimes use print statements when debugging, and it is very annoying to have to manually disable side effect checking or rename functions every time i want to add a print statement to a ghostwheel >defn. Is there a way to globally disable side effect checking for certain statements, or just globally disable it?

Instrumentation breaks when using ghostwheel

I gave up trying to use ghostwheel, because I had issues with instrumentation breaking. Didn't take the time to create a reproducable example, since @gnl seemed gone. Now that you're back, I figured I'd create a repro. Here it is: https://github.com/Saikyun/gw-breaks-instrument

Function call that should break: (ranged-rand 10 0)

Cut from the readme in the repro:

Expected result is both (with gw/check and with st/instrument) throwing an error, because I'm erronously calling ranged-rant
with start higher than end. However, this is what I get:

(TLDR; with ghostwheel, no exception, without ghostwheel, I get the expected exception)

~/programmering/clojure/test-ghostwheel $ node out/gw.js 
Checking starter.ghostwheel ...
Passed all 1 checks
4
shadow-cljs - #6 ready!
interrupt: 2
~/programmering/clojure/test-ghostwheel $ node out/spec.js 
SHADOW import error /Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/shadow.module.main.append.js

/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/core.cljs:11332
(defn ^{:jsdoc ["@constructor"]}
^
Error: Call to #'starter.spec/ranged-rand did not conform to spec.
    at new cljs$core$ExceptionInfo (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/core.cljs:11332:1)
    at Function.cljs$core$IFn$_invoke$arity$3 (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/core.cljs:11361:1)
    at Function.cljs$core$IFn$_invoke$arity$2 (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/core.cljs:11361:1)
    at conform_BANG_ (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/spec/test/alpha.cljs:92:24)
    at conform_BANG__STAR_ (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/spec/test/alpha.cljs:87:3)
    at Object.G__42922__delegate (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/spec/test/alpha.cljs:118:19)
    at Object.G__42922 [as ranged_rand] (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/spec/test/alpha.cljs:116:22)
    at Function.starter$spec$init (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/starter/spec.cljs:17:1)
    at Function.cljs$core$IFn$_invoke$arity$2 (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/core.cljs:3912:8)
    at Function.cljs$core$IFn$_invoke$arity$2 (/Users/jona/programmering/clojure/test-ghostwheel/.shadow-cljs/builds/node-spec/dev/out/cljs-runtime/cljs/core.cljs:3948:6)

If I try to add a call to clojure.spec.test.alpha/instrument manually in the ghostwheel code, I get an compilation error:

[:node-gw] Build failure:
------ ERROR -------------------------------------------------------------------
 File: /Users/jona/programmering/clojure/test-ghostwheel/src/starter/ghostwheel.cljs:15:1
--------------------------------------------------------------------------------
  12 |   (println (ranged-rand 10 0)))
  13 | 
  14 | (g/check)
  15 | (st/instrument)
-------^------------------------------------------------------------------------
Error in phase :compilation
Unable to resolve var: >defn- in this context at line 15 starter/ghostwheel.cljs
--------------------------------------------------------------------------------

Empty error messages in browser console

Description :octocat:

Empty Ghostwheel error message appears in browser console.

Reproduction guide 🪲

Following the ReadMe:

  • add [gnl/ghostwheel "0.2.3"] to project.clj (using Figwheel).
  • add #:ghostwheel.core{:check true, :num-tests 10} to the namespace.
  • add the following code taken from the ReadMe:
(>defn addition
  [a b]
  [pos-int? pos-int? => int? | #(> % a) #(> % b)]
  (+ a b))
  • add (ghostwheel/check) at the end of the file.

Observed behaviour: 👀 💔
ghostwheel-no-error
(NOTE: foo and ranged-rand also from the docs, I just don't add them here)

Expected behaviour: ❤️ 😄
no-error
(NOTE: Disregard the number of checks, it's just a pic from the ReadMe)

Use metadata key for fn-body spec generation.

This is an issue that started out as a comment to #26

Existing Feature

Proposed Problem (1)

  • Effortless spec-based stub generation in nil-body functions for rapid prototyping is a potentially useful tool
  • Currently this is enabled by default, which is a jarring experience if one is not aware of this feature; as explained in #26 (comment)

Proposed Solution (1)

  1. Improve documentation: the feature is mentioned in the readme #introduction, but that would also be a good place to mention (or link to) how to disable/enable it.
  2. The feature is not mentioned :ghostwheel.core/ghostwheel-config nor ghostwheel.utils/ghostwheel-default-config
  3. Honestly, as a user - I am still not sure if and how I can disable this feature.
  4. It's a jarring experience if one is not aware of it, so make it opt-in via configuration.

Alternative Feature

Once I started thinking about this problem, I'd like to suggest an alternative approach.

Proposed Problem (2)

  • Low-effort spec-based stub generation of functions for rapid prototyping is a potentially useful tool
  • The principle of least-surprise is important to Clojure developers
  • Configuration is global on/off, while syntax can be applied locally
  • The road from "cool dev tool" to "potential bug in production" is paved with good intentions (i.e. how easy is it for dev to accidentally not implement a function, but have it "work" during dev/testing?)

Proposed Solution (2)

Instead of global enabled/disabled feature of nil-body functions, we could configure it via metadata (similar to ::g/trace). That way:

  • requires no configuration
  • is opt-in for any function
  • concise enough to easily type (small overhead vs an implicit global configuration)
  • explicit in what is not yet implemented (easy to grep in codebase, etc.)
(>defn ^::g/fake some-string-to-int
  [x] [string? => int?]
  nil)

We could even take this even a step-further and remove the nil-body constraint. Any time we add the metadata key ::g/fake to a function, it will simply ignore the body and generate random data from spec. Possible use-cases for this? Stubbing out in development an I/O function, etc.

What we call the metadata key itself (::g/fake) is open to discussion. I'm just wondering if this idea has any legs. Thoughts?

Check `>fdef`s in ns, or provide something similar

It's not always feasible to write the entire spec inside the >defn, since the assertion predicates might be fairly involved and can end up being as big as the function itself.

Attempts to move it a little further from the definition via >fdef leads to it no longer being checked by ghostwheel.

It would be great if >fdef or something like it would be checked on (g/check).

Spec assertion failed\n:js-console - failed: set?

Every now and the I get the following error when evaluating a function:

Spec assertion failed\n:js-console - failed: set? in: [:ghostwheel.core/report-output]

It seems to fix itself when I restart the repl.

This is in clojure files with the following header:

  #:ghostwheel.core{:check false :num-tests 10 :outstrument true}

It also gets fixed when I add :report-output #{:repl} to the config.

Getting `core.cljs:3868 Uncaught TypeError: Illegal invocation` in Electron Clojurescript project

Description :octocat:

Adding Ghostwheel to Electron Clojurescript project produces error in browser console that prevents Ghostwheel logging.

With this project.clj config (only related parts):

  :dependencies [[org.clojure/clojure "1.9.0"]
                 [org.clojure/clojurescript "1.10.339"]
                 [gnl/ghostwheel.tracer "0.3.5"]
                 [gnl/ghostwheel "0.3.5"]
                 [cljsjs/nodejs-externs "1.0.4-1"]
                 [reagent "0.8.1"]]

  :plugins [[lein-cljsbuild "1.1.7"]]

  :min-lein-version "2.5.3"

  :cljsbuild {:builds {:app {:source-paths ["src/cljs"]
                             :compiler {:output-to     "app/js/p/app.js"
                                        :output-dir    "app/js/p/out"
                                        :asset-path    "js/p/out"
                                        :optimizations :none
                                        :pretty-print  true
                                        :cache-analysis true}}}}

  :clean-targets ^{:protect false} [:target-path "out" "app/js/p"]

  :figwheel {:css-dirs ["app/css"]}
  :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}

  :profiles {:dev {:cljsbuild {:builds {:app {:source-paths ["env/dev/cljs"]
                                              :compiler {:source-map true
                                                         :main       "skarbnik.dev"
                                                         :verbose true
                                                         :preloads [devtools.preload]
                                                         :external-config {:ghostwheel {}}}
                                              :figwheel {:on-jsload "skarbnik.core/mount-root"}}}}
                   :source-paths ["env/dev/cljs"]

                   :dependencies [[figwheel-sidecar "0.5.17"]
                                  [binaryage/devtools "0.9.10"]
                                  [cider/piggieback "0.3.1"]]

                   :plugins [[lein-ancient "0.6.8"]
                             [lein-kibit "0.1.2"]
                             [lein-cljfmt "0.4.1"]
                             [lein-figwheel "0.5.17"]]}

Reproduction guide 🪲

  • Creating project with lein new electron <name>.
  • Bumping up versions of Clojure and dependencies as per project.clj above.
  • Do all steps to run Cljs-electron app as in electron template instructions.
  • Add Ghostwheel to core.cljs and some example function such as:
(>defn addition
      
       [a b]
       [pos-int? pos-int? => int? | #(> % a) #(> % b)]
       (- a b))
  • Add (g/check) at the bottom of the core.cljs.
  • run lein figwheel and in another terminal grunt launch, that should start app.

Observed behaviour: 👀 💔
uncaught-type-error

Expected behaviour: ❤️ 😄
Ghostwheel's report.

Invalid regular expression

I ran into this seemingly obscure exception when trying to set up ghostwheel.

Uncaught SyntaxError: Invalid regular expression: /[\s    - 

 â�Ÿã€€]+/: Range out of order in character class

It turned out that my html document was missing a <meta charset="utf-8"> in the <head> tag.

I'm just posting this for posterity in case anyone stumbles onto the same issue.

A strange problem: function returning nil instead returns random numbers

Hi, @gnl — 

Keep up all the amazing work. Just ran into the strange problem... Here's a function without ghostwheel...

(defn should-return-nil
  [x]
  nil)
=> #'x.asin/should-return-nil
(should-return-nil "")
=> nil
(should-return-nil "")
=> nil

And here's the same function with ghostwheel — it returns seemingly random numbers. I'm wondering if I'm doing something wrong?

(>defn should-return-nil-gw
  [x] [string? => int?]
  nil)
=> [x.asin/should-return-nil-gw]

; this fails, as expected...
(should-return-nil-gw 12)
Syntax error (ExceptionInfo) compiling at (asin.clj:23:3).
Call to amazonreviews.asin/should-return-nil-gw did not conform to spec:
12 - failed: string? in: [0] at: [:args :x]

; this has surprising return value
(should-return-nil-gw "")
=> -6922
(should-return-nil-gw "")
=> 0
(should-return-nil-gw "")
=> -31855
  1. Shouldn't the return value fail the spec, as it is not nilable?
  2. Why is it returning seemingly random numbers?

Thank you!
Gene

No reason shown for why a function failed check

When checking my project, I see this in my browser console:

image

Based on the README for this project, I would expect to see some details after the Call: in the above log. Am I missing something? I'm not sure how to debug from here.

This might be related to #20.

Test only changed functions

Does it runs generative tests for every specced function in a namespace or only for what's changed? Haven't tried it yet, but how does it impacts perf when everything gets checked on every code reload?
Thanks!

s/cat arg spec behaving incorrectly?

I have a function that expects a pair of keywords in one of the arguments, and I'm having a hard time getting a ghostwheel spec working. Here is a minimal repro:

(>defn foo
  [x]
  [(s/cat :a keyword? :b keyword?) => any?]
  (first x))
=> #'test/foo__ghostwheel-test

(foo [:x :y])

Execution error - invalid arguments to test/foo at (form-init2634038421333083665.clj:1).
form-init2634038421333083665.clj:1
-- Spec failed --------------------
Function arguments
  ([:x :y])
   ^^^^^^^
should satisfy
  keyword?
-------------------------
Detected 1 error

Am I misunderstanding something here?

I can't for the life of me figure out how to get tracing to work

With this code:

(ns showcase.ghostwheel
  (:require
   [ghostwheel.tracer]
   [ghostwheel.core :as g :refer [>defn | =>]]))

(>defn ^::g/trace 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)))))

(g/check)

And the following in my shadow-cljs.edn:

{:compiler-options
  :ghostwheel 
  {:check true 
   :num-tests 10 
   :outstrument true
   :report-output :repl}}

The automatic tests works great! So thanks for that.

However, I can't figure out how to get tracing to work. Am I missing anything?

In the readme, there's an example that looks like this:

(g/check #(foo 2 4))

But when I try similar code, I get a spec error, saying that g/check won't take that input.

>defmethod

We currently need to create seperate >defns and delegate to them. Adding >defmethod macros would ease this.

NullPointerException when a >defn calls another >defn with invalid arguments

I accidentally hit enter before I was ready, so apologies to anyone who may have gotten a confusing email notification.

I am getting a NullPointerException related to clojure.string/split-lines when a >defn is being generatively tested, and calls another function with arguments that don't match the spec. I find myself constantly hitting this. Instead of a concise error message I get pages and pages of nested stack traces.

Here is my minimal reproduction.

(>defn inner
       [input]
       [int? => string?]
       (str input))

(>defn outer
       [input]
       [string? => string?]
       (inner input))

(g/check)

It appears the error is coming from line 180 in reporting.cljc. Here is a snippet of the stack trace:

{:file "Matcher.java",
 :line 1770,
 :type :error,
 :expected
 (clojure.core/and
  (clojure.core/every?
   (fn*
    [p1__2441__2443__auto__]
    (clojure.core/->
     p1__2441__2443__auto__
     :clojure.spec.test.check/ret
     :pass?))
   spec-checks__2442__auto__)
  true
  true),
 :actual #error {
 :cause nil
 :via
 [{:type java.lang.NullPointerException
   :message nil
   :at [java.util.regex.Matcher getTextLength "Matcher.java" 1770]}]
 :trace
 [[java.util.regex.Matcher getTextLength "Matcher.java" 1770]
  [java.util.regex.Matcher reset "Matcher.java" 416]
  [java.util.regex.Matcher <init> "Matcher.java" 253]
  [java.util.regex.Pattern matcher "Pattern.java" 1135]
  [java.util.regex.Pattern split "Pattern.java" 1263]
  [java.util.regex.Pattern split "Pattern.java" 1336]
  [clojure.string$split invokeStatic "string.clj" 224]
  [clojure.string$split_lines invokeStatic "string.clj" 228]
  [clojure.string$split_lines invoke "string.clj" 228]
  [ghostwheel.reporting$report_spec_check invokeStatic "reporting.c
ljc" 180]
  [ghostwheel.reporting$report_spec_check invoke "reporting.cljc" 163]
  [ghostwheel.reporting$eval2004$fn__2005 invoke "reporting.cljc" 208]
  [clojure.lang.MultiFn invoke "MultiFn.java" 229]
  [clojure.test$do_report invokeStatic "test.clj" 357]
  [clojure.test$do_report invoke "test.clj" 351]
  [ghostwheel_repr.core$fn__11380 invokeStatic "core.clj" 12]
;; stack trace continues into clojure internals...
]
;; other properties elided
}

Tracing support for Clojure

The tracing support is pretty awesome, is the clojure support being worked, is help needed? On that note didn't see a repo for ghostwheel.tracing

Make macros easier to use

You example currently uses :refer-macros for CLJS. This is not necessary with one tiny tweak.

(:require [ghostwheel.core :as g
            ;; `?` is an optional shortcut for `s/nilable`
            :refer-macros [>defn >defn- >fdef ?]
            ;; Optional - you can use `:ret`, `:st` and `:gen` instead.
            :refer [=> | <-]])

Inside ghostwheel.core you only need to add a (:require-macros [ghostweel.core]) for CLJS. The compiler will then figure out when to use a macro on its own so the user only has to :refer and never :refer-macros.

(:require [ghostwheel.core :as g
            ;; `?` is an optional shortcut for `s/nilable`
            ;; Optional - you can use `:ret`, `:st` and `:gen` instead.
            :refer [>defn >defn- >fdef ? => | <-]])

Macro idea for async instrumentation: >defn-go

I have a lot of core.async in my (CLJS) app. I guess spec + core.async is a very big topic, and at some point the Clojure community will probably come up with a powerful approach. In the mean time, this is more of a low-hanging-fuit idea that could be very helpful.

In async code there are a lot of functions where the top level form is (go ...). Often the return value is discarded, but sometimes not, e.g. an async function to fetch a value from a server via HTTP.

The normal return spec is pretty much useless, as such functions always return a channel.

What you really want is to be able to spec the (single) value that is sent over the returned channel. From an instrumentation point of view, this could be done if we lift the (go ...) to be part of the macro that defines the function (and the specs), i.e. something like this:

(>defn-go my-fn 
  [...args]
  [...arg-specs => ret-spec]
  ...body)

which, when instrumented, would expand to something along the lines of

(defn my-fn
  [...args]
  ; check passed args against arg-specs (as usual)
  (go
    (let [result# ...body]
      ; check result# against ret-spec
      result#)))

I'm intending on having a crack at implementing this macro myself. Any advice would be greatly appreciated. If it sounds like something that could be a contribution, so much the better.

Instrumentation?

I cannot for the life of me actually get instrumentation to turn on.

I tried putting #:ghostwheel.core{:instrument true} in my namespace and I tried in the function itself:

(>defn gtest
  ^{:g/instrument true}
  [myint]
  [int? => any?]
  (print myint))

But if I pass a string I don't get an error. Am I missing something here?

Call to ghostwheel.core/>defn did not conform to spec.

This example:

(ns app
  (:require
   [spec-tools.data-spec :as ds]
   [ghostwheel.core :as g :refer [>defn >defn- >fdef => | <- ?]]))

(def significant-correlations
  (ds/spec
   {:input keyword?
    :score int?
    :average float?
    :correlations {:biomarker keyword?
                   :slope float?
                   :rsq float?
                   :datapoints int?}}))

(>defn make-html
       [data]
       [significant-correlations :ret string?]
       "")

Gives me this error when I try to compile:

Syntax error macroexpanding ghostwheel.core/>defn.
Call to ghostwheel.core/>defn did not conform to spec.
-- Spec failed --------------------

  (... ... ... [significant-correlations ... ...] ...)
                ^^^^^^^^^^^^^^^^^^^^^^^^

should be one of: (quote =>), :ret

or value

  (... ... ... [significant-correlations :ret string?] ...)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

should satisfy

  nil?

I'm having a really hard time trying to figure out how to fix this, any ideas?

>fdef doesn't work for me

I did something like this:

(ns app.foobar.test
  (:require
   [ghostwheel.core :refer [>fdef =>]]
   [app.foobar.hocs :as hocs]))

(>fdef
 hocs/my-func
 [arg]
 [int? => int?])

My config:

                            {:ghostwheel
                             {:check         true
                              :outstrument   true
                              :num-tests-ext 240
                              :ignore-fx     true}}

What am I doing wrong?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.