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.
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