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,53 @@
1
+ module Propr
2
+ class Runner
3
+ attr_reader :minpass, :maxskip
4
+
5
+ def initialize(minpass, maxskip, scale)
6
+ @minpass, @maxskip, @scale =
7
+ minpass, maxskip, scale || lambda {|*_| 1 }
8
+ end
9
+
10
+ def run(property, generator)
11
+ passed = 0
12
+ skipped = 0
13
+ wrapped = Dsl::Check.wrap(generator)
14
+
15
+ until passed >= @minpass or skipped >= @maxskip
16
+ input, _, success =
17
+ Random.run(wrapped, @scale.call(passed, skipped, @minpass, @maxskip))
18
+
19
+ # Generator should've returned an argument list. Except, for convenience,
20
+ # single-argument properties should have generators which return a single
21
+ # value, not an argument list, and we'll make it an argument list *here*.
22
+ input = property.arity == 1 ?
23
+ [input] : input
24
+
25
+ if success
26
+ begin
27
+ result = property.call(*input)
28
+ # result = property.arity == 1 ?
29
+ # property.call(input) : property.call(*input)
30
+
31
+ if result
32
+ passed += 1
33
+ else
34
+ # Falsifiable
35
+ return [false, passed, skipped, input]
36
+ end
37
+ rescue GuardFailure => e
38
+ # GuardFailure in property
39
+ skipped += 1
40
+ rescue
41
+ raise Failure.new($!, input, nil, passed, skipped)#, nil, location
42
+ end
43
+ else
44
+ # GuardFailure in generator
45
+ skipped += 1
46
+ end
47
+ end
48
+
49
+ # Might have not passed enough tests
50
+ [passed >= @minpass, passed, skipped, nil]
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,16 @@
1
+ class Array
2
+ # @return [Array<Array>]
3
+ def shrink
4
+ return Array.new if empty?
5
+
6
+ combination(size - 1).to_a.tap do |shrunken|
7
+ shrunken << []
8
+ size.times do |n|
9
+ head = self[0, n]
10
+ tail = self[n+1..-1]
11
+ item = self[n]
12
+ shrunken.concat(item.shrink.map{|m| head + [m] + tail })
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ require "bigdecimal"
2
+
3
+ class BigDecimal
4
+ # @return [Array<BigDecimal>]
5
+ def shrink
6
+ limit = 10
7
+
8
+ Array.unfold(self) do |seed|
9
+ limit -= 1
10
+ zero = 0
11
+ seed_ = zero + (seed - zero) / 2
12
+
13
+ (limit > 0 && (seed - seed_).abs > 1e-5)
14
+ .maybe([self + zero - seed, seed_])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ class TrueClass
2
+ def shrink
3
+ [false]
4
+ end
5
+ end
6
+
7
+ class FalseClass
8
+ def shrink
9
+ []
10
+ end
11
+ end
File without changes
@@ -0,0 +1,12 @@
1
+ require "date"
2
+
3
+ class Date
4
+ # @return [Array<Date>]
5
+ def shrink
6
+ Array.unfold(jd) do |seed|
7
+ zero = 2415021 # 1900-01-01
8
+ seed_ = zero + (seed - zero) / 2
9
+ (seed != seed_).maybe([self + zero - seed, seed_])
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ require "bigdecimal"
2
+
3
+ class Float
4
+ # @return [Array<Float>]
5
+ def shrink
6
+ limit = 10
7
+
8
+ Array.unfold(self) do |seed|
9
+ limit -= 1
10
+ zero = 0
11
+ seed_ = zero + (seed - zero) / 2
12
+
13
+ (limit > 0 && (seed - seed_).abs >= 1e-5)
14
+ .maybe([self + zero - seed, seed_])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ class Hash
2
+ # @return [Array<Hash>]
3
+ def shrink
4
+ return Array.new if empty?
5
+
6
+ array = to_a
7
+ array.combination(size - 1).map{|pairs| Hash[pairs] }.tap do |shrunken|
8
+ shrunken << Hash.new
9
+
10
+ size.times do |n|
11
+ head = array[0, n]
12
+ tail = array[n+1..-1]
13
+ k, v = array[n]
14
+ shrunken.concat(v.shrink.map{|m| Hash[head + [[k, m]] + tail] })
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ class Integer
2
+ # @return [Enumerator<Integer>]
3
+ def shrink
4
+ Array.unfold(self) do |seed|
5
+ zero = 0
6
+ seed_ = zero + (seed - zero) / 2
7
+ (seed != seed_).maybe([zero + self - seed, seed_])
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,11 @@
1
+ class Fr::Maybe::Some
2
+ def shrink
3
+ [Fr.none, map(&:shrink)]
4
+ end
5
+ end
6
+
7
+ class Fr::Maybe::None_
8
+ def shrink
9
+ []
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class NilClass
2
+ def shrink
3
+ []
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ class Object
2
+ def shrink
3
+ []
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ class Range
2
+ # def shrink
3
+ # end
4
+ end
@@ -0,0 +1,4 @@
1
+ class Rational
2
+ # def shrink
3
+ # end
4
+ end
@@ -0,0 +1,18 @@
1
+ class Set
2
+ # @return [Array<Set>]
3
+ def shrink
4
+ return Array.new if empty?
5
+
6
+ array = to_a
7
+ array.combination(size - 1).map(&:to_set).tap do |shrunken|
8
+ shrunken << Set.new
9
+
10
+ size.times do |n|
11
+ head = array[0, n]
12
+ tail = array[n+1..-1]
13
+ item = array[n]
14
+ shrunken.concat(item.shrink.map{|m| (head + [m] + tail).to_set })
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ class String
2
+ # @return [Array<String>]
3
+ def shrink
4
+ case size
5
+ when 0 then []
6
+ when 1
7
+ shrunken = []
8
+ shrunken << downcase if self =~ /[[:upper:]]/
9
+ shrunken << " " if self =~ /(?! )\s/
10
+ shrunken << "a" if self =~ /[b-z]/
11
+ shrunken << "A" if self =~ /[B-Z]/
12
+ shrunken << "0" if self =~ /[1-9]/
13
+ shrunken << ""
14
+ shrunken
15
+ else
16
+ split(//).shrink.map(&:join)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ class Symbol
2
+ def shrink
3
+ to_s.shrink.map(&:to_sym)
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ class Time
2
+ def shrink
3
+ Array.unfold(to_f) do |seed|
4
+ zero = 0 # 1969-12-31 00:00:00 UTC
5
+ seed_ = zero + (seed - zero) / 2
6
+ ((seed - seed_).abs > 1e-2).maybe([self + zero - seed, seed_])
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ require "spec_helper"
2
+
3
+ describe Array, "#random" do
4
+
5
+ property("empty") { error? { Array.new.random }}
6
+ .check
7
+
8
+ property("member?"){|xs| guard(!xs.empty?); xs.member?(m.eval xs.random) }
9
+ .check { Array.random(min: 1) { Integer.random }}
10
+ .check { Array.random(min: 1) { String.random }}
11
+
12
+ end
@@ -0,0 +1,12 @@
1
+ require "spec_helper"
2
+
3
+ describe Hash, "#random" do
4
+
5
+ property("empty") { error? { Hash.new.random }}
6
+ .check
7
+
8
+ property("member?"){|xs| guard(!xs.empty?); k, v = m.eval(xs.random); xs.member?(k) }
9
+ .check { Hash.random { sequence [Integer.random, String.random] }}
10
+ .check { Hash.random { sequence [String.random, Integer.random] }}
11
+
12
+ end
@@ -0,0 +1,13 @@
1
+ require "spec_helper"
2
+
3
+ describe Range, "#random" do
4
+
5
+ property("empty") {|x| fails? { (x...x).random }}
6
+ # .check { Integer.random }
7
+ # .check { String.random }
8
+
9
+ property("member?"){|xs| guard(!xs.empty?); xs.member?(xs.random) }
10
+ # .check { Range.random(min: Integer.random, max: Integer.random) }
11
+ # .check { Range.random(min: String.random, max: String.random) }
12
+
13
+ end
@@ -0,0 +1,12 @@
1
+ require "spec_helper"
2
+
3
+ describe Set, "#random" do
4
+
5
+ property("empty") { fails? { Set.new.random }}
6
+ # .check
7
+
8
+ property("member?"){|xs| guard(!xs.empty?); xs.member?(xs.random) }
9
+ # .check { Set.random { Integer.random }}
10
+ # .check { Set.random { String.random }}
11
+
12
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe Propr, "guard" do
4
+
5
+ # @todo: Not happy with this scoping issue. We should
6
+ # be able to use let(:int) { ... } instead.
7
+ def self.int
8
+ (0..100).random
9
+ end
10
+
11
+ property("callable from property") do
12
+ guard(true)
13
+ true
14
+ end.check
15
+
16
+ property("callable from check") do |x|
17
+ true
18
+ end.check { guard(true) }
19
+
20
+ property("skips test on unsuitable input") do |n|
21
+ guard(n > 10)
22
+ n > 10
23
+ end.check { int }
24
+
25
+ property("supresses unsuitable input") do |n|
26
+ n > 10
27
+ end.check { bind(int){|n| bind(guard(n > 10)) { unit(n) }}}
28
+
29
+ property("conjunction") do |n|
30
+ guard(n > 10, n < 90)
31
+ n > 10 && n < 90
32
+ end.check { int }
33
+
34
+ property("conjunction") do |n|
35
+ n > 10 && n < 90
36
+ end.check { bind(int){|n| bind(guard(n > 10, n < 90)) { unit(n) }}}
37
+
38
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe Array, ".random" do
4
+
5
+ describe "min and max" do
6
+ property("bounded"){|a,b| guard a <= b; m.eval(Array.random(min: a, max: b) { m.unit nil }).length >= a }
7
+ .check{ sequence [Integer.random(min: 0, max: 10), Integer.random(min: 10, max: 50)] }
8
+
9
+ property("bounded"){|a,b| guard a <= b; m.eval(Array.random(min: a, max: b) { m.unit nil }).length <= b }
10
+ .check{ sequence [Integer.random(min: 0, max: 10), Integer.random(min: 10, max: 50)] }
11
+
12
+ # @todo: only run once
13
+ property("empty"){|xs| xs.empty? }
14
+ .check{ Array.random(min: 0, max: 0) { unit nil }}
15
+
16
+ # @todo: only run once
17
+ property("singleton"){|xs| xs == [nil] }
18
+ .check{ Array.random(min: 1, max: 1) { unit nil }}
19
+ end
20
+
21
+ describe "items" do
22
+ property("sequence"){|xs| xs.uniq.length <= 1 }
23
+ .check{ x = 0; Array.random { unit(x += 1) }}
24
+
25
+ property("sequence"){|xs| xs.empty? or xs.first == 1 }
26
+ .check{ x = 0; Array.random { unit(x += 1) }}
27
+
28
+ property("sequence"){|xs| xs.empty? or xs.last == 1 }
29
+ .check{ x = 0; Array.random { unit(x += 1) }}
30
+
31
+ property("repeat"){|xs| xs.uniq.length <= 1 }
32
+ .check{ Array.random { unit(100) }}
33
+
34
+ property("repeat"){|xs| xs == [100]*xs.length }
35
+ .check{ Array.random { unit(100) }}
36
+ end
37
+
38
+ end
@@ -0,0 +1,18 @@
1
+ require "spec_helper"
2
+
3
+ describe Hash, ".random_vals" do
4
+
5
+ generators = [String, Integer, Float, Boolean, Time].map(&:random)
6
+
7
+ property("preserves hash keys"){|xs| xs.keys == m.eval(Hash.random_vals(xs)).keys }
8
+ .check(Hash[a: Integer.random, b: Propr::Random.unit(0)])
9
+ .check{ Hash.random { sequence [generators.random, unit(unit(0))] }}
10
+
11
+ property("evaluates hash values"){|xs| m.eval(Hash.random_vals(xs)).values.all?{|v| String === v }}
12
+ .check(Hash[a: String.random, b: String.random])
13
+ # .check{ Hash.random { sequence [Symbol.random, unit(String.random)] }}
14
+
15
+ property("evaluates hash values"){|xs| m.eval(Hash.random_vals(xs)).values.all?{|v| v == 0 }}
16
+ .check{ Hash.random { sequence [Symbol.random, unit(unit(0))] }}
17
+
18
+ end
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ describe Integer, ".random" do
4
+
5
+ property("min"){|n| n >= 0 }
6
+ .check{ Integer.random(min: 0) }
7
+
8
+ property("max"){|n| n <= 0 }
9
+ .check{ Integer.random(max: 0) }
10
+
11
+ property("bounded"){|a,b| guard a <= b; m.eval(Integer.random(min: a, max: b)) >= a }
12
+ .check{ bind(sequence [Integer.random, Integer.random]){|xs| unit xs.sort }}
13
+
14
+ property("bounded"){|a,b| guard a <= b; m.eval(Integer.random(min: a, max: b)) <= b }
15
+ .check{ bind(sequence [Integer.random, Integer.random]){|xs| unit xs.sort }}
16
+
17
+ property("min == max"){|n| m.eval(Integer.random(min: n, max: n)) == n }
18
+ .check{ Integer.random }
19
+
20
+ property("min <= max"){|b,a| guard a >= b; error? { m.eval(Integer.random(min: a + 1, max: b)) }}
21
+ .check{ bind(sequence [Integer.random, Integer.random]){|xs| unit xs.sort }}
22
+
23
+ end
@@ -0,0 +1,43 @@
1
+ require "spec_helper"
2
+
3
+ describe Range, ".random" do
4
+
5
+ context "min" do
6
+ property("bounded"){|n| m.eval(Range.random(min: n)).first >= n }
7
+ .check{ Integer.random }
8
+ end
9
+
10
+ context "max" do
11
+ property("bounded"){|n| m.eval(Range.random(max: n)).last <= n }
12
+ .check{ Integer.random }
13
+ end
14
+
15
+ context "min and max" do
16
+ property("bounded"){|a,b| guard a <= b; m.eval(Range.random(min: a, max: b)).first >= a }
17
+ .check{ bind(sequence [Integer.random, Integer.random]){|xs| unit xs.sort }}
18
+
19
+ property("bounded"){|a,b| guard a <= b; m.eval(Range.random(min: a, max: b)).last <= b }
20
+ .check{ bind(sequence [Integer.random, Integer.random]){|xs| unit xs.sort }}
21
+ end
22
+
23
+ context "inclusive?" do
24
+ property("true"){|r| not r.exclude_end? }
25
+ .check{ Range.random(inclusive?: true, max: Integer::MAX) }
26
+
27
+ property("false"){|r| r.exclude_end? }
28
+ .check{ Range.random(inclusive?: false, min: -Integer::MAX) }
29
+ end
30
+
31
+ context "block" do
32
+ property "called once" do
33
+ count = 0
34
+ range = m.eval(Range.random(inclusive?: true) { m.unit(count += 1) })
35
+ range.should == (1..1)
36
+ end.check
37
+ end
38
+
39
+ specify "args required" do
40
+ expect { Range.random }.to raise_error
41
+ end
42
+
43
+ end