propr 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/NOTES.md +62 -0
  2. data/README.md +553 -0
  3. data/Rakefile +83 -0
  4. data/TODO.md +64 -0
  5. data/lib/propr.rb +123 -0
  6. data/lib/propr/dsl.rb +6 -0
  7. data/lib/propr/dsl/check.rb +49 -0
  8. data/lib/propr/dsl/property.rb +62 -0
  9. data/lib/propr/property.rb +23 -0
  10. data/lib/propr/random.rb +143 -0
  11. data/lib/propr/random/array.rb +19 -0
  12. data/lib/propr/random/bigdecimal.rb +43 -0
  13. data/lib/propr/random/boolean.rb +7 -0
  14. data/lib/propr/random/complex.rb +0 -0
  15. data/lib/propr/random/date.rb +17 -0
  16. data/lib/propr/random/float.rb +60 -0
  17. data/lib/propr/random/hash.rb +55 -0
  18. data/lib/propr/random/integer.rb +38 -0
  19. data/lib/propr/random/maybe.rb +0 -0
  20. data/lib/propr/random/nil.rb +8 -0
  21. data/lib/propr/random/range.rb +32 -0
  22. data/lib/propr/random/rational.rb +0 -0
  23. data/lib/propr/random/set.rb +22 -0
  24. data/lib/propr/random/string.rb +41 -0
  25. data/lib/propr/random/symbol.rb +13 -0
  26. data/lib/propr/random/time.rb +14 -0
  27. data/lib/propr/rspec.rb +97 -0
  28. data/lib/propr/runner.rb +53 -0
  29. data/lib/propr/shrink/array.rb +16 -0
  30. data/lib/propr/shrink/bigdecimal.rb +17 -0
  31. data/lib/propr/shrink/boolean.rb +11 -0
  32. data/lib/propr/shrink/complex.rb +0 -0
  33. data/lib/propr/shrink/date.rb +12 -0
  34. data/lib/propr/shrink/float.rb +17 -0
  35. data/lib/propr/shrink/hash.rb +18 -0
  36. data/lib/propr/shrink/integer.rb +10 -0
  37. data/lib/propr/shrink/maybe.rb +11 -0
  38. data/lib/propr/shrink/nil.rb +5 -0
  39. data/lib/propr/shrink/object.rb +5 -0
  40. data/lib/propr/shrink/range.rb +4 -0
  41. data/lib/propr/shrink/rational.rb +4 -0
  42. data/lib/propr/shrink/set.rb +18 -0
  43. data/lib/propr/shrink/string.rb +19 -0
  44. data/lib/propr/shrink/symbol.rb +5 -0
  45. data/lib/propr/shrink/time.rb +9 -0
  46. data/spec/examples/choose/array.example +12 -0
  47. data/spec/examples/choose/hash.example +12 -0
  48. data/spec/examples/choose/range.example +13 -0
  49. data/spec/examples/choose/set.example +12 -0
  50. data/spec/examples/guard.example +38 -0
  51. data/spec/examples/random/array.example +38 -0
  52. data/spec/examples/random/hash.example +18 -0
  53. data/spec/examples/random/integer.example +23 -0
  54. data/spec/examples/random/range.example +43 -0
  55. data/spec/examples/scale.example +17 -0
  56. data/spec/examples/shrink/array.example +20 -0
  57. data/spec/examples/shrink/bigdecimal.example +20 -0
  58. data/spec/examples/shrink/float.example +20 -0
  59. data/spec/examples/shrink/hash.example +20 -0
  60. data/spec/examples/shrink/integer.example +21 -0
  61. data/spec/examples/shrink/maybe.example +24 -0
  62. data/spec/examples/shrink/set.example +21 -0
  63. data/spec/examples/shrink/string.example +17 -0
  64. data/spec/issues/003.example +9 -0
  65. data/spec/spec_helper.rb +24 -0
  66. 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
@@ -0,0 +1,7 @@
1
+ class Boolean
2
+ def self.random(m = Propr::Random)
3
+ m.bind(m.rand) do |n|
4
+ m.unit(n > 0.5)
5
+ end
6
+ end
7
+ 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,8 @@
1
+ class NilClass
2
+ end
3
+
4
+ class << NilClass
5
+ def random(m = Propr::Random)
6
+ m.unit(nil)
7
+ end
8
+ end
@@ -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
@@ -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