propr 0.2.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.
- data/NOTES.md +62 -0
- data/README.md +553 -0
- data/Rakefile +83 -0
- data/TODO.md +64 -0
- data/lib/propr.rb +123 -0
- data/lib/propr/dsl.rb +6 -0
- data/lib/propr/dsl/check.rb +49 -0
- data/lib/propr/dsl/property.rb +62 -0
- data/lib/propr/property.rb +23 -0
- data/lib/propr/random.rb +143 -0
- data/lib/propr/random/array.rb +19 -0
- data/lib/propr/random/bigdecimal.rb +43 -0
- data/lib/propr/random/boolean.rb +7 -0
- data/lib/propr/random/complex.rb +0 -0
- data/lib/propr/random/date.rb +17 -0
- data/lib/propr/random/float.rb +60 -0
- data/lib/propr/random/hash.rb +55 -0
- data/lib/propr/random/integer.rb +38 -0
- data/lib/propr/random/maybe.rb +0 -0
- data/lib/propr/random/nil.rb +8 -0
- data/lib/propr/random/range.rb +32 -0
- data/lib/propr/random/rational.rb +0 -0
- data/lib/propr/random/set.rb +22 -0
- data/lib/propr/random/string.rb +41 -0
- data/lib/propr/random/symbol.rb +13 -0
- data/lib/propr/random/time.rb +14 -0
- data/lib/propr/rspec.rb +97 -0
- data/lib/propr/runner.rb +53 -0
- data/lib/propr/shrink/array.rb +16 -0
- data/lib/propr/shrink/bigdecimal.rb +17 -0
- data/lib/propr/shrink/boolean.rb +11 -0
- data/lib/propr/shrink/complex.rb +0 -0
- data/lib/propr/shrink/date.rb +12 -0
- data/lib/propr/shrink/float.rb +17 -0
- data/lib/propr/shrink/hash.rb +18 -0
- data/lib/propr/shrink/integer.rb +10 -0
- data/lib/propr/shrink/maybe.rb +11 -0
- data/lib/propr/shrink/nil.rb +5 -0
- data/lib/propr/shrink/object.rb +5 -0
- data/lib/propr/shrink/range.rb +4 -0
- data/lib/propr/shrink/rational.rb +4 -0
- data/lib/propr/shrink/set.rb +18 -0
- data/lib/propr/shrink/string.rb +19 -0
- data/lib/propr/shrink/symbol.rb +5 -0
- data/lib/propr/shrink/time.rb +9 -0
- data/spec/examples/choose/array.example +12 -0
- data/spec/examples/choose/hash.example +12 -0
- data/spec/examples/choose/range.example +13 -0
- data/spec/examples/choose/set.example +12 -0
- data/spec/examples/guard.example +38 -0
- data/spec/examples/random/array.example +38 -0
- data/spec/examples/random/hash.example +18 -0
- data/spec/examples/random/integer.example +23 -0
- data/spec/examples/random/range.example +43 -0
- data/spec/examples/scale.example +17 -0
- data/spec/examples/shrink/array.example +20 -0
- data/spec/examples/shrink/bigdecimal.example +20 -0
- data/spec/examples/shrink/float.example +20 -0
- data/spec/examples/shrink/hash.example +20 -0
- data/spec/examples/shrink/integer.example +21 -0
- data/spec/examples/shrink/maybe.example +24 -0
- data/spec/examples/shrink/set.example +21 -0
- data/spec/examples/shrink/string.example +17 -0
- data/spec/issues/003.example +9 -0
- data/spec/spec_helper.rb +24 -0
- metadata +143 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
class Array
|
2
|
+
def random(options = {}, m = Propr::Random)
|
3
|
+
m.bind(m.rand(size)) do |index|
|
4
|
+
m.unit(self[index])
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class << Array
|
10
|
+
def random(options = {}, m = Propr::Random)
|
11
|
+
min = options[:min] || 0
|
12
|
+
max = options[:max] || 10
|
13
|
+
item = yield
|
14
|
+
|
15
|
+
m.bind(Integer.random(min: min, max: max, center: min)) do |size|
|
16
|
+
m.sequence([item]*size)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "bigdecimal"
|
2
|
+
|
3
|
+
class << BigDecimal
|
4
|
+
INF = BigDecimal("Infinity")
|
5
|
+
NAN = BigDecimal("NaN")
|
6
|
+
|
7
|
+
# @return [BigDecimal]
|
8
|
+
def random(options = {}, m = Propr::Random)
|
9
|
+
min = BigDecimal(options[:min] || -INF)
|
10
|
+
max = BigDecimal(options[:max] || INF)
|
11
|
+
|
12
|
+
min_ = if min.finite? then min else BigDecimal(-Float::MAX, 0) end
|
13
|
+
max_ = if max.finite? then max else BigDecimal( Float::MAX, 0) end
|
14
|
+
|
15
|
+
range = max_ - min_
|
16
|
+
center = options.fetch(:center, :mid)
|
17
|
+
center =
|
18
|
+
case center
|
19
|
+
when :mid then min_ + (max_ - min_).div(2)
|
20
|
+
when :min then min_
|
21
|
+
when :max then max_
|
22
|
+
when Numeric
|
23
|
+
raise ArgumentError,
|
24
|
+
"center < min" if center < min
|
25
|
+
raise ArgumentError,
|
26
|
+
"center > max" if center > max
|
27
|
+
center
|
28
|
+
else raise ArgumentError,
|
29
|
+
"center must be :min, :mid, :max, or min <= Integer <= max"
|
30
|
+
end
|
31
|
+
|
32
|
+
# @todo: -INF, +INF, -0.0, NAN
|
33
|
+
m.bind(m.rand(range)) do |a|
|
34
|
+
m.bind(m.rand(max_ - min_)) do |b|
|
35
|
+
c = BigDecimal(a) + BigDecimal(min_, 0)
|
36
|
+
c += c / BigDecimal(b) # not 0..1
|
37
|
+
c = max if c > max
|
38
|
+
c = min if c < min
|
39
|
+
m.scale(c, range, center)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
File without changes
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "date"
|
2
|
+
|
3
|
+
class << Date
|
4
|
+
MIN = Date.jd(1721058) # 0000-01-01
|
5
|
+
MAX = Date.jd(5373484) # 9999-12-31
|
6
|
+
|
7
|
+
def random(options = {}, m = Propr::Random)
|
8
|
+
# These would be constants but `jd` is only defined
|
9
|
+
# after `require "date"`.
|
10
|
+
min = (options[:min] || MIN).jd
|
11
|
+
max = (options[:max] || MAX).jd
|
12
|
+
|
13
|
+
m.bind(Integer.random(options.merge(min: min, max: max))) do |n|
|
14
|
+
m.unit(jd(n))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require "bigdecimal"
|
2
|
+
|
3
|
+
class << Float
|
4
|
+
# @return [Float]
|
5
|
+
def random(options = {}, m = Propr::Random)
|
6
|
+
min = (options[:min] || -Float::INFINITY).to_f
|
7
|
+
max = (options[:max] || Float::INFINITY).to_f
|
8
|
+
|
9
|
+
min_ = if min.finite? then min else -Float::MAX end
|
10
|
+
max_ = if max.finite? then max else Float::MAX end
|
11
|
+
|
12
|
+
range = max_.to_i - min_.to_i
|
13
|
+
center = options.fetch(:center, :mid)
|
14
|
+
center =
|
15
|
+
case center
|
16
|
+
when :min then min
|
17
|
+
when :max then max
|
18
|
+
when :mid
|
19
|
+
if (max_ - min_).finite?
|
20
|
+
min_ + (max_ - min_).fdiv(2)
|
21
|
+
else
|
22
|
+
(min_ + max_).fdiv(2)
|
23
|
+
end
|
24
|
+
when Numeric
|
25
|
+
raise ArgumentError,
|
26
|
+
"center < min" if center < min
|
27
|
+
raise ArgumentError,
|
28
|
+
"center > max" if center > max
|
29
|
+
center
|
30
|
+
else raise ArgumentError,
|
31
|
+
"center must be :min, :mid, :max, or min <= Integer <= max"
|
32
|
+
end
|
33
|
+
|
34
|
+
# @todo: -Float::INFINITY, +Float::INFINITY, -0.0, Float::NAN
|
35
|
+
|
36
|
+
# One approach is to count all `n` possible Float values inside
|
37
|
+
# the [min, max] interval (n <= 2^64 for double precision), then
|
38
|
+
# generate a Fixnum inside [0, n] and map it back to the nth
|
39
|
+
# Float value in [min, max].
|
40
|
+
#
|
41
|
+
# Instead, this method just counts the `n` possible whole values
|
42
|
+
# inside [min, max], generates a Fixnum inside [0, n - 1], then
|
43
|
+
# maps that back to the nth whole value inside [min, max - 1].
|
44
|
+
# Next we tack on a random fractional value inside [0, 1).
|
45
|
+
m.bind(m.rand(range)) do |whole|
|
46
|
+
m.bind(m.rand) do |fraction|
|
47
|
+
# Need to perform scaling on BigDecimal to prevent floating
|
48
|
+
# point underflow and overflow (e.g., BIG / HUGE == 0.0, and
|
49
|
+
# BIG + small == BIG).
|
50
|
+
value = BigDecimal(min_, 15) + whole
|
51
|
+
value += BigDecimal(fraction, 0)
|
52
|
+
center = BigDecimal(center, 0)
|
53
|
+
|
54
|
+
m.bind(m.scale(value, range, center)) do |d|
|
55
|
+
m.unit(d.to_f)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
class Hash
|
2
|
+
def random(m = Propr::Random)
|
3
|
+
m.bind(keys.random) do |k|
|
4
|
+
m.unit([k, self[k]])
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class << Hash
|
10
|
+
|
11
|
+
# @example
|
12
|
+
# Hash.random do
|
13
|
+
# m.bind(Integer.random){|k| m.bind(String.random){|v| m.unit([k,v]) }}
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# Hash.random do
|
18
|
+
# m.sequence([Integer.random, String.random])
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
def random(options = {}, m = Propr::Random)
|
22
|
+
min = options[:min] || 0
|
23
|
+
max = options[:max] || 10
|
24
|
+
pair = yield
|
25
|
+
|
26
|
+
# @todo: Be sure we created enough *unique* keys
|
27
|
+
#
|
28
|
+
# Hash.random(min: 10) do
|
29
|
+
# # key space could have at most 6 elements
|
30
|
+
# m.sequence([Integer.random(min: 0, max: 5), String.random])
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
m.bind(Integer.random(options.merge(min: min, max: max))) do |size|
|
34
|
+
m.bind(m.sequence([pair]*size)) do |pairs|
|
35
|
+
m.unit(Hash[pairs])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# @example
|
41
|
+
# Hash.random_vals \
|
42
|
+
# name: String.random,
|
43
|
+
# count: Integer.random(min: 0),
|
44
|
+
# weight: Float.random
|
45
|
+
#
|
46
|
+
def random_vals(hash, m = Propr::Random)
|
47
|
+
# Convert hash of key => generator to a list of pair-generators,
|
48
|
+
# where the pairs correspond to the original set of [key, value]
|
49
|
+
pairs = hash.map{|k,v| m.bind(v){|v| m.unit([k, v]) }}
|
50
|
+
|
51
|
+
m.bind(m.sequence(pairs)) do |pairs|
|
52
|
+
m.unit(Hash[pairs])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Integer
|
2
|
+
MAX = 2 ** (0.size * 8 - 2) - 1
|
3
|
+
MIN = -MAX + 1
|
4
|
+
end
|
5
|
+
|
6
|
+
class << Integer
|
7
|
+
def random(options = {}, m = Propr::Random)
|
8
|
+
min = (options[:min] || Integer::MIN).to_i
|
9
|
+
max = (options[:max] || Integer::MAX).to_i
|
10
|
+
|
11
|
+
raise ArgumentError,
|
12
|
+
"min > max" if min > max
|
13
|
+
|
14
|
+
range = max - min
|
15
|
+
center = options.fetch(:center, :mid)
|
16
|
+
center =
|
17
|
+
case center
|
18
|
+
when :mid then min + (max - min).div(2)
|
19
|
+
when :min then min
|
20
|
+
when :max then max
|
21
|
+
when Numeric
|
22
|
+
raise ArgumentError,
|
23
|
+
"center < min" if center < min
|
24
|
+
raise ArgumentError,
|
25
|
+
"center > max" if center > max
|
26
|
+
center
|
27
|
+
else raise ArgumentError,
|
28
|
+
"center must be :min, :mid, :max, or min <= Integer <= max"
|
29
|
+
end
|
30
|
+
|
31
|
+
m.bind(m.rand(range + 1)) do |rnd|
|
32
|
+
m.bind(m.scale(rnd + min, range, BigDecimal(center))) do |n|
|
33
|
+
# Round up or down to integer nearest center
|
34
|
+
m.unit(n > center ? n.floor : n.ceil)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
File without changes
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class Range
|
2
|
+
def random(options = {})
|
3
|
+
# @todo: This won't work for some types, e.g. String.
|
4
|
+
# @todo: Should this be skewed/scaled?
|
5
|
+
min.class.random(options.merge(min: min, max: max))
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class << Range
|
10
|
+
def random(options = {}, m = Propr::Random)
|
11
|
+
random =
|
12
|
+
if block_given?
|
13
|
+
yield
|
14
|
+
else
|
15
|
+
min = options[:min]
|
16
|
+
max = options[:max]
|
17
|
+
min or max or raise ArgumentError,
|
18
|
+
"must provide min, max, or block"
|
19
|
+
(min or max).class.random(options)
|
20
|
+
end
|
21
|
+
|
22
|
+
m.bind(random) do |a|
|
23
|
+
m.bind(random) do |b|
|
24
|
+
if options.fetch(:inclusive?, rand > 0.5)
|
25
|
+
m.unit(a..b)
|
26
|
+
else
|
27
|
+
m.unit(a...b)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
File without changes
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Set
|
2
|
+
def random(options = {}, m = Propr::Random)
|
3
|
+
m.bind(m.rand(size)) do |index|
|
4
|
+
m.unit(self.to_a[index])
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class << Set
|
10
|
+
def random(options = {}, m = Propr::Random)
|
11
|
+
min = options[:min] || 0
|
12
|
+
max = options[:max] || 10
|
13
|
+
item = yield
|
14
|
+
|
15
|
+
# @todo: Be sure we created enough *unique* elements
|
16
|
+
m.bind(Integer.random(options.merge(min: min, max: max))) do |size|
|
17
|
+
m.bind(m.sequence([item]*size)) do |xs|
|
18
|
+
m.unit(xs.to_set)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class << String
|
2
|
+
module Characters
|
3
|
+
ASCII = (0..127).inject("", &:<<)
|
4
|
+
ALL = (0..255).inject("", &:<<)
|
5
|
+
|
6
|
+
def self.of(regexp, set = Characters::ALL)
|
7
|
+
CLASSES[regexp] || set.scan(regexp)
|
8
|
+
end
|
9
|
+
|
10
|
+
CLASSES = Hash.new
|
11
|
+
CLASSES.update \
|
12
|
+
:any => ALL.split(//),
|
13
|
+
:ascii => ASCII.split(//),
|
14
|
+
:alnum => Characters.of(/[[:alnum:]]/),
|
15
|
+
:alpha => Characters.of(/[[:alpha:]]/),
|
16
|
+
:blank => Characters.of(/[[:blank:]]/),
|
17
|
+
:cntrl => Characters.of(/[[:cntrl:]]/),
|
18
|
+
:digit => Characters.of(/[[:digit:]]/),
|
19
|
+
:graph => Characters.of(/[[:graph:]]/),
|
20
|
+
:lower => Characters.of(/[[:lower:]]/),
|
21
|
+
:print => Characters.of(/[[:print:]]/),
|
22
|
+
:punct => Characters.of(/[[:punct:]]/),
|
23
|
+
:space => Characters.of(/[[:space:]]/),
|
24
|
+
:upper => Characters.of(/[[:upper:]]/),
|
25
|
+
:xdigit => Characters.of(/[[:xdigit:]]/)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [String]
|
29
|
+
def random(options = {}, m = Propr::Random)
|
30
|
+
min = options[:min] || 0
|
31
|
+
max = options[:max] || 10
|
32
|
+
options = Hash[center: min].merge(options)
|
33
|
+
charset = Characters.of(options.fetch(:charset, :print))
|
34
|
+
|
35
|
+
m.bind(Integer.random(options.merge(min: min, max: max))) do |size|
|
36
|
+
m.bind(m.sequence(size.times.map { charset.random })) do |chars|
|
37
|
+
m.unit(chars.join)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class << Symbol
|
2
|
+
# @note: Beware of memory consumption. Symbols are never garbage
|
3
|
+
# collected, and we're generating them at random!
|
4
|
+
#
|
5
|
+
def random(options = {}, m = Propr::Random)
|
6
|
+
min = options[:min] || 0
|
7
|
+
max = options[:max] || 10
|
8
|
+
options = Hash[charset: /[a-z_]/]
|
9
|
+
m.bind(String.random(options)) do |s|
|
10
|
+
m.unit(s.to_sym)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class << Time
|
2
|
+
# MIN = Time.at(0) # 1969-12-31 00:00:00 UTC
|
3
|
+
MIN = Time.at(-30610224000) # 1000-01-01 00:00:00 UTC
|
4
|
+
MAX = Time.at(253402300799) # 9999-12-31 23:59:59 UTC
|
5
|
+
|
6
|
+
def random(options = {}, m = Propr::Random)
|
7
|
+
min = (options[:min] || MIN).to_f
|
8
|
+
max = (options[:max] || MAX).to_f
|
9
|
+
|
10
|
+
m.bind(Float.random(options.merge(min: min, max: max))) do |ms|
|
11
|
+
m.unit(at(ms))
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/propr/rspec.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
module Propr
|
2
|
+
|
3
|
+
class RSpecAdapter
|
4
|
+
def initialize(group, options, property)
|
5
|
+
@options, @group, @property =
|
6
|
+
options, group, property
|
7
|
+
|
8
|
+
# Run each property 100 times, allow 50 retries, and
|
9
|
+
# start the scale at 0, grow suddenly towards the end
|
10
|
+
@runner = Runner.new(100, 50,
|
11
|
+
# lambda{|p,s,t,_| (p+s <= t ? p+s : t) / t })
|
12
|
+
lambda{|p,s,t,_| (BigDecimal(p+s <= t ? p+s : t) / t) })
|
13
|
+
end
|
14
|
+
|
15
|
+
def check(*args, &generator)
|
16
|
+
m = self
|
17
|
+
runner = @runner
|
18
|
+
property = @property
|
19
|
+
|
20
|
+
if block_given?
|
21
|
+
location = location(generator)
|
22
|
+
|
23
|
+
@group.example(@property.name, @options.merge(caller: location)) do
|
24
|
+
success, passed, skipped, counterex =
|
25
|
+
runner.run(property, generator)
|
26
|
+
|
27
|
+
unless success
|
28
|
+
if skipped >= runner.maxskip
|
29
|
+
raise NoMoreTries.new(runner.maxskip), nil, location
|
30
|
+
else
|
31
|
+
raise Falsifiable.new(counterex, m.shrink(counterex), passed, skipped), nil, location
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
else
|
36
|
+
location = location(caller)
|
37
|
+
|
38
|
+
@group.example(@property.name, @options.merge(caller: location)) do
|
39
|
+
unless property.call(*args)
|
40
|
+
raise Falsifiable.new(args, m.shrink(args), 0, 0), nil, location
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return `self` to allow chaining calls to `check`
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
# TODO: this method is not RSpec specific
|
50
|
+
def shrink(counterex)
|
51
|
+
if @property.arity.zero?
|
52
|
+
return []
|
53
|
+
end
|
54
|
+
|
55
|
+
xs = [counterex]
|
56
|
+
|
57
|
+
while true
|
58
|
+
# Generate simpler examples
|
59
|
+
ys = Array.bind(xs) do |args|
|
60
|
+
head, *tail = args.map{|x| x.respond_to?(:shrink) ? x.shrink : [x] }
|
61
|
+
head.product(*tail)
|
62
|
+
end
|
63
|
+
|
64
|
+
zs = []
|
65
|
+
|
66
|
+
# Collect counter examples
|
67
|
+
until ys.empty? or zs.length >= 10
|
68
|
+
args = ys.delete_at(rand(ys.size))
|
69
|
+
|
70
|
+
unless @property.call(*args)
|
71
|
+
zs.push(args)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if zs.empty?
|
76
|
+
# No simpler counter examples
|
77
|
+
return xs.first
|
78
|
+
else
|
79
|
+
# Try to further simplify these
|
80
|
+
xs = zs
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def location(data)
|
88
|
+
case data
|
89
|
+
when Proc
|
90
|
+
["#{data.source_location.join(":")}"]
|
91
|
+
when Array
|
92
|
+
[data.first]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|