Skip to content
/ piggy Public

Test for spec compatibility and breaking changes.

License

Notifications You must be signed in to change notification settings

sgepigon/piggy

Folders and files

NameName
Last commit message
Last commit date

Latest commit

e7828fc · Nov 25, 2019

History

85 Commits
Dec 18, 2018
Aug 1, 2018
Nov 18, 2019
Nov 17, 2019
Nov 25, 2019
Aug 1, 2018
Aug 1, 2018
Nov 25, 2019
Nov 25, 2019
Oct 14, 2019
Oct 14, 2019

Repository files navigation

piggy

“Ralph made a step forward and Jack smacked Piggy’s head. Piggy’s glasses flew off and tinkled on the rocks. Piggy cried out in terror: ‘My specs! One side’s broken.”

— William Golding, Lord of the Flies, Chapter 4

piggy is a Clojure library that helps with broken specs.

Project Status

Will be in alpha as long as clojure.spec is in alpha.

Installation

Download from https://github.com/sgepigon/piggy.

Usage

Let's say we're writing a spec for clojure.core/+. We might start out like this:

(ns piggy.demo.readme
  (:require
   [clojure.spec.alpha :as s]
   [piggy.combinators.alpha :as pc]))

(+ 2 2)
;; => 4

(s/fspec :args (s/cat :x int? :y int?)
         :ret int?)

;; `s/fspec` returns the function itself on a successful conform
(s/conform (s/fspec :args (s/cat :x int? :y int?)
                    :ret int?)
           +)
;; => #function[clojure.core/+]

;; Can also use `s/explain` to print a human readable message:
(s/explain (s/fspec :args (s/cat :x int? :y int?)
                    :ret int?)
           +)
;; => Success!

We think about it a little more and realize + is variadic:

(+) ; => 0
(+ 1) ; => 1
(+ 0 1 2 3 4 5 6 7 8 9) ; => 45

We can use s/* for zero or more ints.

(s/explain (s/fspec :args (s/* int?)
                    :ret int?)
           +)
;; => (-8782045379102980082 -441326657751795727) - failed: integer overflow

Two things are going on here:

  1. Because we switch from a 2-arity—(s/cat :x int? y: int?)—to variadic—(s/* int?)—we're hitting integer overflow.

  2. Turns out clojure.core/+ doesn't auto-promote. But there is a built-in Clojure function, clojure.core/+', that does arbitrary precision.

    So let's try this with +'.

(s/explain (s/fspec :args (s/* int?)
                    :ret int?)
           +')
;; => 9223372036854775808N - failed: int? at: [:ret]

We can see it auto-promotes but now our return spec is wrong. We also realize +' takes all sorts of numbers.

(+ -1 3.14 22/7 0x77)
;; => 124.28285714285714

Let's update int? to number?

(s/explain (s/fspec :args (s/* number?)
                    :ret number?)
           +')
;; => Success!

Cool, we're more comfortable with this spec. How do we know change isn't a breaking change?

compat and fcompat are spec combinators that encodes a compatibility property between two specs.

compat is for simple comparsions over specs and predicates while fcompat is used to compare compatibility between functions. s/spec is to compat as s/fspec is to fcompat.

The combinators are is a spec themselves, so the same functions that work on clojure.spec specs (s/conform, s/unform, s/explain, s/gen, s/with-gen, and s/describe) work on compat and fcompat.

(s/explain (pc/fcompat :old (s/fspec :args (s/cat :x int? :y int?)
                                     :ret int?)
                       :new (s/fspec :args (s/* number?)
                                     :ret number?))
           +')
;; => 1.0 - failed: int? at: [:ret :old]

We see here it's a breaking change: we promised an int? but now we're saying we're returning a number?—this is weakening a promise.

We can show it trivially with clojure.core/any?

(s/explain (pc/fcompat :old (s/fspec :args (s/* number?)
                                     :ret number?)
                       :new (s/fspec :args (s/* number?)
                                     :ret any?))
           +')
;; => \space - failed: clojure.core/number?: any? is less constrained than
;; number? at: [:ret :old]

Similarlity, we can show a breaking change by requiring more in the arguments:

(s/explain (pc/fcompat :old (s/fspec :args (s/* number?)
                                     :ret number?)
                       :new (s/fspec :args (s/cat :x number? :y number?)
                                     :ret number?))
           +')
;; => () - failed: Insufficient input at: [:args :new :x]

Developers

Run Clojure unit tests with either:

lein test

or

clj -A:test

License

Copyright © 2018–2019 Santiago Gepigon III

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

Releases

No releases published

Packages

No packages published