speculation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rubocop.yml +87 -0
- data/.travis.yml +16 -0
- data/.yardopts +3 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +29 -0
- data/bin/bundler +17 -0
- data/bin/byebug +17 -0
- data/bin/coderay +17 -0
- data/bin/console +70 -0
- data/bin/cucumber-queue +17 -0
- data/bin/minitest-queue +17 -0
- data/bin/pry +17 -0
- data/bin/rake +17 -0
- data/bin/rspec-queue +17 -0
- data/bin/rubocop +17 -0
- data/bin/ruby-parse +17 -0
- data/bin/ruby-rewrite +17 -0
- data/bin/setup +8 -0
- data/bin/testunit-queue +17 -0
- data/bin/yard +17 -0
- data/bin/yardoc +17 -0
- data/bin/yri +17 -0
- data/lib/speculation/conj.rb +32 -0
- data/lib/speculation/error.rb +17 -0
- data/lib/speculation/gen.rb +106 -0
- data/lib/speculation/identifier.rb +47 -0
- data/lib/speculation/namespaced_symbols.rb +28 -0
- data/lib/speculation/pmap.rb +30 -0
- data/lib/speculation/spec_impl/and_spec.rb +39 -0
- data/lib/speculation/spec_impl/every_spec.rb +176 -0
- data/lib/speculation/spec_impl/f_spec.rb +121 -0
- data/lib/speculation/spec_impl/hash_spec.rb +215 -0
- data/lib/speculation/spec_impl/merge_spec.rb +40 -0
- data/lib/speculation/spec_impl/nilable_spec.rb +36 -0
- data/lib/speculation/spec_impl/or_spec.rb +62 -0
- data/lib/speculation/spec_impl/regex_spec.rb +35 -0
- data/lib/speculation/spec_impl/spec.rb +47 -0
- data/lib/speculation/spec_impl/tuple_spec.rb +67 -0
- data/lib/speculation/spec_impl.rb +36 -0
- data/lib/speculation/test.rb +553 -0
- data/lib/speculation/utils.rb +64 -0
- data/lib/speculation/utils_specs.rb +57 -0
- data/lib/speculation/version.rb +4 -0
- data/lib/speculation.rb +1308 -0
- data/speculation.gemspec +43 -0
- metadata +246 -0
| @@ -0,0 +1,553 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "concurrent"
         | 
| 3 | 
            +
            require "pp"
         | 
| 4 | 
            +
            require "speculation/pmap"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Speculation
         | 
| 7 | 
            +
              module Test
         | 
| 8 | 
            +
                using NamespacedSymbols.refine(self)
         | 
| 9 | 
            +
                using Pmap
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                # @private
         | 
| 12 | 
            +
                S = Speculation
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                @instrumented_methods = Concurrent::Atom.new({})
         | 
| 15 | 
            +
                @instrument_enabled = Concurrent::ThreadLocalVar.new(true)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                class << self
         | 
| 18 | 
            +
                  # if false, instrumented methods call straight through
         | 
| 19 | 
            +
                  attr_accessor :instrument_enabled
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                # Disables instrument's checking of calls within a block
         | 
| 23 | 
            +
                def self.with_instrument_disabled
         | 
| 24 | 
            +
                  instrument_enabled.value = false
         | 
| 25 | 
            +
                  yield
         | 
| 26 | 
            +
                ensure
         | 
| 27 | 
            +
                  instrument_enabled.value = true
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                # Given an opts hash as per instrument, returns the set of
         | 
| 31 | 
            +
                # Speculation::Identifiers for methods that can be instrumented.
         | 
| 32 | 
            +
                # @param opts [Hash]
         | 
| 33 | 
            +
                # @return [Array<Identifier>]
         | 
| 34 | 
            +
                def self.instrumentable_methods(opts = {})
         | 
| 35 | 
            +
                  if opts[:gen]
         | 
| 36 | 
            +
                    unless opts[:gen].keys.all? { |k| k.is_a?(Method) || k.is_a?(Symbol) }
         | 
| 37 | 
            +
                      raise ArgumentError, "instrument :gen expects Method or Symbol keys"
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  S.registry.keys.select(&method(:fn_spec_name?)).to_set.tap do |set|
         | 
| 42 | 
            +
                    set.merge(opts[:spec].keys)    if opts[:spec]
         | 
| 43 | 
            +
                    set.merge(opts[:stub])         if opts[:stub]
         | 
| 44 | 
            +
                    set.merge(opts[:replace].keys) if opts[:replace]
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                # @param method_or_methods [Method, Identifier, Array<Method>, Array<Identifier>]
         | 
| 49 | 
            +
                #   Instruments the methods named by method-or-methods, a method or collection
         | 
| 50 | 
            +
                #   of methods, or all instrumentable methods if method-or-methods is not
         | 
| 51 | 
            +
                #   specified.
         | 
| 52 | 
            +
                #   If a method has an :args fn-spec, replaces the method with a method that
         | 
| 53 | 
            +
                #   checks arg conformance (throwing an exception on failure) before
         | 
| 54 | 
            +
                #   delegating to the original method.
         | 
| 55 | 
            +
                # @param opts [Hash] opts hash can be used to override registered specs, and/or to replace
         | 
| 56 | 
            +
                #   method implementations entirely. Opts for methods not included in
         | 
| 57 | 
            +
                #   method-or-methods are ignored. This facilitates sharing a common options
         | 
| 58 | 
            +
                #   hash across many different calls to instrument
         | 
| 59 | 
            +
                # @option opts :spec [Hash] a map from methods to override specs.
         | 
| 60 | 
            +
                #   :spec overrides registered method specs with specs you provide. Use :spec
         | 
| 61 | 
            +
                #   overrides to provide specs for libraries that do not have them, or to
         | 
| 62 | 
            +
                #   constrain your own use of a fn to a subset of its spec'ed contract.
         | 
| 63 | 
            +
                #   :spec can be used in combination with :stub or :replace.
         | 
| 64 | 
            +
                #
         | 
| 65 | 
            +
                # @option opts :stub [Set, Array] a set of methods to be replaced by stubs.
         | 
| 66 | 
            +
                #   :stub replaces a fn with a stub that checks :args, then uses the :ret spec
         | 
| 67 | 
            +
                #   to generate a return value.
         | 
| 68 | 
            +
                #
         | 
| 69 | 
            +
                # @option opts :gen [Hash{Symbol => Proc}] a map from spec names to generator overrides.
         | 
| 70 | 
            +
                #   :gen overrides are used only for :stub generation.
         | 
| 71 | 
            +
                #
         | 
| 72 | 
            +
                # @option opts :replace [Hash{Method => Proc}] a map from methods to replacement procs.
         | 
| 73 | 
            +
                #   :replace replaces a method with a method that checks args conformance,
         | 
| 74 | 
            +
                #   then invokes the method/proc you provide, enabling arbitrary stubbing and
         | 
| 75 | 
            +
                #   mocking.
         | 
| 76 | 
            +
                #
         | 
| 77 | 
            +
                # @return [Array<Identifier>] a collection of Identifiers naming the methods instrumented.
         | 
| 78 | 
            +
                def self.instrument(method_or_methods = instrumentable_methods, opts = {})
         | 
| 79 | 
            +
                  if opts[:gen]
         | 
| 80 | 
            +
                    gens = opts[:gen].reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }
         | 
| 81 | 
            +
                    opts = opts.merge(:gen => gens)
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                  Array(method_or_methods).
         | 
| 85 | 
            +
                    map { |method| S.Identifier(method) }.
         | 
| 86 | 
            +
                    uniq.
         | 
| 87 | 
            +
                    map { |ident| instrument1(ident, opts) }.
         | 
| 88 | 
            +
                    compact
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                # Undoes instrument on the method_or_methods, specified as in instrument.
         | 
| 92 | 
            +
                # With no args, unstruments all instrumented methods.
         | 
| 93 | 
            +
                # @param method_or_methods [Method, Identifier, Array<Method>, Array<Identifier>]
         | 
| 94 | 
            +
                # @return [Array<Identifier>] a collection of Identifiers naming the methods unstrumented
         | 
| 95 | 
            +
                def self.unstrument(method_or_methods = nil)
         | 
| 96 | 
            +
                  method_or_methods ||= @instrumented_methods.value.keys
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                  Array(method_or_methods).
         | 
| 99 | 
            +
                    map { |method| S.Identifier(method) }.
         | 
| 100 | 
            +
                    map { |ident| unstrument1(ident) }.
         | 
| 101 | 
            +
                    compact
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                # Runs generative tests for method using spec and opts.
         | 
| 105 | 
            +
                # @param method [Method, Identifier]
         | 
| 106 | 
            +
                # @param spec [Spec]
         | 
| 107 | 
            +
                # @param opts [Hash]
         | 
| 108 | 
            +
                # @return [Hash]
         | 
| 109 | 
            +
                # @see check see check for options and return
         | 
| 110 | 
            +
                def self.check_method(method, spec, opts = {})
         | 
| 111 | 
            +
                  validate_check_opts(opts)
         | 
| 112 | 
            +
                  check1(S.Identifier(method), spec, opts)
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                # @param opts [Hash] an opts hash as per `check`
         | 
| 116 | 
            +
                # @return [Array<Identifier>] the set of Identifiers that can be checked.
         | 
| 117 | 
            +
                def self.checkable_methods(opts = {})
         | 
| 118 | 
            +
                  validate_check_opts(opts)
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                  S.
         | 
| 121 | 
            +
                    registry.
         | 
| 122 | 
            +
                    keys.
         | 
| 123 | 
            +
                    select(&method(:fn_spec_name?)).
         | 
| 124 | 
            +
                    reject(&:instance_method?).
         | 
| 125 | 
            +
                    to_set.
         | 
| 126 | 
            +
                    tap { |set| set.merge(opts[:spec].keys) if opts[:spec] }
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                # Run generative tests for spec conformance on method_or_methods. If
         | 
| 130 | 
            +
                # method_or_methods is not specified, check all checkable methods.
         | 
| 131 | 
            +
                #
         | 
| 132 | 
            +
                # @param method_or_methods [Array<Method>, Method]
         | 
| 133 | 
            +
                # @param opts [Hash]
         | 
| 134 | 
            +
                # @option opts :num_tests [Integer] (1000) number of times to generatively test each method
         | 
| 135 | 
            +
                # @option opts :gen [Hash] map from spec names to generator overrides.
         | 
| 136 | 
            +
                #   Generator overrides are passed to Speculation.gen when generating method args.
         | 
| 137 | 
            +
                # @return [Array<Identifier>] an array of check result hashes with the following keys:
         | 
| 138 | 
            +
                #   * :spec       the spec tested
         | 
| 139 | 
            +
                #   * :method     optional method tested
         | 
| 140 | 
            +
                #   * :failure    optional test failure
         | 
| 141 | 
            +
                #   * :result     optional boolean as to whether all generative tests passed
         | 
| 142 | 
            +
                #   * :num_tests  optional number of generative tests ran
         | 
| 143 | 
            +
                #
         | 
| 144 | 
            +
                #   :failure is a hash that will contain a :"Speculation/failure" key with possible values:
         | 
| 145 | 
            +
                #
         | 
| 146 | 
            +
                #   * :check_failed   at least one checked return did not conform
         | 
| 147 | 
            +
                #   * :no_args_spec   no :args spec provided
         | 
| 148 | 
            +
                #   * :no_fspec       no fspec provided
         | 
| 149 | 
            +
                #   * :no_gen         unable to generate :args
         | 
| 150 | 
            +
                #   * :instrument     invalid args detected by instrument
         | 
| 151 | 
            +
                def self.check(method_or_methods = nil, opts = {})
         | 
| 152 | 
            +
                  method_or_methods ||= checkable_methods
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                  Array(method_or_methods).
         | 
| 155 | 
            +
                    map { |method| S.Identifier(method) }.
         | 
| 156 | 
            +
                    select { |ident| checkable_methods(opts).include?(ident) }.
         | 
| 157 | 
            +
                    pmap { |ident| check1(ident, S.get_spec(ident), opts) }
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                # Given a check result, returns an abbreviated version suitable for summary use.
         | 
| 161 | 
            +
                # @param x [Hash]
         | 
| 162 | 
            +
                # @return [Hash]
         | 
| 163 | 
            +
                def self.abbrev_result(x)
         | 
| 164 | 
            +
                  if x[:failure]
         | 
| 165 | 
            +
                    x.reject { |k, _| k == :ret.ns }.
         | 
| 166 | 
            +
                      merge(:spec    => x[:spec].inspect,
         | 
| 167 | 
            +
                            :failure => unwrap_failure(x[:failure]))
         | 
| 168 | 
            +
                  else
         | 
| 169 | 
            +
                    x.reject { |k, _| [:spec, :ret.ns].include?(k) }
         | 
| 170 | 
            +
                  end
         | 
| 171 | 
            +
                end
         | 
| 172 | 
            +
             | 
| 173 | 
            +
                # Given a collection of check_results, e.g. from `check`, pretty prints the
         | 
| 174 | 
            +
                # summary_result (default abbrev_result) of each.
         | 
| 175 | 
            +
                #
         | 
| 176 | 
            +
                # @param check_results [Array] a collection of check_results
         | 
| 177 | 
            +
                # @yield [Hash]
         | 
| 178 | 
            +
                # @return [Hash] a hash with :total, the total number of results, plus a key with a
         | 
| 179 | 
            +
                #   count for each different :type of result.
         | 
| 180 | 
            +
                # @see check see check for check_results
         | 
| 181 | 
            +
                # @see abbrev_result
         | 
| 182 | 
            +
                def self.summarize_results(check_results, &summary_result)
         | 
| 183 | 
            +
                  summary_result ||= method(:abbrev_result)
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                  check_results.reduce(:total => 0) { |summary, result|
         | 
| 186 | 
            +
                    pp summary_result.call(result)
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                    result_key = result_type(result)
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                    summary.merge(
         | 
| 191 | 
            +
                      :total     => summary[:total].next,
         | 
| 192 | 
            +
                      result_key => summary.fetch(result_key, 0).next
         | 
| 193 | 
            +
                    )
         | 
| 194 | 
            +
                  }
         | 
| 195 | 
            +
                end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                class << self
         | 
| 198 | 
            +
                  private
         | 
| 199 | 
            +
             | 
| 200 | 
            +
                  def spec_checking_fn(ident, method, fspec)
         | 
| 201 | 
            +
                    fspec = S.send(:maybe_spec, fspec)
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                    conform = ->(args, block) do
         | 
| 204 | 
            +
                      conformed_args = S.conform(fspec.argspec, args)
         | 
| 205 | 
            +
                      conformed_block = S.conform(fspec.blockspec, block) if fspec.blockspec
         | 
| 206 | 
            +
             | 
| 207 | 
            +
                      if conformed_args == :invalid.ns(S)
         | 
| 208 | 
            +
                        backtrace = backtrace_relevant_to_instrument(caller)
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                        ed = S.
         | 
| 211 | 
            +
                          _explain_data(fspec.argspec, [:args], [], [], args).
         | 
| 212 | 
            +
                          merge(:args.ns(S) => args, :failure.ns(S) => :instrument, :caller.ns => backtrace)
         | 
| 213 | 
            +
             | 
| 214 | 
            +
                        io = StringIO.new
         | 
| 215 | 
            +
                        S.explain_out(ed, io)
         | 
| 216 | 
            +
                        msg = io.string
         | 
| 217 | 
            +
             | 
| 218 | 
            +
                        raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n #{msg}", ed)
         | 
| 219 | 
            +
                      elsif conformed_block == :invalid.ns(S)
         | 
| 220 | 
            +
                        backtrace = backtrace_relevant_to_instrument(caller)
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                        ed = S.
         | 
| 223 | 
            +
                          _explain_data(fspec.blockspec, [:block], [], [], block).
         | 
| 224 | 
            +
                          merge(:block.ns(S) => block, :failure.ns(S) => :instrument, :caller.ns => backtrace)
         | 
| 225 | 
            +
             | 
| 226 | 
            +
                        io = StringIO.new
         | 
| 227 | 
            +
                        S.explain_out(ed, io)
         | 
| 228 | 
            +
                        msg = io.string
         | 
| 229 | 
            +
             | 
| 230 | 
            +
                        raise Speculation::Error.new("Call to '#{ident}' did not conform to spec:\n #{msg}", ed)
         | 
| 231 | 
            +
                      end
         | 
| 232 | 
            +
                    end
         | 
| 233 | 
            +
             | 
| 234 | 
            +
                    ->(*args, &block) do
         | 
| 235 | 
            +
                      method = method.bind(self) if method.is_a?(UnboundMethod)
         | 
| 236 | 
            +
             | 
| 237 | 
            +
                      if Test.instrument_enabled.value
         | 
| 238 | 
            +
                        Test.with_instrument_disabled do
         | 
| 239 | 
            +
                          conform.call(args, block) if fspec.argspec
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                          begin
         | 
| 242 | 
            +
                            Test.instrument_enabled.value = true
         | 
| 243 | 
            +
                            method.call(*args, &block)
         | 
| 244 | 
            +
                          ensure
         | 
| 245 | 
            +
                            Test.instrument_enabled.value = false
         | 
| 246 | 
            +
                          end
         | 
| 247 | 
            +
                        end
         | 
| 248 | 
            +
                      else
         | 
| 249 | 
            +
                        method.call(*args, &block)
         | 
| 250 | 
            +
                      end
         | 
| 251 | 
            +
                    end
         | 
| 252 | 
            +
                  end
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                  def no_fspec(ident, spec)
         | 
| 255 | 
            +
                    S::Error.new("#{ident} not spec'ed", :method => ident, :spec => spec, :failure.ns(S) => :no_fspec)
         | 
| 256 | 
            +
                  end
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                  def instrument1(ident, opts)
         | 
| 259 | 
            +
                    spec = S.get_spec(ident)
         | 
| 260 | 
            +
             | 
| 261 | 
            +
                    raw, wrapped = @instrumented_methods.
         | 
| 262 | 
            +
                      value.
         | 
| 263 | 
            +
                      fetch(ident, {}).
         | 
| 264 | 
            +
                      values_at(:raw, :wrapped)
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                    current = ident.get_method
         | 
| 267 | 
            +
                    to_wrap = wrapped == current ? raw : current
         | 
| 268 | 
            +
             | 
| 269 | 
            +
                    ospec = instrument_choose_spec(spec, ident, opts[:spec])
         | 
| 270 | 
            +
                    raise no_fspec(ident, spec) unless ospec
         | 
| 271 | 
            +
             | 
| 272 | 
            +
                    ofn = instrument_choose_fn(to_wrap, ospec, ident, opts)
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                    checked = spec_checking_fn(ident, ofn, ospec)
         | 
| 275 | 
            +
             | 
| 276 | 
            +
                    ident.redefine_method!(checked)
         | 
| 277 | 
            +
             | 
| 278 | 
            +
                    wrapped = ident.get_method
         | 
| 279 | 
            +
             | 
| 280 | 
            +
                    @instrumented_methods.swap do |methods|
         | 
| 281 | 
            +
                      methods.merge(ident => { :raw => to_wrap, :wrapped => wrapped })
         | 
| 282 | 
            +
                    end
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                    ident
         | 
| 285 | 
            +
                  end
         | 
| 286 | 
            +
             | 
| 287 | 
            +
                  def instrument_choose_fn(f, spec, ident, opts)
         | 
| 288 | 
            +
                    stubs   = (opts[:stub] || []).map(&S.method(:Identifier))
         | 
| 289 | 
            +
                    over    = opts[:gen] || {}
         | 
| 290 | 
            +
                    replace = (opts[:replace] || {}).reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                    if stubs.include?(ident)
         | 
| 293 | 
            +
                      Gen.generate(S.gen(spec, over))
         | 
| 294 | 
            +
                    else
         | 
| 295 | 
            +
                      replace.fetch(ident, f)
         | 
| 296 | 
            +
                    end
         | 
| 297 | 
            +
                  end
         | 
| 298 | 
            +
             | 
| 299 | 
            +
                  def instrument_choose_spec(spec, ident, overrides)
         | 
| 300 | 
            +
                    (overrides || {}).
         | 
| 301 | 
            +
                      reduce({}) { |h, (k, v)| h.merge(S.Identifier(k) => v) }.
         | 
| 302 | 
            +
                      fetch(ident, spec)
         | 
| 303 | 
            +
                  end
         | 
| 304 | 
            +
             | 
| 305 | 
            +
                  def unstrument1(ident)
         | 
| 306 | 
            +
                    instrumented = @instrumented_methods.value[ident]
         | 
| 307 | 
            +
                    return unless instrumented
         | 
| 308 | 
            +
             | 
| 309 | 
            +
                    raw, wrapped = instrumented.values_at(:raw, :wrapped)
         | 
| 310 | 
            +
             | 
| 311 | 
            +
                    @instrumented_methods.swap do |h|
         | 
| 312 | 
            +
                      h.reject { |k, _v| k == ident }
         | 
| 313 | 
            +
                    end
         | 
| 314 | 
            +
             | 
| 315 | 
            +
                    current = ident.get_method
         | 
| 316 | 
            +
             | 
| 317 | 
            +
                    # Only redefine to original if it has not been modified since it was
         | 
| 318 | 
            +
                    # instrumented.
         | 
| 319 | 
            +
                    if wrapped == current
         | 
| 320 | 
            +
                      ident.tap { |i| i.redefine_method!(raw) }
         | 
| 321 | 
            +
                    end
         | 
| 322 | 
            +
                  end
         | 
| 323 | 
            +
             | 
| 324 | 
            +
                  def explain_check(args, spec, v, role)
         | 
| 325 | 
            +
                    data = unless S.valid?(spec, v)
         | 
| 326 | 
            +
                             S._explain_data(spec, [role], [], [], v).
         | 
| 327 | 
            +
                               merge(:args.ns       => args,
         | 
| 328 | 
            +
                                     :val.ns        => v,
         | 
| 329 | 
            +
                                     :failure.ns(S) => :check_failed)
         | 
| 330 | 
            +
                           end
         | 
| 331 | 
            +
             | 
| 332 | 
            +
                    S::Error.new("Specification-based check failed", data).tap do |e|
         | 
| 333 | 
            +
                      e.set_backtrace(caller)
         | 
| 334 | 
            +
                    end
         | 
| 335 | 
            +
                  end
         | 
| 336 | 
            +
             | 
| 337 | 
            +
                  # Returns true if call passes specs, otherwise returns a hash with
         | 
| 338 | 
            +
                  # :backtrace, :cause and :data keys. :data will have a
         | 
| 339 | 
            +
                  # :"Speculation/failure" key.
         | 
| 340 | 
            +
                  def check_call(method, spec, args, block)
         | 
| 341 | 
            +
                    conformed_args = S.conform(spec.argspec, args) if spec.argspec
         | 
| 342 | 
            +
             | 
| 343 | 
            +
                    if conformed_args == :invalid.ns(S)
         | 
| 344 | 
            +
                      return explain_check(args, spec.argspec, args, :args)
         | 
| 345 | 
            +
                    end
         | 
| 346 | 
            +
             | 
| 347 | 
            +
                    conformed_block = S.conform(spec.blockspec, block) if spec.blockspec
         | 
| 348 | 
            +
             | 
| 349 | 
            +
                    if conformed_block == :invalid.ns(S)
         | 
| 350 | 
            +
                      return explain_check(block, spec.block, block, :block)
         | 
| 351 | 
            +
                    end
         | 
| 352 | 
            +
             | 
| 353 | 
            +
                    ret = method.call(*args, &block)
         | 
| 354 | 
            +
             | 
| 355 | 
            +
                    conformed_ret = S.conform(spec.retspec, ret) if spec.retspec
         | 
| 356 | 
            +
             | 
| 357 | 
            +
                    if conformed_ret == :invalid.ns(S)
         | 
| 358 | 
            +
                      return explain_check(args, spec.retspec, ret, :ret)
         | 
| 359 | 
            +
                    end
         | 
| 360 | 
            +
             | 
| 361 | 
            +
                    return true unless spec.argspec && spec.retspec && spec.fnspec
         | 
| 362 | 
            +
             | 
| 363 | 
            +
                    if S.valid?(spec.fnspec, :args => conformed_args, :block => conformed_block, :ret => conformed_ret)
         | 
| 364 | 
            +
                      true
         | 
| 365 | 
            +
                    else
         | 
| 366 | 
            +
                      explain_check(args, spec.fnspec, { :args => conformed_args, :block => conformed_block, :ret => conformed_ret }, :fn)
         | 
| 367 | 
            +
                    end
         | 
| 368 | 
            +
                  end
         | 
| 369 | 
            +
             | 
| 370 | 
            +
                  def quick_check(method, spec, opts)
         | 
| 371 | 
            +
                    gen = opts[:gen]
         | 
| 372 | 
            +
                    num_tests = opts.fetch(:num_tests, 1000)
         | 
| 373 | 
            +
             | 
| 374 | 
            +
                    args_gen = begin
         | 
| 375 | 
            +
                                 S.gen(spec.argspec, gen)
         | 
| 376 | 
            +
                               rescue => e
         | 
| 377 | 
            +
                                 return { :result => e }
         | 
| 378 | 
            +
                               end
         | 
| 379 | 
            +
             | 
| 380 | 
            +
                    block_gen = if spec.blockspec
         | 
| 381 | 
            +
                                  begin
         | 
| 382 | 
            +
                                    S.gen(spec.blockspec, gen)
         | 
| 383 | 
            +
                                  rescue => e
         | 
| 384 | 
            +
                                    return { :result => e }
         | 
| 385 | 
            +
                                  end
         | 
| 386 | 
            +
                                else
         | 
| 387 | 
            +
                                  Utils.constantly(nil)
         | 
| 388 | 
            +
                                end
         | 
| 389 | 
            +
             | 
| 390 | 
            +
                    combined_gen = ->(r) { [args_gen.call(r), block_gen.call(r)] }
         | 
| 391 | 
            +
             | 
| 392 | 
            +
                    rantly_quick_check(combined_gen, num_tests) { |(args, block)| check_call(method, spec, args, block) }
         | 
| 393 | 
            +
                  end
         | 
| 394 | 
            +
             | 
| 395 | 
            +
                  def make_check_result(method, spec, check_result)
         | 
| 396 | 
            +
                    result = { :spec   => spec,
         | 
| 397 | 
            +
                               :ret.ns => check_result,
         | 
| 398 | 
            +
                               :method => method }
         | 
| 399 | 
            +
             | 
| 400 | 
            +
                    if check_result[:result] && check_result[:result] != true
         | 
| 401 | 
            +
                      result[:failure] = check_result[:result]
         | 
| 402 | 
            +
                    end
         | 
| 403 | 
            +
             | 
| 404 | 
            +
                    if check_result[:shrunk]
         | 
| 405 | 
            +
                      result[:failure] = check_result[:shrunk][:result]
         | 
| 406 | 
            +
                    end
         | 
| 407 | 
            +
             | 
| 408 | 
            +
                    result
         | 
| 409 | 
            +
                  end
         | 
| 410 | 
            +
             | 
| 411 | 
            +
                  def check1(ident, spec, opts)
         | 
| 412 | 
            +
                    specd = S.spec(spec)
         | 
| 413 | 
            +
             | 
| 414 | 
            +
                    reinstrument = unstrument(ident).any?
         | 
| 415 | 
            +
                    method = ident.get_method
         | 
| 416 | 
            +
             | 
| 417 | 
            +
                    if specd.argspec # or blockspec?
         | 
| 418 | 
            +
                      check_result = quick_check(method, spec, opts)
         | 
| 419 | 
            +
                      make_check_result(method, spec, check_result)
         | 
| 420 | 
            +
                    else
         | 
| 421 | 
            +
                      failure = { :info         => "No :args spec",
         | 
| 422 | 
            +
                                  failure.ns(S) => :no_args_spec }
         | 
| 423 | 
            +
             | 
| 424 | 
            +
                      { :failure => failure,
         | 
| 425 | 
            +
                        :method  => method,
         | 
| 426 | 
            +
                        :spec    => spec }
         | 
| 427 | 
            +
                    end
         | 
| 428 | 
            +
                  ensure
         | 
| 429 | 
            +
                    instrument(ident) if reinstrument
         | 
| 430 | 
            +
                  end
         | 
| 431 | 
            +
             | 
| 432 | 
            +
                  def validate_check_opts(opts)
         | 
| 433 | 
            +
                    return unless opts[:gen]
         | 
| 434 | 
            +
             | 
| 435 | 
            +
                    unless opts[:gen].keys.all? { |k| k.is_a?(Method) || k.is_a?(Symbol) }
         | 
| 436 | 
            +
                      raise ArgumentErorr, "check :gen expects Method or Symbol keys"
         | 
| 437 | 
            +
                    end
         | 
| 438 | 
            +
                  end
         | 
| 439 | 
            +
             | 
| 440 | 
            +
                  def backtrace_relevant_to_instrument(backtrace)
         | 
| 441 | 
            +
                    backtrace.drop_while { |line| line.include?(__FILE__) }
         | 
| 442 | 
            +
                  end
         | 
| 443 | 
            +
             | 
| 444 | 
            +
                  def fn_spec_name?(spec_name)
         | 
| 445 | 
            +
                    spec_name.is_a?(S::Identifier)
         | 
| 446 | 
            +
                  end
         | 
| 447 | 
            +
             | 
| 448 | 
            +
                  # Reimplementation of Rantly's `check` since it does not provide direct access to results
         | 
| 449 | 
            +
                  # (shrunk data etc.), instead printing them to STDOUT.
         | 
| 450 | 
            +
                  def rantly_quick_check(gen, num_tests)
         | 
| 451 | 
            +
                    i = 0
         | 
| 452 | 
            +
                    limit = 100
         | 
| 453 | 
            +
             | 
| 454 | 
            +
                    Rantly.singleton.generate(num_tests, limit, gen) do |val|
         | 
| 455 | 
            +
                      args, blk = val
         | 
| 456 | 
            +
                      i += 1
         | 
| 457 | 
            +
             | 
| 458 | 
            +
                      result = begin
         | 
| 459 | 
            +
                                 yield([args, blk])
         | 
| 460 | 
            +
                               rescue => e
         | 
| 461 | 
            +
                                 e
         | 
| 462 | 
            +
                               end
         | 
| 463 | 
            +
             | 
| 464 | 
            +
                      unless result == true
         | 
| 465 | 
            +
                        # This is a Rantly Tuple.
         | 
| 466 | 
            +
                        args = ::Tuple.new(args)
         | 
| 467 | 
            +
             | 
| 468 | 
            +
                        if args.respond_to?(:shrink)
         | 
| 469 | 
            +
                          shrunk = shrink(args, result, ->(v) { yield([v, blk]) })
         | 
| 470 | 
            +
             | 
| 471 | 
            +
                          shrunk[:smallest] = [shrunk[:smallest].array, blk]
         | 
| 472 | 
            +
             | 
| 473 | 
            +
                          return { :fail      => args.array,
         | 
| 474 | 
            +
                                   :block     => blk,
         | 
| 475 | 
            +
                                   :num_tests => i,
         | 
| 476 | 
            +
                                   :result    => result,
         | 
| 477 | 
            +
                                   :shrunk    => shrunk }
         | 
| 478 | 
            +
                        else
         | 
| 479 | 
            +
                          return { :fail      => args.array,
         | 
| 480 | 
            +
                                   :block     => blk,
         | 
| 481 | 
            +
                                   :num_tests => i,
         | 
| 482 | 
            +
                                   :result    => result }
         | 
| 483 | 
            +
                        end
         | 
| 484 | 
            +
                      end
         | 
| 485 | 
            +
                    end
         | 
| 486 | 
            +
             | 
| 487 | 
            +
                    { :num_tests => i,
         | 
| 488 | 
            +
                      :result    => true }
         | 
| 489 | 
            +
                  end
         | 
| 490 | 
            +
             | 
| 491 | 
            +
                  # reimplementation of Rantly's shrinking.
         | 
| 492 | 
            +
                  def shrink(data, result, block, depth = 0, iteration = 0)
         | 
| 493 | 
            +
                    smallest = data
         | 
| 494 | 
            +
                    max_depth = depth
         | 
| 495 | 
            +
             | 
| 496 | 
            +
                    if data.shrinkable?
         | 
| 497 | 
            +
                      while iteration < 1024
         | 
| 498 | 
            +
                        shrunk_data = data.shrink
         | 
| 499 | 
            +
                        result = begin
         | 
| 500 | 
            +
                                   block.call(shrunk_data.array)
         | 
| 501 | 
            +
                                 rescue => e
         | 
| 502 | 
            +
                                   e
         | 
| 503 | 
            +
                                 end
         | 
| 504 | 
            +
             | 
| 505 | 
            +
                        unless result == true
         | 
| 506 | 
            +
                          shrunk = shrink(shrunk_data, result, block, depth + 1, iteration + 1)
         | 
| 507 | 
            +
             | 
| 508 | 
            +
                          branch_smallest, branch_depth, iteration =
         | 
| 509 | 
            +
                            shrunk.values_at(:smallest, :depth, :iteration)
         | 
| 510 | 
            +
             | 
| 511 | 
            +
                          if branch_depth > max_depth
         | 
| 512 | 
            +
                            smallest = branch_smallest
         | 
| 513 | 
            +
                            max_depth = branch_depth
         | 
| 514 | 
            +
                          end
         | 
| 515 | 
            +
                        end
         | 
| 516 | 
            +
             | 
| 517 | 
            +
                        break unless data.retry?
         | 
| 518 | 
            +
                      end
         | 
| 519 | 
            +
                    end
         | 
| 520 | 
            +
             | 
| 521 | 
            +
                    { :depth     => max_depth,
         | 
| 522 | 
            +
                      :iteration => iteration,
         | 
| 523 | 
            +
                      :result    => result,
         | 
| 524 | 
            +
                      :smallest  => smallest }
         | 
| 525 | 
            +
                  end
         | 
| 526 | 
            +
             | 
| 527 | 
            +
                  ### check reporting ###
         | 
| 528 | 
            +
             | 
| 529 | 
            +
                  def failure_type(x)
         | 
| 530 | 
            +
                    x.data[:failure.ns(S)] if x.is_a?(S::Error)
         | 
| 531 | 
            +
                  end
         | 
| 532 | 
            +
             | 
| 533 | 
            +
                  def unwrap_failure(x)
         | 
| 534 | 
            +
                    failure_type(x) ? x.data : x
         | 
| 535 | 
            +
                  end
         | 
| 536 | 
            +
             | 
| 537 | 
            +
                  # Returns the type of the check result. This can be any of the
         | 
| 538 | 
            +
                  # :"Speculation/failure" symbols documented in 'check', or:
         | 
| 539 | 
            +
                  #
         | 
| 540 | 
            +
                  # :check_passed   all checked method returns conformed
         | 
| 541 | 
            +
                  # :check_raised   checked fn threw an exception
         | 
| 542 | 
            +
                  def result_type(ret)
         | 
| 543 | 
            +
                    failure = ret[:failure]
         | 
| 544 | 
            +
             | 
| 545 | 
            +
                    if failure.nil?
         | 
| 546 | 
            +
                      :check_passed
         | 
| 547 | 
            +
                    else
         | 
| 548 | 
            +
                      failure_type(failure) || :check_raised
         | 
| 549 | 
            +
                    end
         | 
| 550 | 
            +
                  end
         | 
| 551 | 
            +
                end
         | 
| 552 | 
            +
              end
         | 
| 553 | 
            +
            end
         | 
| @@ -0,0 +1,64 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require "set"
         | 
| 3 | 
            +
            require "speculation/conj"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Speculation
         | 
| 6 | 
            +
              using Conj
         | 
| 7 | 
            +
             | 
| 8 | 
            +
              # @private
         | 
| 9 | 
            +
              module Utils
         | 
| 10 | 
            +
                def self.hash?(x)
         | 
| 11 | 
            +
                  x.respond_to?(:store)
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def self.array?(x)
         | 
| 15 | 
            +
                  x.respond_to?(:at)
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def self.collection?(xs)
         | 
| 19 | 
            +
                  xs.respond_to?(:each)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                def self.identity(x)
         | 
| 23 | 
            +
                  x
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                def self.constantly(x)
         | 
| 27 | 
            +
                  ->(*) { x }
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def self.complement(&f)
         | 
| 31 | 
            +
                  ->(*args) { !f.call(*args) }
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def self.distinct?(xs)
         | 
| 35 | 
            +
                  seen = Set[]
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  xs.each do |x|
         | 
| 38 | 
            +
                    if seen.include?(x)
         | 
| 39 | 
            +
                      return false
         | 
| 40 | 
            +
                    else
         | 
| 41 | 
            +
                      seen << x
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  true
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                def self.ident?(x)
         | 
| 49 | 
            +
                  x.is_a?(Symbol) || x.is_a?(Identifier)
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def self.method?(x)
         | 
| 53 | 
            +
                  x.is_a?(Method) || x.is_a?(UnboundMethod)
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                def self.empty(coll)
         | 
| 57 | 
            +
                  coll.class.new
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def self.into(to, from)
         | 
| 61 | 
            +
                  from.reduce(to) { |memo, obj| memo.conj(obj) }
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
            end
         | 
| @@ -0,0 +1,57 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Speculation
         | 
| 4 | 
            +
              # @private
         | 
| 5 | 
            +
              module UtilsSpecs
         | 
| 6 | 
            +
                using Speculation::NamespacedSymbols.refine(self)
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                S = Speculation
         | 
| 9 | 
            +
                U = Speculation::Utils
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                S.fdef(U.method(:hash?),
         | 
| 12 | 
            +
                       :args => S.tuple(:any.ns(S)),
         | 
| 13 | 
            +
                       :ret  => :boolean.ns(S))
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                S.fdef(U.method(:array?),
         | 
| 16 | 
            +
                       :args => S.tuple(:any.ns(S)),
         | 
| 17 | 
            +
                       :ret  => :boolean.ns(S))
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                S.fdef(U.method(:collection?),
         | 
| 20 | 
            +
                       :args => S.tuple(:any.ns(S)),
         | 
| 21 | 
            +
                       :ret  => :boolean.ns(S))
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                S.fdef(U.method(:identity),
         | 
| 24 | 
            +
                       :args => S.cat(:x => :any.ns(S)),
         | 
| 25 | 
            +
                       :ret  => :any.ns(S),
         | 
| 26 | 
            +
                       :fn   => ->(x) { x[:args][:x].equal?(x[:ret]) })
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                S.fdef(U.method(:complement),
         | 
| 29 | 
            +
                       :args  => :empty.ns(S),
         | 
| 30 | 
            +
                       :block => S.fspec(:args => S.zero_or_more(:any.ns(S)),
         | 
| 31 | 
            +
                                         :ret  => :any.ns(S)),
         | 
| 32 | 
            +
                       :ret   => S.fspec(:args => S.zero_or_more(:any.ns(S)),
         | 
| 33 | 
            +
                                         :ret  => :boolean.ns(S)))
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                S.fdef(U.method(:constantly),
         | 
| 36 | 
            +
                       :args => S.cat(:x => :any.ns(S)),
         | 
| 37 | 
            +
                       :ret  => Proc,
         | 
| 38 | 
            +
                       :fn   => ->(x) { x[:args][:x].equal?(x[:ret].call) })
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                S.fdef(U.method(:distinct?),
         | 
| 41 | 
            +
                       :args => S.cat(:coll => Enumerable),
         | 
| 42 | 
            +
                       :ret  => :boolean.ns(S))
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                S.fdef(U.method(:ident?),
         | 
| 45 | 
            +
                       :args => S.cat(:x => :any.ns(S)),
         | 
| 46 | 
            +
                       :ret  => :boolean.ns(S))
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                S.fdef(U.method(:method?),
         | 
| 49 | 
            +
                       :args => S.cat(:x => :any.ns(S)),
         | 
| 50 | 
            +
                       :ret  => :boolean.ns(S))
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                S.fdef(U.method(:empty),
         | 
| 53 | 
            +
                       :args => S.cat(:coll => Enumerable),
         | 
| 54 | 
            +
                       :ret  => S.and(Enumerable, ->(coll) { coll.empty? }),
         | 
| 55 | 
            +
                       :fn   => ->(x) { x[:args][:coll].class == x[:ret].class })
         | 
| 56 | 
            +
              end
         | 
| 57 | 
            +
            end
         |