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