speculation 0.4.0 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +154 -53
- data/examples/codebreaker.rb +8 -8
- data/examples/game_of_life.rb +120 -0
- data/examples/sinatra-web-app/Gemfile +1 -1
- data/examples/sinatra-web-app/Gemfile.lock +3 -5
- data/examples/sinatra-web-app/app.rb +5 -5
- data/examples/spec_guide.rb +33 -28
- data/lib/speculation.rb +175 -145
- data/lib/speculation/gen.rb +11 -0
- data/lib/speculation/namespaced_symbols.rb +4 -2
- data/lib/speculation/predicates.rb +48 -0
- data/lib/speculation/spec.rb +4 -0
- data/lib/speculation/spec/and_spec.rb +6 -2
- data/lib/speculation/spec/every_spec.rb +41 -18
- data/lib/speculation/spec/f_spec.rb +8 -4
- data/lib/speculation/spec/hash_spec.rb +49 -28
- data/lib/speculation/spec/merge_spec.rb +6 -2
- data/lib/speculation/spec/nilable_spec.rb +8 -4
- data/lib/speculation/spec/nonconforming_spec.rb +5 -1
- data/lib/speculation/spec/or_spec.rb +10 -3
- data/lib/speculation/spec/predicate_spec.rb +15 -4
- data/lib/speculation/spec/regex_spec.rb +8 -4
- data/lib/speculation/spec/tuple_spec.rb +14 -6
- data/lib/speculation/test.rb +24 -24
- data/lib/speculation/utils.rb +4 -46
- data/lib/speculation/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8931f31e4324a18fab4cbc70ba363c072a0fcf78
|
4
|
+
data.tar.gz: 886f8a36b120d8dcebf90a139ec9a0d74817759d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1cfef5d20928e45e5852a6e25ea04e28fdce96aa36dc3f877e364ba63af52018983bfdfbee523b0221fd29a160a1a0c2dd45c33c3ebe372531fff1e9e6b2dfa
|
7
|
+
data.tar.gz: bb4eb7ded2d8ab2335d9993b2185492d0821e59c65d362b6152af6d06d551867b39dcf6eea23a98eb3e25cbb7dd9d2b3e81485db90b5ff0f46c75a939f25e2d5
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
A Ruby port of Clojure's `clojure.spec`. This library is largely a copy-and-paste from clojure.spec so all credit goes to Rich Hickey and contributors for their original work.
|
4
4
|
|
5
|
-
See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec) for a thorough outline of the problems clojure.spec (and thus Speculation) was built to address and how it addresses them. As a brief summary, Speculation allows you to
|
5
|
+
See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec) for a thorough outline of the problems clojure.spec (and thus Speculation) was built to address and how it addresses them. As a brief summary, Speculation allows you to describe the structure of datastructures and methods, enabling:
|
6
6
|
|
7
7
|
* declarative data validation and destructuring
|
8
8
|
* error reporting
|
@@ -11,85 +11,178 @@ See [clojure.spec - Rationale and Overview](https://clojure.org/about/spec) for
|
|
11
11
|
|
12
12
|
## Project Goals
|
13
13
|
|
14
|
-
The goal of this project is to match clojure.spec as closely as possible, from design to features to API. There aren't, and won't be any
|
14
|
+
The goal of this project is to match clojure.spec as closely as possible, from design to features to API. There aren't, and won't be, any significant departures from clojure.spec.
|
15
15
|
|
16
|
-
##
|
16
|
+
## Usage
|
17
17
|
|
18
|
-
|
19
|
-
* [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec), demonstrating most features.
|
20
|
-
* [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec)
|
21
|
-
* [json_parser.rb](examples/json_parser.rb): JSON parser using Speculation.
|
18
|
+
API Documentation is available at [RubyDoc](http://www.rubydoc.info/github/english/speculation) and [the wiki](https://github.com/english/speculation/wiki) covers features at a higher level. The API is more-or-less the same as `clojure.spec` so if you're already familiar clojure.spec with then you should feel at home with Speculation. Most guides, talks and discussions around clojure.spec should apply equally well to Speculation.
|
22
19
|
|
23
|
-
|
20
|
+
To demonstrate most of the features of Speculation we can explore an implementation of [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life):
|
21
|
+
|
22
|
+
### Game of Life demonstration
|
23
|
+
|
24
|
+
First, we'll require the [speculation][speculation] library along with the [generator][gen] and [test][test] modules. We'll be referring to `Speculation` a lot, so we'll add a shorthand to save us some typing:
|
24
25
|
|
25
|
-
|
26
|
+
```ruby
|
27
|
+
require 'speculation'
|
28
|
+
require 'speculation/test'
|
29
|
+
require 'speculation/gen'
|
26
30
|
|
27
|
-
|
31
|
+
S = Speculation
|
32
|
+
```
|
33
|
+
|
34
|
+
The Game of Life can be modelled with just a few simple entities. Let's describe them up front:
|
28
35
|
|
29
|
-
|
36
|
+
```ruby
|
37
|
+
# Our 'world' is a set of 'cells'.
|
38
|
+
S.def :"gol/world", S.coll_of(:"gol/cell", kind: Set)
|
30
39
|
|
31
|
-
|
40
|
+
# A cell is a tuple of coordinates.
|
41
|
+
S.def :"gol/cell", S.tuple(:"gol/coordinate", :"gol/coordinate")
|
32
42
|
|
33
|
-
|
34
|
-
S.
|
35
|
-
S.valid?(:even?.to_proc, 2)
|
36
|
-
S.valid?(String, "foo")
|
37
|
-
S.valid?(Enumerable, [1, 2, 3])
|
38
|
-
S.valid?(/^\d+$/, "123")
|
39
|
-
S.valid?(Set[:foo, :bar, :baz], :foo)
|
43
|
+
# A coordinate is just an integer.
|
44
|
+
S.def :"gol/coordinate", S.with_gen(Integer) { S.gen(S.int_in(-5..5)) }
|
40
45
|
```
|
41
46
|
|
42
|
-
|
47
|
+
Let's unpick what we've done so far:
|
43
48
|
|
44
|
-
|
49
|
+
- We've registered 'specs' (via [`S.def`][s-def]) to a global registry of specs, naming then with [namespaced Symbols][ns-symbols].
|
50
|
+
- We've created both complex ([`S.coll_of`][coll_of] and [`S.tuple`][tuple]) and simple (`Integer`) specs.
|
51
|
+
- We've also leveraged the built-in generators for `S.coll_of` and `S.tuple` but swapped out the `Integer` `:"gol/coordinate"` generator for another built-in: [`S.int_in`][int_in]. While we don't have an upper or lower bound on a valid coordinate, using a more restrictive generator allows us to experiment with smaller worlds.
|
45
52
|
|
46
|
-
|
53
|
+
Before we move on to implementing the logic of the Game of Life, let's get an idea of the kind of data we'll be working with.
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
S::Gen.generate(S.gen(:"gol/world"))
|
57
|
+
# => #<Set: {[2, -3], [-3, 5], [1, 1], [0, -3], [-5, 2], [-2, -3]}>
|
58
|
+
S::Gen.generate(S.gen(:"gol/world"))
|
59
|
+
# => #<Set: {}>
|
60
|
+
```
|
47
61
|
|
48
|
-
|
49
|
-
module MyModule
|
50
|
-
extend Speculation::NamespacedSymbols
|
62
|
+
Our up front definition of specs is already paying off. We can generate random, valid examples of our expected domain entities and play around with them, either in tests or in a REPL (e.g. Pry, IRB). Without this kind of exploration we may not have initially considered the case where the world is empty!
|
51
63
|
|
52
|
-
|
53
|
-
# => :"MyModule/foo"
|
64
|
+
Now to the logic of the game. Before we can implement [the rules](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Rules), we must be able to find the neighbouring cells for a given cell.
|
54
65
|
|
55
|
-
|
56
|
-
|
66
|
+
```ruby
|
67
|
+
def self.neighbours(cell)
|
68
|
+
cell_x, cell_y = cell
|
69
|
+
(-1..1).to_a.repeated_permutation(2).map { |(x, y)| [cell_x - x, cell_y - y] }
|
57
70
|
end
|
58
71
|
```
|
59
72
|
|
60
|
-
|
73
|
+
Now I think that should work... But I'm not so confident. Let's write a spec for the method. Its argument should be a cell, which we already have a spec for. We'll assume our world has no boundaries, so any cell should have a set of 8 neighbours.
|
61
74
|
|
62
|
-
|
75
|
+
```ruby
|
76
|
+
S.fdef method(:neighbours),
|
77
|
+
args: S.cat(cell: :"gol/cell"),
|
78
|
+
ret: S.coll_of(:"gol/cell", count: 8, kind: Set)
|
79
|
+
```
|
80
|
+
|
81
|
+
Now that we've described the inputs and outputs of the method, we can generatively test it:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
S::Test.summarize_results S::Test.check(method(:neighbours))
|
85
|
+
# {:spec=>"Speculation::FSpec(main.neighbours)",
|
86
|
+
# :method=>#<Method: main.neighbours>,
|
87
|
+
# :failure=>
|
88
|
+
# {:problems=>
|
89
|
+
# [{:path=>[:ret],
|
90
|
+
# :val=>[[-1, 6], [-1, 5], [-1, 4], [-2, 6], [-2, 5], [-2, 4], [-3, 6], [-3, 5], [-3, 4]],
|
91
|
+
# :via=>[],
|
92
|
+
# :in=>[],
|
93
|
+
# :pred=>[Set,[[[-1, 6], [-1, 5], [-1, 4], [-2, 6], [-2, 5], [-2, 4], [-3, 6], [-3, 5], [-3, 4]]]]}],
|
94
|
+
# :spec=>Speculation::EverySpec(),
|
95
|
+
# :value=>[[-1, 6], [-1, 5], [-1, 4], [-2, 6], [-2, 5], [-2, 4], [-3, 6], [-3, 5], [-3, 4]],
|
96
|
+
# :args=>[[-2, 5]],
|
97
|
+
# :val=> [[-1, 6], [-1, 5], [-1, 4], [-2, 6], [-2, 5], [-2, 4], [-3, 6], [-3, 5], [-3, 4]],
|
98
|
+
# :failure=>:check_failed}}
|
99
|
+
# => {:total=>1, :check_failed=>1}
|
100
|
+
```
|
63
101
|
|
64
|
-
|
102
|
+
Great, it's found a problem! This is saying that an input case was generated that failed our spec. The `:problems` values let's us know exactly what it found wrong. It's saying that the `:ret` part of our `neighbours` spec failed the 'Set' predicate. We can see that the value at `:val` is an Array, not a Set! There's our problem! Let's fix it by calling `to_set` before we return the collection of cells:
|
65
103
|
|
66
|
-
```
|
67
|
-
def self.
|
68
|
-
|
104
|
+
```ruby
|
105
|
+
def self.neighbours(cell)
|
106
|
+
cell_x, cell_y = cell
|
107
|
+
(-1..1).to_a.repeated_permutation(2).map { |(x, y)| [cell_x - x, cell_y - y] }.to_set
|
69
108
|
end
|
70
109
|
|
71
|
-
S.
|
110
|
+
S::Test.summarize_results S::Test.check(method(:neighbours))
|
111
|
+
# {:spec=>"Speculation::FSpec(main.neighbours)",
|
112
|
+
# :method=>#<Method: main.neighbours>,
|
113
|
+
# :failure=>
|
114
|
+
# {:problems=>
|
115
|
+
# [{:path=>[:ret],
|
116
|
+
# :pred=>
|
117
|
+
# [#<Method: Speculation::Predicates.count_eq?>,
|
118
|
+
# [8, #<Set: {[-4, -1], [-4, -2], [-4, -3], [-5, -1], [-5, -2], [-5, -3], [-6, -1], [-6, -2], [-6, -3]}>]],
|
119
|
+
# :val=>
|
120
|
+
# #<Set: {[-4, -1], [-4, -2], [-4, -3], [-5, -1], [-5, -2], [-5, -3], [-6, -1], [-6, -2], [-6, -3]}>,
|
121
|
+
# :via=>[],
|
122
|
+
# :in=>[]}],
|
123
|
+
# :spec=>Speculation::EverySpec(),
|
124
|
+
# :value=>
|
125
|
+
# #<Set: {[-4, -1], [-4, -2], [-4, -3], [-5, -1], [-5, -2], [-5, -3], [-6, -1], [-6, -2], [-6, -3]}>,
|
126
|
+
# :args=>[[-5, -2]],
|
127
|
+
# :val=>
|
128
|
+
# #<Set: {[-4, -1], [-4, -2], [-4, -3], [-5, -1], [-5, -2], [-5, -3], [-6, -1], [-6, -2], [-6, -3]}>,
|
129
|
+
# :failure=>:check_failed}}
|
130
|
+
# => {:total=>1, :check_failed=>1}
|
72
131
|
```
|
73
132
|
|
74
|
-
|
133
|
+
So we've fixed our Set problem, but now we have another. The `:ret` spec has failed once again, but this time the `:pred` is `Speculation::Predicates.count_eq?`, with an argument of 8 and then a set of 9 cells. Aha! We're including the given cell in this set of neighbours, therefore getting one too neighbouring cells back! That's an easy fix:
|
75
134
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
135
|
+
```ruby
|
136
|
+
def self.neighbours(cell)
|
137
|
+
cell_x, cell_y = cell
|
138
|
+
block = (-1..1).to_a.repeated_permutation(2).map { |(x, y)| [cell_x - x, cell_y - y] }.to_set
|
139
|
+
block - Set[cell]
|
81
140
|
end
|
82
141
|
|
83
|
-
S.
|
84
|
-
|
85
|
-
:ret => String)
|
142
|
+
S::Test.summarize_results S::Test.check(method(:neighbours))
|
143
|
+
# => {:total=>1, :check_passed=>1}
|
86
144
|
```
|
87
145
|
|
88
|
-
|
146
|
+
Great, we've managed to verify that, after generating many random inputs (1,000 by default), our method's return value satisfies the properties we defined in its spec. That gives me more confidence than a small handful of hand-written example tests would!
|
147
|
+
|
148
|
+
We can gain additional leverage from our spec: we can [`instrument`][instrument] the `neighbours` method so that it lets us know when it's been invoked with arguments that do not conform to its `:args` spec.
|
149
|
+
|
150
|
+
Before we do that, let's observe the method's current behavior when we provide deceptively invalid arguments:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
neighbours([1.0, 2.0])
|
154
|
+
# => #<Set: {[2.0, 3.0], [2.0, 2.0], [2.0, 1.0], [1.0, 3.0], [1.0, 1.0], [0.0, 3.0], [0.0, 2.0], [0.0, 1.0]}>
|
155
|
+
```
|
156
|
+
|
157
|
+
Here, we've provided a tuple of floats as a coordinate. This didn't raise any errors with our current implementation; it has dutifully gone to work and returned a value. However, the return value doesn't make sense: our program deals with integer pair coordinates. This situation would most likely lead to either invalid data or an exception at a later stage in our program, far away from the root cause of the problem (calling a method with incorrect types).
|
158
|
+
|
159
|
+
Let's address that by instrumenting our method so that it verifies its arguments at invocation time:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
S::Test.instrument method(:neighbours)
|
163
|
+
neighbours([1.0, 2.0])
|
164
|
+
# Speculation::Error: Call to 'main.neighbours' did not conform to spec:
|
165
|
+
# In: [0, 1] val: 2.0 fails spec: :"gol/coordinate" at: [:args, :cell, 1] predicate: [Integer, [2.0]]
|
166
|
+
# In: [0, 0] val: 1.0 fails spec: :"gol/coordinate" at: [:args, :cell, 0] predicate: [Integer, [1.0]]
|
167
|
+
# :spec Speculation::RegexSpec()
|
168
|
+
# :value [[1.0, 2.0]]
|
169
|
+
# :args [[1.0, 2.0]]
|
170
|
+
# :failure :instrument
|
171
|
+
# :caller "(pry):44:in `<main>'"
|
172
|
+
# from /Users/jamie/Projects/speculation/lib/speculation/test.rb:237:in `block in spec_checking_fn'
|
173
|
+
```
|
89
174
|
|
90
|
-
|
175
|
+
We can see from the error message that our argument has two problems: both elements of our array argument have failed the Integer predicate for the `:"gol/coordinate"` spec. This is arguably much better feedback than if we hadn't instrumented this method.
|
91
176
|
|
92
|
-
|
177
|
+
We've demonstrated several Speculation features, so we'll leave this demo here. See the full [Game of Life example](examples/game_of_life.rb) where we take this idea further.
|
178
|
+
|
179
|
+
## Examples
|
180
|
+
|
181
|
+
* [sinatra-web-app](examples/sinatra-web-app): A small Sinatra web application demonstrating parameter validation and API error message generation.
|
182
|
+
* [spec_guide.rb](examples/spec_guide.rb): Speculation port of Clojure's [spec guide](https://clojure.org/guides/spec), demonstrating most features.
|
183
|
+
* [codebreaker.rb](examples/codebreaker.rb): Speculation port of the 'codebreaker' game described in [Interactive development with clojure.spec](http://blog.cognitect.com/blog/2016/10/5/interactive-development-with-clojurespec).
|
184
|
+
* [game_of_life.rb](examples/game_of_life.rb): [Conway's Game of Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) implementation.
|
185
|
+
* [json_parser.rb](examples/json_parser.rb): (toy) JSON parser using Speculation.
|
93
186
|
|
94
187
|
## Project status
|
95
188
|
|
@@ -97,10 +190,8 @@ Speculation will mirror any changes made to clojure.spec. clojure.spec is still
|
|
97
190
|
|
98
191
|
While most of features of clojure.spec are implemented in Speculation, a few remain:
|
99
192
|
|
100
|
-
- [`
|
101
|
-
- [`
|
102
|
-
- [`abbrev`](https://clojuredocs.org/clojure.spec/abbrev)
|
103
|
-
- [`describe`](https://clojuredocs.org/clojure.spec/describe)
|
193
|
+
- [`multi-spec`](https://clojure.github.io/clojure/branch-master/clojure.spec-api.html#clojure.spec/multi-spec) - Ruby doesn't have an equivalent of multimethods...
|
194
|
+
- [`describe`](https://clojuredocs.org/clojure.spec/describe) - Since we can't capture the source code of a Ruby method/proc, we won't be able to match Clojure's `s/describe` but we may be able to come up with something close.
|
104
195
|
|
105
196
|
## Improvements
|
106
197
|
|
@@ -114,7 +205,7 @@ Some things I hope to focus on in the near future:
|
|
114
205
|
|
115
206
|
## Development
|
116
207
|
|
117
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run
|
208
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake` to run Rubocop and the test suite. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
118
209
|
|
119
210
|
## Contributing
|
120
211
|
|
@@ -123,3 +214,13 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/englis
|
|
123
214
|
## License
|
124
215
|
|
125
216
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
217
|
+
|
218
|
+
[def]: http://www.rubydoc.info/github/english/speculation/master/Speculation#def-class_method
|
219
|
+
[coll_of]: http://www.rubydoc.info/github/english/speculation/master/Speculation#coll_of-class_method
|
220
|
+
[tuple]: http://www.rubydoc.info/github/english/speculation/master/Speculation#tuple-class_method
|
221
|
+
[int_in]: http://www.rubydoc.info/github/english/speculation/master/Speculation#int_in-class_method
|
222
|
+
[ns-symbols]: https://github.com/english/speculation/wiki/Namespaced-Symbols
|
223
|
+
[speculation]: http://www.rubydoc.info/github/english/speculation/master/Speculation
|
224
|
+
[gen]: http://www.rubydoc.info/github/english/speculation/master/Speculation/Gen
|
225
|
+
[test]: http://www.rubydoc.info/github/english/speculation/master/Speculation/Test
|
226
|
+
[instrument]: http://www.rubydoc.info/github/english/speculation/master/Speculation/Test#instrument-class_method
|
data/examples/codebreaker.rb
CHANGED
@@ -72,7 +72,7 @@ def self.score(secret, guess)
|
|
72
72
|
end
|
73
73
|
|
74
74
|
STest.check method(:score)
|
75
|
-
# [{:spec=>Speculation::FSpec(main.score), :
|
75
|
+
# [{:spec=>Speculation::FSpec(main.score), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
|
76
76
|
|
77
77
|
def self.score(secret, guess)
|
78
78
|
{ ns(:exact_matches) => 4,
|
@@ -82,12 +82,12 @@ end
|
|
82
82
|
# STest.check method(:score)
|
83
83
|
|
84
84
|
# [{:spec=>Speculation::FSpec(main.score),
|
85
|
-
# :
|
85
|
+
# :ret=>
|
86
86
|
# {:fail=>[[:y, :b, :r, :r, :b], [:y, :w, :g, :r, :g]],
|
87
87
|
# :block=>nil,
|
88
88
|
# :num_tests=>1,
|
89
89
|
# :result=>
|
90
|
-
# #<Speculation::Error: {:
|
90
|
+
# #<Speculation::Error: {:problems=>
|
91
91
|
# [{:path=>[:fn],
|
92
92
|
# :val=>
|
93
93
|
# {:args=>{:secret=>[:y, :b, :r, :r, :b], :guess=>[:y, :w, :g, :r, :g]},
|
@@ -109,7 +109,7 @@ S.exercise_fn method(:score)
|
|
109
109
|
|
110
110
|
STest.check method(:score)
|
111
111
|
|
112
|
-
# [{:spec=>Speculation::FSpec(main.score), :
|
112
|
+
# [{:spec=>Speculation::FSpec(main.score), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
|
113
113
|
|
114
114
|
def self.score(secret, guess)
|
115
115
|
{ ns(:exact_matches) => exact_matches(secret, guess),
|
@@ -144,7 +144,7 @@ S.exercise_fn method(:exact_matches)
|
|
144
144
|
# [[[:r, :b, :r, :c], [:y, :r, :g, :b]], nil, 0],
|
145
145
|
|
146
146
|
STest.check method(:exact_matches)
|
147
|
-
# [{:spec=>Speculation::FSpec(main.exact_matches), :
|
147
|
+
# [{:spec=>Speculation::FSpec(main.exact_matches), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}]
|
148
148
|
|
149
149
|
STest.instrument method(:exact_matches)
|
150
150
|
S.exercise_fn method(:score)
|
@@ -163,7 +163,7 @@ end
|
|
163
163
|
# S.exercise_fn method(:score)
|
164
164
|
|
165
165
|
# Speculation::Error: Call to 'main.exact_matches' did not conform to spec:
|
166
|
-
# In: [1] val: [:w, :y, :c] fails spec: :"Object/code" at: [:args, :guess] predicate: [#<Method: Speculation::
|
166
|
+
# In: [1] val: [:w, :y, :c] fails spec: :"Object/code" at: [:args, :guess] predicate: [#<Method: Speculation::Predicates.count_between?>, [4, 6, [:w, :y, :c]]]
|
167
167
|
# Speculation/args [[:r, :b, :c, :y, :b, :r], [:w, :y, :c]]
|
168
168
|
# Speculation/failure :instrument
|
169
169
|
# Speculation::Test/caller "(pry):69:in `score'"
|
@@ -189,7 +189,7 @@ S.exercise_fn method(:exact_matches), 10, S.get_spec(method(:match_count))
|
|
189
189
|
|
190
190
|
STest.check_method method(:exact_matches), S.get_spec(method(:match_count))
|
191
191
|
|
192
|
-
# {:spec=>Speculation::FSpec(main.match_count), :
|
192
|
+
# {:spec=>Speculation::FSpec(main.match_count), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.exact_matches>}
|
193
193
|
|
194
194
|
STest.instrument method(:exact_matches), :spec => { method(:exact_matches) => S.get_spec(method(:match_count)) }
|
195
195
|
|
@@ -202,7 +202,7 @@ S.exercise_fn method(:score)
|
|
202
202
|
|
203
203
|
STest.check method(:score)
|
204
204
|
|
205
|
-
# [{:spec=>Speculation::FSpec(main.score), :
|
205
|
+
# [{:spec=>Speculation::FSpec(main.score), :ret=>{:num_tests=>1000, :result=>true}, :method=>#<Method: main.score>}]
|
206
206
|
|
207
207
|
def self.all_matches(secret, guess)
|
208
208
|
frequencies = ->(xs) { xs.group_by(&:itself).transform_values(&:count) }
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "speculation"
|
3
|
+
require "speculation/gen"
|
4
|
+
require "speculation/test"
|
5
|
+
|
6
|
+
S = Speculation
|
7
|
+
STest = S::Test
|
8
|
+
Gen = S::Gen
|
9
|
+
|
10
|
+
S.def :"gol/coordinate", S.with_gen(Integer) { S.gen(S.int_in(-5..5)) }
|
11
|
+
S.def :"gol/cell", S.tuple(:"gol/coordinate", :"gol/coordinate")
|
12
|
+
S.def :"gol/world", S.coll_of(:"gol/cell", :kind => Set)
|
13
|
+
|
14
|
+
def self.neighbours(cell)
|
15
|
+
cell_x, cell_y = cell
|
16
|
+
(-1..1).repeated_permutation(2).map { |(x, y)| [cell_x - x, cell_y - y] }.to_set
|
17
|
+
block - Set[cell]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.cell_distance(cell_a, cell_b)
|
21
|
+
[
|
22
|
+
(cell_a[0] - cell_b[0]).abs,
|
23
|
+
(cell_a[1] - cell_b[1]).abs
|
24
|
+
]
|
25
|
+
end
|
26
|
+
|
27
|
+
S.fdef method(:neighbours),
|
28
|
+
:args => S.cat(:cell => :"gol/cell"),
|
29
|
+
:ret => S.coll_of(:"gol/cell", :count => 8, :kind => Set),
|
30
|
+
:fn => ->(fn) {
|
31
|
+
cell = fn[:args][:cell]
|
32
|
+
neighbours = fn[:ret]
|
33
|
+
neighbours.all? { |neighbour| [[1, 0], [0, 1], [1, 1]].include?(cell_distance(cell, neighbour)) }
|
34
|
+
}
|
35
|
+
|
36
|
+
S.exercise_fn(method(:neighbours))
|
37
|
+
STest.instrument(method(:neighbours))
|
38
|
+
# STest.summarize_results STest.check(method(:neighbours))
|
39
|
+
|
40
|
+
def self.alive_neighbours(world, cell)
|
41
|
+
neighbours(cell).intersection(world)
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.alive?(world, cell)
|
45
|
+
world.include?(cell)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.tick(world)
|
49
|
+
world.
|
50
|
+
flat_map { |cell| neighbours(cell).to_a }.
|
51
|
+
select { |cell|
|
52
|
+
alive_neighbour_count = alive_neighbours(world, cell).count
|
53
|
+
|
54
|
+
if alive?(world, cell)
|
55
|
+
(2..3).cover?(alive_neighbour_count)
|
56
|
+
else
|
57
|
+
alive_neighbour_count == 3
|
58
|
+
end
|
59
|
+
}.
|
60
|
+
to_set
|
61
|
+
end
|
62
|
+
|
63
|
+
S.fdef method(:tick),
|
64
|
+
:args => S.cat(:world => :"gol/world"),
|
65
|
+
:ret => :"gol/world"
|
66
|
+
|
67
|
+
S.exercise_fn(method(:tick))
|
68
|
+
STest.instrument(method(:tick))
|
69
|
+
# STest.check(method(:tick))
|
70
|
+
|
71
|
+
def self.simulation(world)
|
72
|
+
Enumerator.new do |yielder|
|
73
|
+
loop do
|
74
|
+
yielder << world
|
75
|
+
world = tick(world)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.serialize_world(world)
|
81
|
+
return [[]] if world.empty?
|
82
|
+
|
83
|
+
min_y, max_y = world.minmax_by(&:last).map(&:last)
|
84
|
+
min_x, max_x = world.minmax_by(&:first).map(&:first)
|
85
|
+
|
86
|
+
(min_y.pred..max_y.next).map { |y| (min_x.pred..max_x.next).map { |x| world.include?([x, y]) } }
|
87
|
+
end
|
88
|
+
|
89
|
+
S.fdef method(:serialize_world),
|
90
|
+
:args => S.cat(:world => :"gol/world"),
|
91
|
+
:ret => S.coll_of(S.coll_of(:"Speculation/boolean")),
|
92
|
+
:fn => ->(fn) { fn[:ret].flatten.count(&:itself) == fn[:args][:world].count }
|
93
|
+
|
94
|
+
def self.print_world(world, out)
|
95
|
+
world.each do |line|
|
96
|
+
line.each do |cell|
|
97
|
+
if cell
|
98
|
+
out << "\u2588"
|
99
|
+
else
|
100
|
+
out << "\u2591"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
out << "\n"
|
105
|
+
end
|
106
|
+
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
init = Gen.generate(S.gen(:"gol/world"))
|
111
|
+
worlds = simulation(init).lazy.take_while { |world| !world.empty? }
|
112
|
+
|
113
|
+
worlds.first(10).each do |world|
|
114
|
+
print "\033[2J"
|
115
|
+
puts world.sort.to_s
|
116
|
+
print_world(serialize_world(world), STDOUT)
|
117
|
+
sleep 0.5
|
118
|
+
end
|
119
|
+
|
120
|
+
# STest.summarize_results STest.check
|