Module: Speculation

Extended by:
NamespacedSymbols
Defined in:
lib/speculation/spec/f_spec.rb,
lib/speculation.rb,
lib/speculation/gen.rb,
lib/speculation/pmap.rb,
lib/speculation/spec.rb,
lib/speculation/test.rb,
lib/speculation/error.rb,
lib/speculation/utils.rb,
lib/speculation/version.rb,
lib/speculation/predicates.rb,
lib/speculation/spec/or_spec.rb,
lib/speculation/spec/and_spec.rb,
lib/speculation/spec/hash_spec.rb,
lib/speculation/spec/every_spec.rb,
lib/speculation/spec/merge_spec.rb,
lib/speculation/spec/regex_spec.rb,
lib/speculation/spec/tuple_spec.rb,
lib/speculation/method_identifier.rb,
lib/speculation/spec/nilable_spec.rb,
lib/speculation/namespaced_symbols.rb,
lib/speculation/spec/predicate_spec.rb,
lib/speculation/spec/nonconforming_spec.rb

Overview

This is a Ruby translation of clojure.spec:

https://github.com/clojure/clojure/blob/master/src/clj/clojure/spec.clj

All credit belongs with Rich Hickey and contributors for their original work.

Defined Under Namespace

Modules: Gen, NamespacedSymbols, Predicates, Test Classes: Error

Constant Summary collapse

VERSION =
"0.4.1".freeze

Class Attribute Summary collapse

Class Method Summary collapse

Methods included from NamespacedSymbols

namespace, namespaced_name, ns, symbol

Class Attribute Details

.check_assertsObject

Enables or disables spec asserts. Defaults to false.


24
25
26
# File 'lib/speculation.rb', line 24

def check_asserts
  @check_asserts
end

.coll_check_limitObject

The number of elements validated in a collection spec'ed with 'every'.


35
36
37
# File 'lib/speculation.rb', line 35

def coll_check_limit
  @coll_check_limit
end

.coll_error_limitObject

The number of errors reported by explain in a collection spec'ed with 'every'


39
40
41
# File 'lib/speculation.rb', line 39

def coll_error_limit
  @coll_error_limit
end

.fspec_iterationsObject

The number of times an anonymous fn specified by fspec will be (generatively) tested during conform.


32
33
34
# File 'lib/speculation.rb', line 32

def fspec_iterations
  @fspec_iterations
end

.recursion_limitObject

A soft limit on how many times a branching spec (or/alt/zero_or_more/opt keys) can be recursed through during generation. After this a non-recursive branch will be chosen.


28
29
30
# File 'lib/speculation.rb', line 28

def recursion_limit
  @recursion_limit
end

Class Method Details

.alt(kv_specs) ⇒ Hash

Returns regex op that returns a two item array containing the key of the first matching pred and the corresponding value. Thus can be destructured to refer generically to the components of the return.

Examples:

S.alt(even: :even?.to_proc, small: -> (n) { n < 42 })

526
527
528
# File 'lib/speculation.rb', line 526

def self.alt(kv_specs)
  _alt(kv_specs.values, kv_specs.keys).merge(:id => SecureRandom.uuid)
end

.and(*preds) ⇒ Spec

Returns a spec that returns the conformed value. Successive conformed values propagate through rest of predicates.

Examples:

S.and(Numeric, -> (n) { n < 42 })

410
411
412
# File 'lib/speculation.rb', line 410

def self.and(*preds)
  AndSpec.new(preds)
end

.and_keys(*ks) ⇒ Object

See Also:


391
392
393
# File 'lib/speculation.rb', line 391

def self.and_keys(*ks)
  [:"Speculation/and", *ks]
end

.assert(spec, x) ⇒ Object

Can be enabled or disabled at runtime:

  • enabled/disabled by setting `check_asserts`.

  • enabled by setting environment variable SPECULATION_CHECK_ASSERTS to the string “true”

Defaults to false if not set.

Raises:

  • (Error)

    with explain_data plus :Speculation/failure of :assertion_failed


59
60
61
62
63
64
65
66
67
68
# File 'lib/speculation.rb', line 59

def self.assert(spec, x)
  return x unless check_asserts
  return x if valid?(spec, x)

  ed = _explain_data(spec, [], [], [], x).merge(:failure => :assertion_failed)
  out = StringIO.new
  explain_out(ed, out)

  raise Speculation::Error.new("Spec assertion failed\n#{out.string}", ed)
end

.cat(named_specs) ⇒ Hash

Returns regex op that matches (all) values in sequence, returning a map containing the keys of each pred and the corresponding value.

Examples:

S.cat(e: :even?.to_proc, o: :odd?.to_proc)

535
536
537
538
539
540
# File 'lib/speculation.rb', line 535

def self.cat(named_specs)
  keys = named_specs.keys
  predicates = named_specs.values

  pcat(:keys => keys, :predicates => predicates, :return_value => {})
end

.coll_of(pred, opts = {}) ⇒ Spec

Returns a spec for a collection of items satisfying pred. Unlike 'every', coll_of will exhaustively conform every value.

Same options as 'every'. conform will produce a collection corresponding to :into if supplied, else will match the input collection, avoiding rebuilding when possible.

See Also:


476
477
478
# File 'lib/speculation.rb', line 476

def self.coll_of(pred, opts = {})
  every(pred, :conform_all => true, **opts)
end

.conform(spec, value) ⇒ Symbol, Object


136
137
138
139
# File 'lib/speculation.rb', line 136

def self.conform(spec, value)
  spec = MethodIdentifier(spec)
  specize(spec).conform(value)
end

.conformer(f, unformer = nil) ⇒ Spec


555
556
557
# File 'lib/speculation.rb', line 555

def self.conformer(f, unformer = nil)
  spec_impl(f, nil, true, unformer)
end

.constrained(re, *preds) ⇒ Hash


547
548
549
# File 'lib/speculation.rb', line 547

def self.constrained(re, *preds)
  { :op => :amp, :p1 => re, :predicates => preds }
end

.date_in(date_range) ⇒ Object


109
110
111
112
# File 'lib/speculation.rb', line 109

def self.date_in(date_range)
  spec(self.and(Date, ->(x) { date_range.cover?(x) }),
       :gen => ->() { ->(_) { rand(date_range) } })
end

.def(key, spec) ⇒ Symbol, Method

Given a namespace-qualified symbol key, and a spec, spec name, predicate or regex-op makes an entry in the registry mapping key to the spec


292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/speculation.rb', line 292

def(key, spec)
  key = MethodIdentifier(key)

  unless Utils.ident?(key) && (!key.is_a?(Symbol) || NamespacedSymbols.namespace(key))
    raise ArgumentError, "key must be a namespaced Symbol, e.g. #{ns(:my_spec)}, or a Method"
  end

  spec = if spec?(spec) || regex?(spec) || registry[spec]
           spec
         else
           spec_impl(spec, nil, false)
         end

  @registry_ref.swap do |reg|
    reg.merge(key => with_name(spec, key)).freeze
  end

  key.is_a?(MethodIdentifier) ? key.get_method : key
end

.every(pred, opts = {}) ⇒ Spec

Note:

that 'every' does not do exhaustive checking, rather it samples `coll_check_limit` elements. Nor (as a result) does it do any conforming of elements. 'explain' will report at most coll_error_limit problems. Thus 'every' should be suitable for potentially large collections.

Returns spec that validates collection elements against pred

Options Hash (opts):

  • :kind (Object) — default: nil

    a pred/spec that the collection type must satisfy, e.g. `Array` Note that if :kind is specified and :into is not, this pred must generate in order for every to generate.

  • :count (Integer) — default: nil

    specifies coll has exactly this count

  • :min_count (Integer) — default: nil

    coll has count >= min_count

  • :max_count (Integer) — default: nil

    coll has count <= max_count

  • :distinct (Boolean) — default: nil

    all the elements are distinct

  • :gen_max (Integer) — default: 20

    the maximum coll size to generate

  • :into (Array, Hash, Set) — default: Array

    one of [], {}, Set[], the default collection to generate into (default: empty coll as generated by :kind pred if supplied, else [])

  • :gen (Proc)

    generator returning function, which must be a zero arg proc that returns a proc of one arg (Rantly instance) that generates a valid value.

See Also:


443
444
445
446
447
# File 'lib/speculation.rb', line 443

def self.every(pred, opts = {})
  gen = opts.delete(:gen)

  EverySpec.new(pred, opts, gen)
end

.every_kv(kpred, vpred, options = {}) ⇒ Spec

Like 'every' but takes separate key and val preds and works on associative collections.

Same options as 'every', :into defaults to {}

See Also:


459
460
461
462
463
# File 'lib/speculation.rb', line 459

def self.every_kv(kpred, vpred, options = {})
  every(tuple(kpred, vpred), :kfn  => ->(_i, v) { v.first },
                             :into => {},
                             **options)
end

.exercise(spec, n: 10, overrides: {}) ⇒ Array

Generates a number (default 10) of values compatible with spec and maps conform over them, returning a sequence of [val conformed-val] tuples.


646
647
648
649
650
# File 'lib/speculation.rb', line 646

def self.exercise(spec, n: 10, overrides: {})
  Gen.sample(gen(spec, overrides), n).map { |value|
    [value, conform(spec, value)]
  }
end

.exercise_fn(method, n = 10, fspec = nil) ⇒ Array

Exercises the method by applying it to n (default 10) generated samples of its args spec. When fspec is supplied its arg spec is used, and method can be a proc.

Raises:

  • (ArgumentError)

659
660
661
662
663
664
665
666
667
# File 'lib/speculation.rb', line 659

def self.exercise_fn(method, n = 10, fspec = nil)
  fspec ||= get_spec(method)
  raise ArgumentError, "No :args spec found for #{method}" unless fspec && fspec.args

  block_gen = fspec.block ? gen(fspec.block) : Utils.constantly(nil)
  gen = Gen.tuple(gen(fspec.args), block_gen)

  Gen.sample(gen, n).map { |(args, block)| [args, block, method.call(*args, &block)] }
end

.explain(spec, x) ⇒ Object

Given a spec and a value that fails to conform, prints an explaination to STDOUT


229
230
231
# File 'lib/speculation.rb', line 229

def self.explain(spec, x)
  explain_out(explain_data(spec, x))
end

.explain_data(spec, x) ⇒ nil, Hash

Given a spec and a value x which ought to conform, returns nil if x conforms, else a hash with at least the key :problems whose value is a collection of problem-hashes, where problem-hash has at least :path :pred and :val keys describing the predicate and the value that failed at that path.


187
188
189
190
191
# File 'lib/speculation.rb', line 187

def self.explain_data(spec, x)
  spec = MethodIdentifier(spec)
  name = spec_name(spec)
  _explain_data(spec, [], Array(name), [], x)
end

.explain_out(ed, out = STDOUT) ⇒ Object


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/speculation.rb', line 195

def self.explain_out(ed, out = STDOUT)
  return out.puts("Success!") unless ed

  problems = Utils.sort_descending(ed.fetch(:problems)) { |prob| prob[:path] }

  problems.each do |prob|
    path, pred, val, reason, via, inn = prob.values_at(:path, :pred, :val, :reason, :via, :in)

    out.print("In: ", inn.to_a.inspect, " ") unless inn.empty?
    out.print("val: ", val.inspect, " fails")
    out.print(" spec: ", via.last.inspect) unless via.empty?
    out.print(" at: ", path.to_a.inspect) unless path.empty?
    out.print(" predicate: ", pred.inspect)
    out.print(", ", reason.inspect) if reason

    prob.each do |k, v|
      unless [:path, :pred, :val, :reason, :via, :in].include?(k)
        out.print("\n\t ", k.inspect, PP.pp(v, String.new))
      end
    end

    out.puts
  end

  ed.each do |k, v|
    out.puts("#{k.inspect} #{PP.pp(v, String.new)}") unless k == :problems
  end

  nil
end

.explain_str(spec, x) ⇒ String


236
237
238
239
240
# File 'lib/speculation.rb', line 236

def self.explain_str(spec, x)
  out = StringIO.new
  explain_out(explain_data(spec, x), out)
  out.string
end

.fdef(method, spec) ⇒ Method

Note:

Note that :fn specs require the presence of :args and :ret specs to conform values, and so :fn specs will be ignored if :args or :ret are missing.

Once registered, specs are checked by instrument and tested by the runner Speculation::Test.check

Examples:

to register method specs for the Hash[] method:

S.fdef(Hash.method(:[]),
  args: S.alt(
    hash: Hash,
    array_of_pairs: S.coll_of(S.tuple(ns(S, :any), ns(S, :any)), kind: Array),
    kvs: S.constrained(S.one_or_more(ns(S, :any)), -> (kvs) { kvs.count.even? })
  ),
  ret: Hash
)

Options Hash (spec):

  • :args (Hash)

    regex spec for the method arguments as a list

  • :block (Object)

    an fspec for the method's block

  • :ret (Object)

    a spec for the method's return value

  • :fn (Object)

    a spec of the relationship between args and ret - the value passed is { args: conformed_args, block: given_block, ret: conformed_ret } and is expected to contain predicates that relate those values


610
611
612
613
# File 'lib/speculation.rb', line 610

def self.fdef(method, spec)
  self.def(MethodIdentifier(method), fspec(spec))
  method
end

.float_in(min: nil, max: nil, infinite: true, nan: true) ⇒ Spec


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/speculation.rb', line 75

def self.float_in(min: nil, max: nil, infinite: true, nan: true)
  preds = [Float]

  preds.push(->(x) { !x.nan? })      unless nan
  preds.push(->(x) { !x.infinite? }) unless infinite
  preds.push(->(x) { x <= max })     if max
  preds.push(->(x) { x >= min })     if min

  min ||= Float::MIN
  max ||= Float::MAX

  gens = [[20, ->(_) { rand(min.to_f..max.to_f) }]]
  gens << [1, ->(r) { r.choose(Float::INFINITY, -Float::INFINITY) }] if infinite
  gens << [1, ->(_) { Float::NAN }] if nan

  spec(self.and(*preds), :gen => ->() { ->(rantly) { rantly.freq(*gens) } })
end

.fspec(args: nil, ret: nil, fn: nil, block: nil, gen: nil) ⇒ Spec

Takes :args :ret and (optional) :block and :fn kwargs whose values are preds and returns a spec whose conform/explain take a method/proc and validates it using generative testing. The conformed value is always the method itself.

fspecs can generate procs that validate the arguments and fabricate a return value compliant with the :ret spec, ignoring the :fn spec if present.


575
576
577
# File 'lib/speculation.rb', line 575

def self.fspec(args: nil, ret: nil, fn: nil, block: nil, gen: nil)
  FSpec.new(:args => spec(args), :ret => spec(ret), :fn => spec(fn), :block => spec(block), :gen => gen)
end

.gen(spec, overrides = nil) ⇒ Proc

Given a spec, returns the generator for it, or raises if none can be constructed.

Optionally an overrides hash can be provided which should map spec names or paths (array of symbols) to no-arg generator Procs. These will be used instead of the generators at those names/paths. Note that parent generator (in the spec or overrides map) will supersede those of any subtrees. A generator for a regex op must always return a sequential collection (i.e. a generator for Speculation.zero_or_more should return either an empty array or an array with one item in it)


273
274
275
276
# File 'lib/speculation.rb', line 273

def self.gen(spec, overrides = nil)
  spec = MethodIdentifier(spec)
  gensub(spec, overrides, [], :recursion_limit => recursion_limit)
end

.get_spec(key) ⇒ Spec?


320
321
322
# File 'lib/speculation.rb', line 320

def self.get_spec(key)
  registry[MethodIdentifier(key)]
end

.hash_of(kpred, vpred, options = {}) ⇒ Spec

Returns a spec for a hash whose keys satisfy kpred and vals satisfy vpred. Unlike 'every_kv', hash_of will exhaustively conform every value.

Same options as 'every', :kind defaults to `Speculation::Predicates.hash?`, with the addition of:

:conform_keys - conform keys as well as values (default false)

See Also:


493
494
495
496
497
# File 'lib/speculation.rb', line 493

def self.hash_of(kpred, vpred, options = {})
  every_kv(kpred, vpred, :kind        => Predicates.method(:hash?),
                         :conform_all => true,
                         **options)
end

.int_in(range) ⇒ Object


95
96
97
98
# File 'lib/speculation.rb', line 95

def self.int_in(range)
  spec(self.and(Integer, ->(x) { range.include?(x) }),
       :gen => ->() { ->(_) { rand(range) } })
end

.invalid?(value) ⇒ Boolean


128
129
130
# File 'lib/speculation.rb', line 128

def self.invalid?(value)
  value.equal?(:"Speculation/invalid")
end

.keys(req: [], opt: [], req_un: [], opt_un: [], gen: nil) ⇒ Object

Creates and returns a hash validating spec. :req and :opt are both arrays of namespaced-qualified keywords (e.g. “:MyApp/foo”). The validator will ensure the :req keys are present. The :opt keys serve as documentation and may be used by the generator.

The :req key array supports 'and_keys' and 'or_keys' for key groups:

S.keys(req: [ns(:x), ns(:y), S.or_keys(ns(:secret), S.and_keys(ns(:user), ns(:pwd)))],
       opt: [ns(:z)])

There are also _un versions of :req and :opt. These allow you to connect unqualified keys to specs. In each case, fully qualfied keywords are passed, which name the specs, but unqualified keys (with the same name component) are expected and checked at conform-time, and generated during gen:

S.keys(req_un: [:"MyApp/x", :"MyApp/y"])

The above says keys :x and :y are required, and will be validated and generated by specs (if they exist) named :“MyApp/x” :“MyApp/y” respectively.

In addition, the values of all namespace-qualified keys will be validated (and possibly destructured) by any registered specs. Note: there is no support for inline value specification, by design.


381
382
383
# File 'lib/speculation.rb', line 381

def self.keys(req: [], opt: [], req_un: [], opt_un: [], gen: nil)
  HashSpec.new(req, opt, req_un, opt_un, gen)
end

.merge(*preds) ⇒ Spec

Note:

Unlike 'and', merge can generate maps satisfying the union of the predicates.

Returns a spec that returns a conformed hash satisfying all of the specs.


417
418
419
# File 'lib/speculation.rb', line 417

def self.merge(*preds)
  MergeSpec.new(preds)
end

.nilable(pred) ⇒ Spec


627
628
629
# File 'lib/speculation.rb', line 627

def self.nilable(pred)
  NilableSpec.new(pred)
end

.nonconforming(spec) ⇒ Spec


635
636
637
# File 'lib/speculation.rb', line 635

def self.nonconforming(spec)
  NonconformingSpec.new(spec)
end

.one_or_more(pred) ⇒ Hash

an array of matches


509
510
511
# File 'lib/speculation.rb', line 509

def self.one_or_more(pred)
  pcat(:predicates => [pred, rep(pred, pred, [], true)], :return_value => [])
end

.or(key_preds) ⇒ Spec

Returns a destructuring spec that returns a two element array containing the key of the first matching pred and the corresponding value. Thus the 'key' and 'val' functions can be used to refer generically to the components of the tagged return.

Examples:

S.or(even: -> (n) { n.even? }, small: -> (n) { n < 42 })

401
402
403
# File 'lib/speculation.rb', line 401

def self.or(key_preds)
  OrSpec.new(key_preds)
end

.or_keys(*ks) ⇒ Object

See Also:


386
387
388
# File 'lib/speculation.rb', line 386

def self.or_keys(*ks)
  [:"Speculation/or", *ks]
end

.regex?(x) ⇒ Hash, false


122
123
124
# File 'lib/speculation.rb', line 122

def self.regex?(x)
  x.is_a?(Hash) && x[:op] && x
end

.registryHash

Returns the registry hash

See Also:


314
315
316
# File 'lib/speculation.rb', line 314

def self.registry
  @registry_ref.value
end

.spec(pred, gen: nil) ⇒ Spec

NOTE: it is not generally necessary to wrap predicates in spec when using `S.def` etc., only to attach a unique generator.

Optionally takes :gen generator function, which must be a no-arg proc that returns a generator (proc that receives a Rantly instance) that generates a valid value.


347
348
349
# File 'lib/speculation.rb', line 347

def self.spec(pred, gen: nil)
  spec_impl(pred, gen, false) if pred
end

.spec?(x) ⇒ Spec, false


116
117
118
# File 'lib/speculation.rb', line 116

def self.spec?(x)
  x if x.is_a?(Spec)
end

.time_in(time_range) ⇒ Object


102
103
104
105
# File 'lib/speculation.rb', line 102

def self.time_in(time_range)
  spec(self.and(Time, ->(x) { time_range.cover?(x) }),
       :gen => ->() { ->(_) { rand(time_range) } })
end

.tuple(*preds) ⇒ Spec


583
584
585
# File 'lib/speculation.rb', line 583

def self.tuple(*preds)
  TupleSpec.new(preds)
end

.unform(spec, value) ⇒ Object


144
145
146
# File 'lib/speculation.rb', line 144

def self.unform(spec, value)
  specize(spec).unform(value)
end

.valid?(spec, x) ⇒ Boolean


618
619
620
621
622
623
# File 'lib/speculation.rb', line 618

def self.valid?(spec, x)
  spec = MethodIdentifier(spec)
  spec = specize(spec)

  !invalid?(spec.conform(x))
end

.with_gen(spec, &gen) ⇒ Spec

Takes a spec and a no-arg generator returning block and returns a version of the spec that uses

that generator

Yield Returns:

  • Rantly generator


153
154
155
156
157
158
159
160
161
162
163
# File 'lib/speculation.rb', line 153

def self.with_gen(spec, &gen)
  if gen && !gen.arity.zero?
    raise ArgumentError, "gen must be a no-arg block that returns a generator"
  end

  if regex?(spec)
    spec.merge(:gfn => gen)
  else
    specize(spec).with_gen(gen)
  end
end

.zero_or_more(pred) ⇒ Hash


502
503
504
# File 'lib/speculation.rb', line 502

def self.zero_or_more(pred)
  rep(pred, pred, [], false)
end

.zero_or_one(pred) ⇒ Hash

single value (not a collection) if matched.


516
517
518
# File 'lib/speculation.rb', line 516

def self.zero_or_one(pred)
  _alt([pred, accept(:nil)], nil)
end