properb 0.0.1
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.
- checksums.yaml +7 -0
- data/COPYING +674 -0
- data/README.md +56 -0
- data/lib/properb/generator.rb +29 -0
- data/lib/properb/generators/array_generator.rb +75 -0
- data/lib/properb/generators/choice_generator.rb +26 -0
- data/lib/properb/generators/constant_generator.rb +13 -0
- data/lib/properb/generators/deferred_generator.rb +15 -0
- data/lib/properb/generators/frequencies_generator.rb +26 -0
- data/lib/properb/generators/integer_generator.rb +30 -0
- data/lib/properb/generators/mapped_generator.rb +14 -0
- data/lib/properb/generators/recursive_generator.rb +15 -0
- data/lib/properb/generators/selected_generator.rb +19 -0
- data/lib/properb/generators/sized_generator.rb +14 -0
- data/lib/properb/generators/tuple_generator.rb +41 -0
- data/lib/properb/generators.rb +83 -0
- data/lib/properb/rspec/example_group_methods.rb +64 -0
- data/lib/properb/rspec/example_methods.rb +40 -0
- data/lib/properb/rspec.rb +2 -0
- data/lib/properb/shrink_tree.rb +44 -0
- data/lib/properb.rb +111 -0
- metadata +83 -0
data/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Properb
|
|
2
|
+
|
|
3
|
+
Property-based testing in Ruby, with RSpec integration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
This project
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
To install, add the following to your `spec/spec_helper.rb` file:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
RSpec.configure do |config|
|
|
15
|
+
Properb.rspec_install(config)
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then you can use `generate` and `it_always` in your tests to create property-based tests:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
describe "#sort" do
|
|
23
|
+
generate(strings: array(string))
|
|
24
|
+
|
|
25
|
+
it_always "preserves the number of elements", num_tests: 200 do
|
|
26
|
+
expect(strings.sort.length).to be == strings.length
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it_always "has the first and last elements in sorted order" do
|
|
30
|
+
assume(strings).to_not be_empty
|
|
31
|
+
sorted = strings.sort
|
|
32
|
+
expect(sorted[0]).to be <= sorted[-1]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it_always "creates pairwise sorted elements", num_tests: 200 do
|
|
36
|
+
# assume(strings.length).to be >= 2
|
|
37
|
+
assume(strings.uniq).to eq(strings)
|
|
38
|
+
sorted = strings.sort
|
|
39
|
+
(0...sorted.length - 1).each do |i|
|
|
40
|
+
expect(sorted[i]).to be < sorted[i + 1]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Development
|
|
47
|
+
|
|
48
|
+
This project uses Guix as its main dependency management system. A development environment can be created by running:
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
guix shell --development --file=guix.scm
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Contributing
|
|
55
|
+
|
|
56
|
+
Bug reports and changes are welcome on SourceHut at <https://sr.ht/~czan/properb/>.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
class Generator
|
|
3
|
+
def to_properb_generator
|
|
4
|
+
self
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def map(&block)
|
|
8
|
+
Generators::MappedGenerator.new(self, &block)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def select(num_attempts: 10, &block)
|
|
12
|
+
Generators::SelectedGenerator.new(self, num_attempts: num_attempts, &block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def reject(num_attempts: 10, &block)
|
|
16
|
+
select(num_attempts: num_attempts) do |*args|
|
|
17
|
+
!block.call(*args)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def or(generator)
|
|
22
|
+
Generators::ChoiceGenerator.new([self, Generators.gen(generator)])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sized(size)
|
|
26
|
+
Generators::SizedGenerator.new(self, size)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class ArrayGenerator < Generator
|
|
4
|
+
def initialize(generator, length: 0...)
|
|
5
|
+
@generator = generator
|
|
6
|
+
@length = length
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def generate_value(random, size)
|
|
10
|
+
if @length.is_a?(Range)
|
|
11
|
+
length = random.random_number(@length.min..(@length.end || (@length.min + size)))
|
|
12
|
+
min_length = @length.min
|
|
13
|
+
else
|
|
14
|
+
length = min_length = @length
|
|
15
|
+
end
|
|
16
|
+
values = (0...length).map { @generator.generate_value(random, size) }
|
|
17
|
+
ShrinkTree.new(
|
|
18
|
+
values.map(&:value),
|
|
19
|
+
shrink(values, min_length)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def shrink(values, min_length)
|
|
24
|
+
Enumerator.new do |out|
|
|
25
|
+
cutoff = (values.length / 2).ceil
|
|
26
|
+
if cutoff > min_length
|
|
27
|
+
out << ShrinkTree.new(
|
|
28
|
+
values[cutoff..].map(&:value),
|
|
29
|
+
shrink(values[cutoff..], min_length)
|
|
30
|
+
)
|
|
31
|
+
out << ShrinkTree.new(
|
|
32
|
+
values[...-cutoff].map(&:value),
|
|
33
|
+
shrink(values[...-cutoff], min_length)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
if values.length > min_length
|
|
37
|
+
# Try removing individual items in the vector
|
|
38
|
+
(0...values.length).each do |i|
|
|
39
|
+
new_values = values[0...i] + values[i + 1...]
|
|
40
|
+
out << ShrinkTree.new(
|
|
41
|
+
new_values.map(&:value),
|
|
42
|
+
shrink(new_values, min_length)
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
# Try shrinking the individual items in the vector
|
|
47
|
+
(0...values.length).each do |i|
|
|
48
|
+
values[i].children.each do |value|
|
|
49
|
+
new_values = values[0...i] + [value] + values[i + 1...]
|
|
50
|
+
out << ShrinkTree.new(
|
|
51
|
+
new_values.map(&:value),
|
|
52
|
+
shrink(new_values, min_length)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
# Try shrinking pairs of items in the vector
|
|
57
|
+
(0...values.length).each do |i| # rubocop:disable Style/CombinableLoops
|
|
58
|
+
values[i].children.each do |first_value|
|
|
59
|
+
new_values = values[0...i] + [first_value] + values[i + 1...]
|
|
60
|
+
(0...values.length).each do |j|
|
|
61
|
+
new_values[j].children.each do |second_value|
|
|
62
|
+
newer_values = new_values[0...j] + [second_value] + new_values[j + 1...]
|
|
63
|
+
out << ShrinkTree.new(
|
|
64
|
+
newer_values.map(&:value),
|
|
65
|
+
shrink(newer_values, min_length)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class ChoiceGenerator < Generator
|
|
4
|
+
def initialize(generators)
|
|
5
|
+
@generators = generators
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
# Overriding the superclass method to make the distribution even
|
|
9
|
+
def or(generator)
|
|
10
|
+
ChoiceGenerator.new([*@generators, generator])
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def generate_value(random, size)
|
|
14
|
+
integer = IntegerGenerator.new(0...@generators.length)
|
|
15
|
+
.generate_value(random, size)
|
|
16
|
+
saved_random = random.dup
|
|
17
|
+
integer.flat_map do |choice|
|
|
18
|
+
# We want all the sub-generators use the same random number
|
|
19
|
+
# sequence, so we duplicate the generator
|
|
20
|
+
new_random = saved_random.dup
|
|
21
|
+
@generators[choice].generate_value(new_random, size)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class DeferredGenerator < Generator
|
|
4
|
+
def initialize(&block)
|
|
5
|
+
@block = block
|
|
6
|
+
@generator = nil
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def generate_value(random, size)
|
|
10
|
+
@generator = Generators.gen(@block.call)
|
|
11
|
+
@generator.generate_value(random, size)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class FrequenciesGenerator < Generator
|
|
4
|
+
def initialize(frequencies)
|
|
5
|
+
@frequencies = frequencies
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def generate_value(random, size)
|
|
9
|
+
integer = IntegerGenerator.new(0...@frequencies.values.sum)
|
|
10
|
+
.generate_value(random, size)
|
|
11
|
+
saved_random = random.dup
|
|
12
|
+
integer.flat_map do |choice|
|
|
13
|
+
new_random = saved_random.dup
|
|
14
|
+
running = 0
|
|
15
|
+
generator = @frequencies.find do |_generator, weight|
|
|
16
|
+
running += weight
|
|
17
|
+
running > choice
|
|
18
|
+
end.first
|
|
19
|
+
raise "No generator found in `frequencies' - insufficient weights?" unless generator
|
|
20
|
+
|
|
21
|
+
generator.generate_value(new_random, size)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class IntegerGenerator < Generator
|
|
4
|
+
def initialize(range = 0..)
|
|
5
|
+
@range = range
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def generate_value(random, _size)
|
|
9
|
+
value = random.random_number(@range)
|
|
10
|
+
ShrinkTree.new(
|
|
11
|
+
value,
|
|
12
|
+
Enumerator.new do |out|
|
|
13
|
+
out << ShrinkTree.new(@range.min, [])
|
|
14
|
+
shrink(value, @range.min).each { out << _1 }
|
|
15
|
+
end
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def shrink(value, min)
|
|
20
|
+
midpoint = ((value + min) / 2).floor
|
|
21
|
+
return [] if midpoint == min # termination
|
|
22
|
+
|
|
23
|
+
Enumerator.new do |out|
|
|
24
|
+
out << ShrinkTree.new(midpoint, shrink(midpoint, min))
|
|
25
|
+
shrink(value, midpoint).each { out << _1 }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class MappedGenerator < Generator
|
|
4
|
+
def initialize(generator, &block)
|
|
5
|
+
@generator = generator
|
|
6
|
+
@block = block
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def generate_value(random, size)
|
|
10
|
+
@generator.generate_value(random, size).map(&@block)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class RecursiveGenerator < Generator
|
|
4
|
+
def initialize(&block)
|
|
5
|
+
@block = block
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def generate_value(random, size)
|
|
9
|
+
@block
|
|
10
|
+
.call(size, RecursiveGenerator.new(&@block).sized((size / 2).floor))
|
|
11
|
+
.generate_value(random, size)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class SelectedGenerator < Generator
|
|
4
|
+
def initialize(generator, num_attempts: 10, &block)
|
|
5
|
+
@generator = generator
|
|
6
|
+
@num_attempts = num_attempts
|
|
7
|
+
@block = block
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def generate_value(random, size)
|
|
11
|
+
@num_attempts.times do
|
|
12
|
+
generated = @generator.generate_value(random, size)
|
|
13
|
+
return generated.select(&@block).first if @block.call(generated.value)
|
|
14
|
+
end
|
|
15
|
+
raise "Could not generate a value after #{@num_attempts} attempts"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class SizedGenerator < Generator
|
|
4
|
+
def initialize(generator, size)
|
|
5
|
+
@generator = generator
|
|
6
|
+
@size = size
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def generate_value(random, _size)
|
|
10
|
+
@generator.generate_value(random, @size)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module Generators
|
|
3
|
+
class TupleGenerator < Generator
|
|
4
|
+
def initialize(generators)
|
|
5
|
+
@generators = generators
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def generate_value(random, size)
|
|
9
|
+
values = @generators.map { _1.generate_value(random, size) }
|
|
10
|
+
ShrinkTree.new(values.map(&:value), shrink(values))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def shrink(values)
|
|
14
|
+
Enumerator.new do |out|
|
|
15
|
+
# Try shrinking the individual items in the vector
|
|
16
|
+
(0...values.length).each do |i|
|
|
17
|
+
values[i].children.each do |value|
|
|
18
|
+
new_values = values[0...i] + [value] + values[i + 1...]
|
|
19
|
+
out << ShrinkTree.new(new_values.map(&:value), shrink(new_values))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
# Try shrinking pairs of items in the vector
|
|
23
|
+
(0...values.length).each do |i|
|
|
24
|
+
values[i].children.each do |value|
|
|
25
|
+
new_values = values[0...i] + [value] + values[i + 1...]
|
|
26
|
+
(0...values.length).each do |j|
|
|
27
|
+
new_values[j].children.each do |value|
|
|
28
|
+
newer_values = new_values[0...j] + [value] + new_values[j + 1...]
|
|
29
|
+
out << ShrinkTree.new(
|
|
30
|
+
newer_values.map(&:value),
|
|
31
|
+
shrink(newer_values)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require_relative "generators/array_generator"
|
|
2
|
+
require_relative "generators/choice_generator"
|
|
3
|
+
require_relative "generators/constant_generator"
|
|
4
|
+
require_relative "generators/deferred_generator"
|
|
5
|
+
require_relative "generators/frequencies_generator"
|
|
6
|
+
require_relative "generators/integer_generator"
|
|
7
|
+
require_relative "generators/mapped_generator"
|
|
8
|
+
require_relative "generators/recursive_generator"
|
|
9
|
+
require_relative "generators/selected_generator"
|
|
10
|
+
require_relative "generators/sized_generator"
|
|
11
|
+
require_relative "generators/tuple_generator"
|
|
12
|
+
|
|
13
|
+
module Properb
|
|
14
|
+
module Generators
|
|
15
|
+
extend Generators # Bring all these methods onto the module itself
|
|
16
|
+
|
|
17
|
+
def defer(&block)
|
|
18
|
+
DeferredGenerator.new(&block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def gen(object)
|
|
22
|
+
if object.respond_to?(:to_properb_generator)
|
|
23
|
+
object.to_properb_generator
|
|
24
|
+
else
|
|
25
|
+
const(object)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def one_of(*options)
|
|
30
|
+
ChoiceGenerator.new(options.map { gen(_1) })
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def int(range = 0..(2 << 32))
|
|
34
|
+
IntegerGenerator.new(range)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# By default, generate printable ASCII characters
|
|
38
|
+
def string(char_generator = int(32..126).map { _1.chr }, length: 0..)
|
|
39
|
+
array(char_generator, length: length).map { _1.join }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def const(value)
|
|
43
|
+
ConstantGenerator.new(value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def boolean
|
|
47
|
+
const(false).or(const(true))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def maybe(generator)
|
|
51
|
+
ChoiceGenerator.new([const(nil), gen(generator)])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def frequencies(**generators_and_weights)
|
|
55
|
+
FrequenciesGenerator.new(generators_and_weights.transform_values { gen(_1) })
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def array(generator, length: 0..)
|
|
59
|
+
ArrayGenerator.new(gen(generator), length: length)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def tuple(*generators)
|
|
63
|
+
TupleGenerator.new(generators.map { gen(_1) })
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def hashmap(key_generator, value_generator = nil)
|
|
67
|
+
if value_generator.nil?
|
|
68
|
+
unless key_generator.is_a?(Hash)
|
|
69
|
+
raise "Single argument to `hashmap' must be a hash with generator values, not #{key_generator || "nil"}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
tuple(*key_generator.values.map { gen(_1) })
|
|
73
|
+
.map { key_generator.keys.zip(_1).to_h }
|
|
74
|
+
else
|
|
75
|
+
array(tuple(key_generator, value_generator)).map(&:to_h)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def recursive(&block)
|
|
80
|
+
RecursiveGenerator.new(&block)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module RSpec
|
|
3
|
+
module ExampleGroupMethods
|
|
4
|
+
class PropertyExample < ::RSpec::Core::Example
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def with_around_and_singleton_context_hooks
|
|
8
|
+
Properb.for_all(metadata[:generators], **metadata.slice(:size, :num_tests, :assumption_limit)) do |data|
|
|
9
|
+
@example_group_instance.instance_variable_set("@properb_generated_data", data)
|
|
10
|
+
super
|
|
11
|
+
if display_exception
|
|
12
|
+
ex = display_exception
|
|
13
|
+
self.display_exception = nil
|
|
14
|
+
raise ex
|
|
15
|
+
end
|
|
16
|
+
ensure
|
|
17
|
+
@example_group_instance.instance_variable_set("@properb_generated_data", nil)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def generate(**methods_and_generators)
|
|
23
|
+
@properb_generators ||= {}
|
|
24
|
+
@properb_generators.merge!(methods_and_generators)
|
|
25
|
+
@properb_generators.keys.each do |key|
|
|
26
|
+
define_method(key) do
|
|
27
|
+
raise "Cannot call #{key} outside of a property" unless @properb_generated_data
|
|
28
|
+
|
|
29
|
+
@properb_generated_data[key]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def it_always(description, **options, &block)
|
|
35
|
+
PropertyExample.new(self,
|
|
36
|
+
description,
|
|
37
|
+
{ property: true,
|
|
38
|
+
generators: @properb_generators,
|
|
39
|
+
**options },
|
|
40
|
+
block)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def fit_always(description, **options, &block)
|
|
44
|
+
PropertyExample.new(self,
|
|
45
|
+
description,
|
|
46
|
+
{ property: true,
|
|
47
|
+
generators: @properb_generators,
|
|
48
|
+
focus: true,
|
|
49
|
+
**options },
|
|
50
|
+
block)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def xit_always(description, **options, &block)
|
|
54
|
+
PropertyExample.new(self,
|
|
55
|
+
description,
|
|
56
|
+
{ property: true,
|
|
57
|
+
generators: @properb_generators,
|
|
58
|
+
skip: "Temporarily skipped with xit_always",
|
|
59
|
+
**options },
|
|
60
|
+
block)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
module RSpec
|
|
3
|
+
module ExampleMethods
|
|
4
|
+
class AssumptionViolated < StandardError; end
|
|
5
|
+
|
|
6
|
+
class Assumption
|
|
7
|
+
def initialize(target)
|
|
8
|
+
@target = target
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to(matcher = nil, message = nil, &block)
|
|
12
|
+
handle_positive_matcher(@target, matcher, message, &block)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_not(matcher = nil, message = nil, &block)
|
|
16
|
+
handle_negative_matcher(@target, matcher, message, &block)
|
|
17
|
+
end
|
|
18
|
+
alias not_to to_not
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def handle_positive_matcher(actual, matcher, custom_message, &block)
|
|
23
|
+
return if matcher.matches?(actual, &block)
|
|
24
|
+
|
|
25
|
+
raise AssumptionViolated, custom_message || matcher.failure_message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def handle_negative_matcher(actual, matcher, custom_message, &block)
|
|
29
|
+
return unless matcher.matches?(actual, &block)
|
|
30
|
+
|
|
31
|
+
raise AssumptionViolated, custom_message || matcher.failure_message
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def assume(value)
|
|
36
|
+
Assumption.new(value)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Properb
|
|
2
|
+
class ShrinkTree
|
|
3
|
+
attr_reader :value, :children
|
|
4
|
+
|
|
5
|
+
def initialize(value, children)
|
|
6
|
+
@value = value
|
|
7
|
+
@children = children.lazy
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def map(&block)
|
|
11
|
+
ShrinkTree.new(
|
|
12
|
+
block.call(@value),
|
|
13
|
+
@children.map { _1.map(&block) }
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def flat_map(&block)
|
|
18
|
+
result = block.call(@value)
|
|
19
|
+
# First try to shrink the parent generator, then try to shrink
|
|
20
|
+
# the child.
|
|
21
|
+
combined_children = (children.map { _1.flat_map(&block) } + result.children)
|
|
22
|
+
ShrinkTree.new(result.value, combined_children)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return an array of nodes to use to replace the current node,
|
|
26
|
+
# filtered by calling the provided block on the node's values.
|
|
27
|
+
def select(&block)
|
|
28
|
+
if block.call(@value)
|
|
29
|
+
[
|
|
30
|
+
ShrinkTree.new(
|
|
31
|
+
@value,
|
|
32
|
+
@children.flat_map { _1.select(&block) }
|
|
33
|
+
)
|
|
34
|
+
]
|
|
35
|
+
else
|
|
36
|
+
@children.flat_map { _1.select(&block) }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def debug
|
|
41
|
+
{ @value => @children.map(&:debug).to_a }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|