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