quick-sampler 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 58a28eb8cda1b35a7cfde8b6c7c118e3a6373b17
4
+ data.tar.gz: 655c8fb5860814fbf19f7fedf8573e2d310cbcc4
5
+ SHA512:
6
+ metadata.gz: a31df65d1b884a650988a248af0c324a5aad89df9dd5295e85b44b9a3a841ed6effde069088cf5368e846eba23b764ab19d163d3788d8ec1f2879eec903069c8
7
+ data.tar.gz: 4e5ae84816f4e748c3c184775cb5de4e6c34e1cda267081f6d08d20201ec4a1c53811fff2d31ce1f7a55433ff1eb2a70a47452e28a7a7cad43541da51cd7ea0e
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.2
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --markup-provider=redcarpet
2
+ --markup=markdown
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in quick-sampler.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 de Praktijk Index B.V.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Quick Sampler
2
+
3
+ Composable samplers of data: describe your randomness and watch it blend.
4
+
5
+ Quick Sampler is DSL for describing and sampling random data influenced by
6
+ Haskell/Erlang's [QuickCheck][1] generators by Koen Claessen and John Hughes,
7
+ [rantly][2] gem by Howard Yeh, [theft][3] gem by Shawn Anderson, Jessica Kerr's
8
+ [generatron][4] gem, her [Property-Based Testing for Better Code talk][5] and
9
+ the rest of the cause-and-effect chain [all the way][6] to [big bang][7].
10
+
11
+ [1]: http://www.cse.chalmers.se/~rjmh/QuickCheck/
12
+ [2]: https://github.com/hayeah/rantly
13
+ [3]: https://github.com/shawn42/theft
14
+ [4]: https://github.com/jessitron/generatron/
15
+ [5]: http://www.windycityrails.org/videos/2014/#14
16
+ [6]: http://en.wikipedia.org/wiki/Turtles_all_the_way_down
17
+ [7]: http://en.wikipedia.org/wiki/Unmoved_mover
18
+
19
+ ## Sampler vs Generator
20
+
21
+ **Sampler** is the same as **generator** in other randomness frameworks, but
22
+ tries to suggest an expanded understanding of what it can be used for.
23
+ Ordinarily one would *sample* a source of randomness, but one could just as
24
+ well *sample* some "real" data at random and pass it on, verbatim or randomly
25
+ transmuted.
26
+
27
+ The term is supposed to suggest "the way of truth" view of
28
+ unchanging reality that Parmenides described in his *On Nature* in fifth
29
+ century BCE. To see what he meant back when Socrates was a young man - fix your
30
+ random seed and watch your "generators" repeat themselves. Smoke that,
31
+ Heraclitus.
32
+
33
+ <img src="https://cloud.githubusercontent.com/assets/64227/6993106/512cc778-daea-11e4-82dc-01cc8ef958fa.jpg" height="200px">
34
+
35
+ ## Usage
36
+
37
+ The main entry point is {Quick::Sampler.compile}. The method takes sampler
38
+ definition as block and returns a "compiled" sampler:
39
+
40
+ ```
41
+ pry> sampler = Quick::Sampler.compile { integer }
42
+ => #<Enumerator::Lazy: #<Enumerator: #<Enumerator::Generator:0x007f295f9b2af0>:each>>
43
+ pry> sampler.first(5)
44
+ => [1763573971376409386, -1692192782152475313, -3665498119514965288, 0, 0]
45
+ ```
46
+
47
+ So, the truth is out: a sampler is a lazy enumerator (and by extension - Enumerable).
48
+ But [will it blend?][8]
49
+
50
+ [8]: https://github.com/jessitron/gerald#gerald
51
+
52
+ ```irb
53
+ pry> sampler2 = Quick::Sampler.compile { config(upper_bound: 5).string(:lower) }
54
+ => #<Enumerator::Lazy: #<Enumerator: #<Enumerator::Generator:0x007f295fd88058>:each>>
55
+ pry> sampler2.zip(sampler).first(5).to_h
56
+ => {"bjm"=>-4027257104748747508,
57
+ "bcrs"=>-540067903901761386,
58
+ "wqfn"=>2107130696606126069,
59
+ "disw"=>2495326937126240758,
60
+ "rglv"=>2235767748081203791}
61
+ ```
62
+
63
+ Hell yeah, it blends.
64
+
65
+ ### Compile
66
+
67
+ The aim of "compilation" is to deliver us from typing / reading the namespaces
68
+ in sampler definitions. Consider, for example:
69
+
70
+ ```ruby
71
+ Quick::Sampler.compile do
72
+ one_of_weighted integer => 10,
73
+ boolean => 1,
74
+ vector_of(5, integer) => 5
75
+ end
76
+ ```
77
+
78
+ (**Rosetta stone:** `one_of_weighted` is what in Haskell QuickCheck is known as `frequency`)
79
+
80
+ vs hypothetic alternative:
81
+
82
+ ```ruby
83
+ Generators.one_of_weighted Generators.integer => 10,
84
+ Generators.boolean => 1,
85
+ Generators.vector_of(5, Generators.integer) => 5
86
+ ```
87
+
88
+ ### Sampler configuration
89
+
90
+ Some sampling parameters can be passed as arguments to a sampler function (like
91
+ character class `:lower` in the example above). Others - that affect multiple
92
+ sub-samplers in a definition - may be injected with a call to `config(...)` (like
93
+ `upper_bound` above).
94
+
95
+ (**Rosetta stone:** `upper_bound` is what in Haskell QuickCheck is known as `size`)
96
+
97
+ ### Sampler composability
98
+
99
+ The composition using Enumerable API as demonstrated above is pretty flexible and familiar to a
100
+ ruby developer.
101
+
102
+ Some less "linear" shapes of data on the other hand are better
103
+ expressed by the Quick Sampler DSL within the sampler definition:
104
+
105
+ **TODO** replace with a real life example of a complex sampler.
106
+
107
+ ```ruby
108
+ cities_sampler = Quick::Sampler.compile do
109
+ send_to City, :build, name: string(:lower),
110
+ state: pick_from(State.all),
111
+ population: pick_from(1_000..10_000_000)
112
+ end
113
+
114
+ cities = cities_sampler.first(100)
115
+ cities.each &:save!
116
+
117
+ travellers_sampler = Quick::Sampler.compile do
118
+ send_to Person, :build,
119
+ name: string(:lower),
120
+ born_in: pick_from(cities),
121
+ lives_in: pick_from(cities),
122
+ visited: list_of(pick_from(cities))
123
+ end
124
+
125
+ travellers = travellers_sampler.first(1000)
126
+ travellers.each &:save!
127
+ ```
128
+
129
+ ## Installation
130
+
131
+ Add this line to your application's Gemfile:
132
+
133
+ ```ruby
134
+ gem 'quick-sampler'
135
+ ```
136
+
137
+ And then execute:
138
+
139
+ $ bundle
140
+
141
+ Or install it yourself as:
142
+
143
+ $ gem install quick-sampler
144
+
145
+ ## Contributing
146
+
147
+ 1. Fork it ( https://github.com/praktijkindex/quick-sampler/fork )
148
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
149
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
150
+ 4. Push to the branch (`git push origin my-new-feature`)
151
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
8
+ require "yard"
9
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,2 @@
1
+ require "active_support/dependencies"
2
+ ActiveSupport::Dependencies.autoload_paths << File.expand_path("../../..", __FILE__)
@@ -0,0 +1,96 @@
1
+ require "delegate"
2
+
3
+ module Quick
4
+ module Sampler
5
+ # A sampler base class, delegating most work to the underlying lazy enumerator.
6
+ #
7
+ # ### Readable #inspect
8
+ #
9
+ # One superficial extra that {Sampler::Base} provides is a description attribute
10
+ # which is returned by `#inspect`. This can help keep test output readable.
11
+ #
12
+ # ### Shrinking
13
+ #
14
+ # Sampler::Base also provides an {#shrink api for shrinking} failed inputs.
15
+ #
16
+ # From {http://stackoverflow.com/a/16970029/538534 stackoverflow discussion}
17
+ # of shrinking (follow the link for an example) in the original QuickCheck:
18
+ #
19
+ # > When QuickCheck finds an input that violates a property, it will first
20
+ # try to find smaller inputs that also violate the property, in order to
21
+ # give the developer a better message about the nature of the failure.
22
+ #
23
+ # > What it means to be „small“ of course depends on the datatype in
24
+ # question; to QuickCheck it is anything that comes out from from the
25
+ # shrink function.
26
+ #
27
+ class Base < DelegateClass(Enumerator::Lazy)
28
+ attr_accessor :description
29
+
30
+ # @api private
31
+ # Not supposed to be used directly, use {Quick::Sampler.compile} instead.
32
+ #
33
+ # @param [Enumerator, Enumerator::Lazy, #call] source source of data.
34
+ # Non-lazy `Enumerator` will be lazyfied, and anything `call`able will be
35
+ # wrapped into a lazy enumerator.
36
+ # @param [String] description sampler description to be returned by #inspect
37
+ def initialize source, description: "Quick Sampler"
38
+ @description = description
39
+ super(source_to_lazy(source))
40
+ end
41
+
42
+ # @return [String] sampler description, so test output is readable
43
+ def inspect
44
+ description
45
+ end
46
+
47
+ # A very preliminary API for QuickCheck-like input shrinking.
48
+ # A sampler is responsible for shrinking its failed samples.
49
+ #
50
+ # @example Detecting and then shrinking failing examples:
51
+ #
52
+ # # Generate random input data
53
+ # all_inputs = sampler.first(100)
54
+ #
55
+ # # Find failures (by rejecting inputs satisfying the property)
56
+ # failed_inputs = all_inputs.reject{ |input| property(input) }
57
+ #
58
+ # # Shrink failed inputs, continue shrinking as long as property does not hold
59
+ # shrunk_inputs = sampler.shrink(failed_inputs) {|input| !property(input)}
60
+ #
61
+ #
62
+ # @param [Enumerable] samples input samples that failed the preoperty
63
+ #
64
+ # @yieldparam [<Sample>] sample shrunk value to check the property again. If
65
+ # property holds, the value will be discarded, and the one it was shrunk
66
+ # from will be added to the set of "minimal" failing examples.
67
+ #
68
+ # @yieldreturn [Boolean] `true` to keep on shrinking, meaning *"property still fails for
69
+ # shrunken value, try to shrink more"*
70
+ #
71
+ # @return [Array] "minimal" samples that failed to satisfy the property under test
72
+ def shrink samples, &block
73
+ Quick::Sampler::Shrink.while(samples, &block)
74
+ end
75
+
76
+ private
77
+
78
+ def source_to_lazy source
79
+ case source
80
+ when Enumerator::Lazy
81
+ source
82
+ when Enumerator
83
+ source.lazy
84
+ when ->(i) { i.respond_to? :call }
85
+ Enumerator.new do |recipient|
86
+ loop do
87
+ recipient << source.call
88
+ end
89
+ end.lazy
90
+ else
91
+ raise "Don't know how to make a sampler from #{source.inspect}"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,25 @@
1
+ require "active_support/configurable"
2
+ require "active_support/concern"
3
+
4
+ module Quick
5
+ module Sampler
6
+ module Config
7
+ extend ActiveSupport::Concern
8
+ include ActiveSupport::Configurable
9
+
10
+ included do
11
+ config_accessor(:max_iterations) { 1000 }
12
+ config_accessor(:upper_bound) { 25 }
13
+ end
14
+
15
+ def config options = :none_given
16
+ if options == :none_given
17
+ super()
18
+ else
19
+ config.merge!(options)
20
+ self
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,41 @@
1
+ module Quick
2
+ module Sampler
3
+ #@!visibility private
4
+ module DSL::CharacterClass
5
+ class << self
6
+
7
+ def expand *class_names
8
+ class_names
9
+ .map { |name| definitions[name] }
10
+ .compact
11
+ .flat_map { |definition|
12
+ case definition
13
+ when String
14
+ definition.chars
15
+ when Range
16
+ definition.to_a
17
+ when Array
18
+ expand(*definition)
19
+ end
20
+ }.to_a
21
+ end
22
+
23
+ private
24
+
25
+ def definitions
26
+ {
27
+ lower: "a".."z",
28
+ upper: "A".."Z",
29
+ alpha: [:lower, :upper],
30
+ digits: "0".."9",
31
+ alnum: [:alpha, :digits],
32
+ punctuation: %q[~`!@#$%^&*()_-+={}|\\:;"'<,>.?/],
33
+ whitespace: " \t\n",
34
+ printable: [:alnum, :punctuation, :whitespace]
35
+ }
36
+ end
37
+
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,49 @@
1
+ require "delegate"
2
+
3
+ module Quick
4
+ module Sampler
5
+ # A Quick::Sampler wrapper providing a fluid DSL that can be used in a sampler definition
6
+ # passed to {Quick::Sampler.compile}.
7
+ class DSL::Fluidiom < SimpleDelegator
8
+ # SimpleDelegator so that it can unwrap the "original" sampler with `#__getobj__`
9
+ include Quick::Sampler::Config
10
+
11
+ # @api private
12
+ # wraps a `sampler` into a `Fluidiom` instance so it has extra methods while
13
+ # inside the block passed to {Quick::Sampler.compile}
14
+ def initialize sampler, _config = {}
15
+ sampler = Base.new(sampler) unless sampler.is_a? Base
16
+ super(sampler)
17
+ config.merge! _config
18
+ end
19
+
20
+ # @return [Quick::Sampler]
21
+ # the unwrapped original sampler
22
+ def unwrap
23
+ __getobj__
24
+ end
25
+
26
+ # @return [Quick::Sampler]
27
+ # wrapped sampler that starts out with the same config as this one
28
+ def spawn sampler
29
+ self.class.new(sampler, config)
30
+ end
31
+
32
+ # spawn a filtering sampler
33
+ #
34
+ # The produced sampler honors the config variable `max_iterations` and stops
35
+ # iterating when that many original values are tested.
36
+ #
37
+ # @return [Quick::Sampler]
38
+ # a sampler that filter through only samples that satisfy the
39
+ # predicate given as block
40
+ # @yieldparam [Anything] sample
41
+ # a sampled value to be tested
42
+ # @yieldreturn [Boolean]
43
+ # `true` to pass the value through
44
+ def such_that &predicate
45
+ spawn(unwrap.take(max_iterations).select(&predicate))
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,135 @@
1
+ module Quick
2
+ module Sampler
3
+ # DSL methods that allow to combine samplers in various ways.
4
+ #
5
+ # ### Recursive samplers
6
+ #
7
+ # Some of the combinators are recursive: given a nested structure of Arrays and Hashes
8
+ # containing samplers and non-sampler values they would produce an analogous structure
9
+ # where samplers are replaced by their `#next` value.
10
+ module DSL::SimpleCombinators
11
+
12
+ # @return [Quick::Sampler]
13
+ # a sampler producing values from the range or array
14
+ def pick_from source
15
+ case source
16
+ when Range
17
+ feed { rand(source) }
18
+ when Array
19
+ feed { source.sample }
20
+ end
21
+ end
22
+
23
+ # @return [Quick::Sampler]
24
+ # a sampler that recursively samples on of the arguments at random
25
+ def one_of *args
26
+ feed { recursive_sample(args.sample) }
27
+ end
28
+
29
+ # @return [Quick::Sampler]
30
+ # a sampler that recursively samples on of the expressions at random, with
31
+ # likelyhood of picking one of the expressions depends on its weight.
32
+ def one_of_weighted expression_weights
33
+ total_weight, expressions = expression_weights
34
+ .reduce([0, {}]) { |(total_weight, expressions), (expression, weight)|
35
+ total_weight += weight
36
+ [total_weight, expressions.merge(total_weight => expression)]
37
+ }
38
+
39
+ feed {
40
+ dice = rand * total_weight
41
+ weight_class = expressions.keys.find{|w| dice < w}
42
+ recursive_sample(expressions[weight_class])
43
+ }
44
+ end
45
+
46
+ # Sampler of uniform arrays
47
+ #
48
+ # This sampler honors `upper_bound` config variable and samples Arrays of up to
49
+ # that many elements.
50
+ #
51
+ # **Rosetta stone** the single argument version corresponds to QuickCheck's `listOf`.
52
+ # Passing `non_empty: true` turns it into QuickCheck's `listOf1`.
53
+ #
54
+ # @return [Quick::Sampler]
55
+ # a sampler that produces arrays of values sampled from its argument
56
+ # @param [Quick::Sampler] sampler
57
+ # a sampler to sample array elements from
58
+ # @param [Boolean] non_empty
59
+ # pass true to never produce empty arrays
60
+ def list_of sampler, non_empty: false
61
+ lower_bound = non_empty ? 1 : 0
62
+ feed { sampler.first(rand(lower_bound..upper_bound)) }
63
+ end
64
+
65
+ # Sampler of uniform fixed length arrays
66
+ #
67
+ # **Rosetta stone** this sampler corresponds to QuickCheck's `vectorOf`.
68
+ #
69
+ # @return [Quick::Sampler]
70
+ # a sampler that produces arrays of `length` of values sampled from `sampler`
71
+ # @param [Integer] length
72
+ # sample array length
73
+ # @param [Quick::Sampler] sampler
74
+ # a sampler to sample array elements from
75
+ def vector_of length, sampler
76
+ feed { sampler.take(length).force }
77
+ end
78
+
79
+ # Sampler of arbitrary nested structures made up of `Array`s, `Hash`es, `Quick::Sampler`s and
80
+ # non-sampler values
81
+ #
82
+ # @param [Array] args
83
+ # a template structure
84
+ # @return [Quick::Sampler]
85
+ # a recursive sampler
86
+ def list_like *args
87
+ feed { args.map { |arg| recursive_sample(arg) } }
88
+ end
89
+
90
+ # Sampler of arbitrary message send ("method call") results
91
+ #
92
+ # @overload send_to recipient, message, *args
93
+ # @param [Object, Quick::Sampler<Object>] recipient
94
+ # message recipient or a sampler producing recipients
95
+ # @param [Symbol, Quick::Sampler<Symbol>] message
96
+ # message to send or a sampler producing messages
97
+ # @param [Array] args
98
+ # argument list that may contain samplers producing arguments
99
+ #
100
+ # @overload send_to sampler
101
+ # @param [Quick::Sampler<[Object, Symbol, *Anything]>] sampler
102
+ # a sampler producing the message send details
103
+ #
104
+ # @return [Quick::Sampler]
105
+ # a sampler of dynamically generated message send results
106
+ def send_to *args
107
+ call_sampler = if args.count > 1
108
+ list_like *args
109
+ else
110
+ args.first
111
+ end
112
+ feed {
113
+ object, message, *args = call_sampler.next
114
+ object.send( message, *args )
115
+ }
116
+ end
117
+
118
+ private
119
+
120
+ def recursive_sample value
121
+ case value
122
+ when Quick::Sampler
123
+ value.next
124
+ when Hash
125
+ value.map{ |key, value| [key, recursive_sample(value)] }.to_h
126
+ when Array
127
+ value.map{ |value| recursive_sample(value) }
128
+ else
129
+ value
130
+ end
131
+ end
132
+
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,97 @@
1
+ module Quick
2
+ module Sampler
3
+ # Samplers of simple values to form the basis of the sampled data structure.
4
+ #
5
+ # ### Note from the future
6
+ #
7
+ # In the future simple values are sampled from other excellent Gems from behind a
8
+ # composable Quick Sampler API. In the mean time this is possible at the cost of
9
+ # readablity:
10
+ #
11
+ # @example Faker integration
12
+ #
13
+ # Quick::Sampler.compile description: "email address" do
14
+ # feed { Faker::Internet.email }
15
+ # end
16
+ module DSL::SimpleValues
17
+
18
+ # @!volatile
19
+ #
20
+ # Degenerate constant sampler. Will probably be superseeded by
21
+ # a cleaner smarter syntax as I get a better hang of it.
22
+ #
23
+ # @return [Quick::Sampler<Anything>] a sampler of constant value
24
+ # @param [Anything] const
25
+ # the value to keep on sampling
26
+ def const const
27
+ feed { const }
28
+ end
29
+
30
+ # Degenerate sampler of zeros. Like {#const} this will probably
31
+ # go away soon.
32
+ #
33
+ # @return [Quick::Sampler<0>] a sampler of zeros
34
+ def zero
35
+ const 0
36
+ end
37
+
38
+ FixnumRange = -(2**(0.size * 8 -2))..(2**(0.size * 8 -2) -1)
39
+ private_constant :FixnumRange
40
+
41
+ # Samples random fixnums (smaller integers that can be handled quickly by
42
+ # the CPU itself)
43
+ #
44
+ # @return [Quick::Sampler<Fixnum>] a sampler of fixnums
45
+ def fixnum
46
+ pick_from(FixnumRange)
47
+ end
48
+
49
+ # @return [Quick::Sampler<Fixnum>] a sampler of negative fixnums
50
+ def negative_fixnum
51
+ pick_from(FixnumRange.min..-1)
52
+ end
53
+
54
+ # @return [Quick::Sampler<Fixnum>] a sampler of positive fixnums
55
+ def positive_fixnum
56
+ pick_from(1..FixnumRange.max)
57
+ end
58
+
59
+ # A sampler of integers prefering smaller ones
60
+ #
61
+ # It will however sample a large one (from the Fixnum range) occasionally.
62
+ #
63
+ # @return [Quick::Sampler<Fixnum>] a sampler of integers
64
+ def integer
65
+ one_of_weighted(fixnum => 5,
66
+ pick_from(-1_000_000_000..1_000_000_000) => 7,
67
+ pick_from(-1000..1000) => 9,
68
+ pick_from(-100..100) => 11,
69
+ pick_from(-20..20) => 17)
70
+ end
71
+
72
+ alias_method :negative_integer, :negative_fixnum
73
+ alias_method :positive_integer, :positive_fixnum
74
+
75
+ # @return [Quick::Sampler<Boolean>] a sampler of `true` and `false` values
76
+ def boolean
77
+ pick_from([true, false])
78
+ end
79
+
80
+ # This sampler honors `upper_bound` config variable.
81
+ #
82
+ # The sampler will produce strings of random (between 0 and `upper_bound`) length
83
+ # made up of characters belonging to supplied named classes.
84
+ #
85
+ # @returns [Quick::Sampler<String>] random `String` sampler
86
+ # @param [Array<Symbol>] classes
87
+ # Character classes to pick from.
88
+ # @todo document character classes
89
+ def string *classes
90
+ classes = [:printable] if classes.empty?
91
+ repertoire = DSL::CharacterClass.expand(*classes)
92
+ feed { repertoire.sample(rand(upper_bound)).join }
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,71 @@
1
+ module Quick
2
+ module Sampler
3
+ # A domain specific method for sampler definitions.
4
+ #
5
+ # Instance methods of this class are available as barewords inside a
6
+ # sampler definition supplied as a block to {Quick::Sampler.compile}.
7
+ #
8
+ # Methods that produce a sampler actually wrap it in a {Fluidiom} instance
9
+ # that adds a fluid API to the sampler. This wrapping is stripped off from
10
+ # the sampler returned by {Quick::Sampler.compile} - although I'm still
11
+ # undesided if that's the right thing to do.
12
+ #
13
+ # (Incidentally, {Fluidiom} instances for deeper nested sub-samplers get
14
+ # leaked from compile at the moment)
15
+ class DSL
16
+ include Quick::Sampler::Config
17
+ include SimpleValues
18
+ include SimpleCombinators
19
+
20
+ # @api private
21
+ # @param [Binding] binding a context to lookup unknown methods
22
+ def initialize binding = nil
23
+ setup_delegation(binding) if binding
24
+ end
25
+
26
+ # @api private
27
+ # (see Quick::Sampler.compile)
28
+ def self.compile description: nil, &block
29
+ new(block.binding).instance_eval(&block).unwrap.tap do |sampler|
30
+ sampler.description = description
31
+ end
32
+ end
33
+
34
+ # overloaded to display human readable text in tests output
35
+ def inspect
36
+ "Quick Sampler DSL"
37
+ end
38
+
39
+ # Wraps a block into a lazy enumerator which will become sampler.
40
+ #
41
+ # I haven't decided yet if this is a private implementation detail
42
+ # or a powerful albeit confusing DSL verb
43
+ #
44
+ # @yieldreturn [<Sample>] a sampled value
45
+ #
46
+ def feed &block
47
+ Fluidiom.new(Base.new(block), config)
48
+ end
49
+
50
+ private
51
+
52
+
53
+ def setup_delegation binding
54
+ @context = binding.eval("self")
55
+
56
+ # this poor man's delegation is because inheriting from SimpleDelegator breaks
57
+ # autoload for some reason
58
+
59
+ # @!visibility private
60
+ def self.method_missing *args, &block
61
+ @context.send(*args, &block)
62
+ end
63
+
64
+ # @!visibility private
65
+ def self.respond_to? *args
66
+ super || @context.respond_to?(*args)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,38 @@
1
+ require "set"
2
+ using Quick::Sampler::Shrink::Refinements
3
+
4
+ module Quick
5
+ module Sampler
6
+ module Shrink
7
+ #@!visibility private
8
+ module ClassMethods
9
+ #@!visibility private
10
+ module While
11
+ def while values, &block
12
+ shrunk = Set.new
13
+ input_size = values.count
14
+ values = values.reduce(Set.new) { |shrinking, current|
15
+ report "shrunk: #{shrunk.count}, still shrinking: #{shrinking.count}"
16
+ shrunk_current = (current.shrink||[])
17
+ .reject{|candidate| candidate == current}
18
+ .select(&block)
19
+ shrunk << current if shrunk_current.empty?
20
+ shrinking + shrunk_current
21
+ } until values.empty?
22
+ report ""
23
+ shrunk.to_a
24
+ end
25
+
26
+ def report message
27
+ width = 80
28
+ message = "" if message == :clear
29
+ message = message[0...width]
30
+ padding = " " * [width - message.length, 0].max
31
+ print "#{message}#{padding}\r"
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ module Quick
2
+ module Sampler
3
+ module Shrink
4
+ #@!visibility private
5
+ module Refinements
6
+
7
+ refine ::Object do
8
+ def shrink
9
+ end
10
+ end
11
+
12
+ refine ::Integer do
13
+ def shrink
14
+ # -1, 0 and 1 can't be shrunken
15
+ if self > 10
16
+ [self/2]
17
+ elsif self > 1
18
+ [self - 1]
19
+ elsif self < -1
20
+ [-self.abs.shrink.first]
21
+ end
22
+ end
23
+ end
24
+
25
+ refine ::String do
26
+ def shrink
27
+ if length > 1
28
+ remove_indices = (0...length).to_a
29
+ remove_indices = remove_indices.sample(1) if length > 9
30
+ remove_indices.map{|i| remove_by_index(i)}
31
+ end
32
+ end
33
+
34
+ def remove_by_index index
35
+ dup.tap{|copy| copy.slice!(index)}
36
+ end
37
+ end
38
+
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,8 @@
1
+ module Quick
2
+ module Sampler
3
+ #@!visibility private
4
+ module Shrink
5
+ extend ClassMethods::While
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ module Quick
2
+ module Sampler
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ require_relative "sampler/autoload"
2
+ require "active_support/core_ext/module/delegation"
3
+
4
+ # A prefix module for future Quick Sampler's sister libraries.
5
+ # For now just look at {Quick::Sampler} itself.
6
+ module Quick
7
+ # A façade module with the main entry point: {.compile}
8
+ module Sampler
9
+
10
+ class << self
11
+
12
+ # Main entry point of Quick Sampler. Compiles a definition into a sampler. The
13
+ # "compilation" is only a metaphor here, since definition - which is given to `compile` as
14
+ # a block - is simply executed in the context of a fresh {Quick::Sampler::DSL}
15
+ # instance (where available syntax can be found).
16
+ #
17
+ # @example Compile a sampler
18
+ # chaos = Quick::Sampler.compile description: "a bit of everything" do
19
+ # one_of_weighted integer => 5,
20
+ # boolean => 1,
21
+ # pick_from(-10.0..10.0) => 10,
22
+ # string(:alnum) => 15,
23
+ # feed { Faker::Internet.email } => 3
24
+ # end
25
+ #
26
+ # @param [String] description sampler description which
27
+ # will be returned by `#inspect` as well as `#description`
28
+ #
29
+ # @param block sampler definition. Will be `instance_eval`ed in the context of an
30
+ # anonymous instance of {Quick::Sampler::DSL}.
31
+ #
32
+ # @return [Quick::Sampler] compiled sampler
33
+ #
34
+ # @!method compile(description: nil, &block)
35
+ delegate :compile, to: Quick::Sampler::DSL
36
+
37
+ # @api private
38
+ #
39
+ # Tests if `obj` is an instance of a known Quick Sampler sub-type.
40
+ #
41
+ # @param [Object] obj the object to test
42
+ # @return [Boolean] true if the object is of a Quick Sampler sub-type
43
+ def === obj
44
+ Base === obj || DSL::Fluidiom === obj
45
+ end
46
+ end
47
+ end
48
+ end
49
+
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "quick/sampler/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "quick-sampler"
8
+ spec.version = Quick::Sampler::VERSION
9
+ spec.authors = ["Artem Baguinski"]
10
+ spec.email = ["abaguinski@depraktijkindex.nl"]
11
+ spec.summary = %q{Composable samplers of random data}
12
+ spec.description = %q{Describe randomness and watch it blend}
13
+ spec.homepage = "https://github.com/praktijkindex/quick-sampler"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.7"
22
+ spec.add_development_dependency "rake", "~> 10.4"
23
+ spec.add_development_dependency "rspec", "~> 3.2"
24
+ spec.add_development_dependency "yard"
25
+ spec.add_development_dependency "redcarpet"
26
+
27
+ spec.add_dependency "activesupport"
28
+ end
@@ -0,0 +1,41 @@
1
+ describe Quick::Sampler do
2
+ specify do
3
+ expect(Quick::Sampler).to have_method :compile
4
+ end
5
+
6
+ context "given a simple script" do
7
+ let(:sampler) { Quick::Sampler.compile { const(:it_lives!) } }
8
+ it "compiles a sampler from a DSL script" do
9
+ expect(sampler).to be_a Quick::Sampler::Base
10
+ end
11
+ end
12
+
13
+ context "given a description: option" do
14
+ let(:sampler) {
15
+ Quick::Sampler.compile description: "wine sampler" do
16
+ one_of(:chiraz, :pinot_noir, :riesling)
17
+ end
18
+ }
19
+
20
+ it "sets description as sampler's description" do
21
+ expect(sampler.description).to be == "wine sampler"
22
+ end
23
+
24
+ it "makes the sampler to return description when inspected" do
25
+ expect(sampler.inspect).to be == "wine sampler"
26
+ end
27
+ end
28
+
29
+ context "given an unbound reference in the script" do
30
+ let(:external_thing) { 42 }
31
+ let(:sampler) {
32
+ Quick::Sampler.compile do
33
+ const(external_thing)
34
+ end
35
+ }
36
+
37
+ it "resolves it in the context compile was called in" do
38
+ expect(sampler.first).to be 42
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,62 @@
1
+ describe Quick::Sampler::DSL do
2
+ it { is_expected.to have_method :list_like }
3
+ let(:dsl) { subject }
4
+
5
+ describe "#list_like" do
6
+ let(:sampled_lists) {
7
+ dsl.list_like(*args).first(5)
8
+ }
9
+ let(:positive_integer) { dsl.positive_integer }
10
+ let(:negative_integer) { dsl.negative_integer }
11
+
12
+ context "given no arguments" do
13
+ let(:args) { [] }
14
+ it "repeats empty lists" do
15
+ expect(sampled_lists).to all match []
16
+ end
17
+ end
18
+
19
+ context "given only constants" do
20
+ let(:args) { [1, 2, 3] }
21
+ it "repeats args as is" do
22
+ expect(sampled_lists).to all eq args
23
+ end
24
+ end
25
+
26
+ context "given a list of samplers" do
27
+ let(:args) { [positive_integer, negative_integer] }
28
+ it "samples from each sampler" do
29
+ expect(sampled_lists).to all match [a_value > 0, a_value < 0]
30
+ end
31
+ end
32
+
33
+ context "given a combination of samplers and constants" do
34
+ let(:args) { ["text", positive_integer, negative_integer, 3.14159] }
35
+ it "keeps constants as is, but samples the samplers" do
36
+ expect(sampled_lists).to all match ["text", a_value > 0, a_value < 0, 3.14159]
37
+ end
38
+ end
39
+
40
+ context "given a hash with constant keys and values" do
41
+ let(:args) { [const: 42, another: 666] }
42
+ it "repeats the hash as is" do
43
+ expect(sampled_lists).to all match args
44
+ end
45
+ end
46
+
47
+ context "given a hash with sampler values" do
48
+ let(:args) { [positive_integer: positive_integer, negative_integer: negative_integer] }
49
+ it "samples the values" do
50
+ expect(sampled_lists).to all match [positive_integer: a_value > 0, negative_integer: a_value < 1]
51
+ end
52
+ end
53
+
54
+ context "given a nested array with samplers" do
55
+ let(:args) { ["outie", ["innie", positive_integer, negative_integer, 3.14159, option: positive_integer]] }
56
+ it "recurses into the array" do
57
+ expect(sampled_lists).to all match ["outie", ["innie", a_value > 0, a_value < 0, 3.14159, option: a_value > 0]]
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,58 @@
1
+ describe Quick::Sampler::DSL do
2
+ it { is_expected.to have_method :send_to }
3
+ let(:dsl) { subject }
4
+
5
+ describe "#send_to" do
6
+ let(:sampled_results) {
7
+ dsl.send_to(*args).first(5)
8
+ }
9
+ let(:two) { double("two", value: 2, string: "two") }
10
+ let(:three) { double("three", value: 3, string: "three") }
11
+ before do
12
+ allow(two).to receive(:times) { |x| 2 * x }
13
+ allow(three).to receive(:times) { |x| 3 * x }
14
+ end
15
+
16
+ context "given object and method" do
17
+ let(:args) { [two, :value] }
18
+ it "samples a method call result" do
19
+ expect(sampled_results).to all be == 2
20
+ end
21
+ end
22
+
23
+ context "given object, method and argument" do
24
+ let(:args) { [two, :times, 3] }
25
+ it "samples a method call result" do
26
+ expect(sampled_results).to all be == 6
27
+ end
28
+ end
29
+
30
+ context "given a sampler as the only argument" do
31
+ let(:args) { [ dsl.one_of([two, dsl.one_of(:value, :string)], [three, :times, 4]) ] }
32
+ it "samples the message argument" do
33
+ expect(sampled_results).to all eq(2).or eq("two").or eq(12)
34
+ end
35
+ end
36
+
37
+ context "given a sampler as the first argument" do
38
+ let(:args) { [two, dsl.one_of(:value, :string)] }
39
+ it "samples the object to send to" do
40
+ expect(sampled_results).to all eq(2).or eq("two")
41
+ end
42
+ end
43
+
44
+ context "given a sampler as a second argument" do
45
+ let(:args) { [dsl.one_of(two, three), :value] }
46
+ it "samples a message to send" do
47
+ expect(sampled_results).to all eq(2).or eq(3)
48
+ end
49
+ end
50
+
51
+ context "given a sampler as third argument" do
52
+ let(:args) { [two, :times, dsl.one_of(2,3,4)] }
53
+ it "samples the message argument" do
54
+ expect(sampled_results).to all eq(4).or eq(6).or eq(8)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
+ require "quick/sampler"
3
+
4
+ RSpec::Matchers.alias_matcher :have_method, :respond_to
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quick-sampler
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Artem Baguinski
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.4'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.4'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.2'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: redcarpet
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: activesupport
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ description: Describe randomness and watch it blend
98
+ email:
99
+ - abaguinski@depraktijkindex.nl
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - ".gitignore"
105
+ - ".rspec"
106
+ - ".travis.yml"
107
+ - ".yardopts"
108
+ - Gemfile
109
+ - LICENSE.txt
110
+ - README.md
111
+ - Rakefile
112
+ - lib/quick/sampler.rb
113
+ - lib/quick/sampler/autoload.rb
114
+ - lib/quick/sampler/base.rb
115
+ - lib/quick/sampler/config.rb
116
+ - lib/quick/sampler/dsl.rb
117
+ - lib/quick/sampler/dsl/character_class.rb
118
+ - lib/quick/sampler/dsl/fluidiom.rb
119
+ - lib/quick/sampler/dsl/simple_combinators.rb
120
+ - lib/quick/sampler/dsl/simple_values.rb
121
+ - lib/quick/sampler/shrink.rb
122
+ - lib/quick/sampler/shrink/class_methods/while.rb
123
+ - lib/quick/sampler/shrink/refinements.rb
124
+ - lib/quick/sampler/version.rb
125
+ - quick-sampler.gemspec
126
+ - spec/quick/sampler/compile_spec.rb
127
+ - spec/quick/sampler/dsl/list_like_spec.rb
128
+ - spec/quick/sampler/dsl/send_to_spec.rb
129
+ - spec/spec_helper.rb
130
+ homepage: https://github.com/praktijkindex/quick-sampler
131
+ licenses:
132
+ - MIT
133
+ metadata: {}
134
+ post_install_message:
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 2.2.2
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: Composable samplers of random data
154
+ test_files:
155
+ - spec/quick/sampler/compile_spec.rb
156
+ - spec/quick/sampler/dsl/list_like_spec.rb
157
+ - spec/quick/sampler/dsl/send_to_spec.rb
158
+ - spec/spec_helper.rb
159
+ has_rdoc: