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
data/lib/propr/runner.rb
ADDED
@@ -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
|
File without changes
|
@@ -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,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,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
|