quick-sampler 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: