pbt 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0a6f9001f79b18e4cb3699704355bb25cfae69720c9179f63e468617909ef813
4
- data.tar.gz: d384534a54f566e83680b54bfbf42aebc0137ae1ec5dccc937c27f672bfd9abb
3
+ metadata.gz: 02cfb8777d4c14fdb39421dc64c3e4d3e23ec57f1e933cccaac0ef67b8e103b0
4
+ data.tar.gz: 6c30345dec4c52854f52c5a28c2894fb0d41b58d36ada5337f983323440ca9c5
5
5
  SHA512:
6
- metadata.gz: 87827a542745ba10b1f086bf40221280499ed9844f9503f11b43a499badb0ab020ccd214e0b5d0bcb13e0efbe95a3d29fee2a758fd5a47d9d58b356400d48dd3
7
- data.tar.gz: 3947e569a6f48802ca1490b4e2c6763f38e40d08528e0aed8185ec45b418efce41f7596b641da93efea2f7c553a16e3b132a54fe9b4d73d886456b5921d0869e
6
+ metadata.gz: a152aa19cd0f919b86216fe54457b50d7953aa96c93a302d0044ec7535177efbfa14104a20bd13e8fd2ad9aa9b7fc5cb10d6c1fbcf4e9ec9a0117c24b8bc2140
7
+ data.tar.gz: 9ba28731ba906fab360a1df6030d2f4c4d34a591ec70339f8d81fb36d2db427d6ca046849a005fff7ecfcfc4fa684869455f597e38acecd0dbdef2b5a3613904
data/.standard.yml CHANGED
@@ -1,3 +1,3 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/standardrb/standard
3
- ruby_version: 3.3
3
+ ruby_version: 3.1
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.1.0] - 2024-01-27
3
+ ## [0.1.0] - 2024-04-13
4
4
 
5
- - Initial release
5
+ - Implement basic primitive arbitraries
6
+ - Implement composite arbitraries
7
+ - Support shrinking
8
+ - Support multiple concurrency methods
9
+ - Ractor
10
+ - Process
11
+ - Thread
12
+ - None (Run tests sequentially)
13
+ - Documentation
14
+ - Add better examples
15
+ - Arbitrary usage
16
+ - Configuration
17
+
18
+ ## [0.0.1] - 2024-01-27
19
+
20
+ - Initial release (Proof of concept)
data/README.md CHANGED
@@ -1,73 +1,318 @@
1
1
  # Property-Based Testing in Ruby
2
2
 
3
- A property-based testing tool for Ruby, utilizing Ractor for parallelizing test cases.
3
+ [![Gem Version](https://badge.fury.io/rb/pbt.svg)](https://rubygems.org/gems/pbt)
4
+ [![Build Status](https://github.com/ohbarye/pbt/actions/workflows/main.yml/badge.svg)](https://github.com/ohbarye/pbt/actions/workflows/main.yml)
5
+ [![RubyDoc](https://img.shields.io/badge/%F0%9F%93%9ARubyDoc-documentation-informational.svg)](https://www.rubydoc.info/gems/pbt)
6
+
7
+ An experimental property-based testing tool for Ruby that allows you to run test cases in parallel.
8
+
9
+ PBT stands for Property-Based Testing.
10
+
11
+ ## What's Property-Based Testing?
12
+
13
+ Property-Based Testing is a testing methodology that focuses on the properties a system should always satisfy, rather than checking individual examples. Instead of writing tests for predefined inputs and outputs, PBT allows you to specify the general characteristics that your code should adhere to and then automatically generates a wide range of inputs to verify these properties.
14
+
15
+ The key benefits of property-based testing include the ability to cover more edge cases and the potential to discover bugs that traditional example-based tests might miss. It's particularly useful for identifying unexpected behaviors in your code by testing it against a vast set of inputs, including those you might not have considered.
16
+
17
+ For a more in-depth understanding of Property-Based Testing, please refer to external resources.
18
+
19
+ - Original ideas
20
+ - [Property-based testing of privileged programs](https://ieeexplore.ieee.org/document/367311) (1994)
21
+ - [Property-based testing: a new approach to testing for assurance](https://dl.acm.org/doi/abs/10.1145/263244.263267) (1997)
22
+ - [QuickCheck: a lightweight tool for random testing of Haskell programs](https://dl.acm.org/doi/10.1145/351240.351266) (2000)
23
+ - Rather new introductory resources
24
+ - Fred Hebert's book [Property-Based Testing With PropEr, Erlang and Elixir](https://propertesting.com/).
25
+ - [fast-check - Why Property-Based?](https://fast-check.dev/docs/introduction/why-property-based/)
4
26
 
5
27
  ## Installation
6
28
 
7
- Install the gem and add to the application's Gemfile by executing:
29
+ Add this line to your application's Gemfile and run `bundle install`.
8
30
 
9
- ```shell
10
- $ bundle add pbt
31
+ ```ruby
32
+ gem 'pbt'
11
33
  ```
12
34
 
13
- If bundler is not being used to manage dependencies, install the gem by executing:
35
+ If you want to use multi-processes or multi-threads (other than Ractor) as workers to run tests, install the [parallel](https://github.com/grosser/parallel) gem.
14
36
 
15
- ```shell
16
- $ gem install pbt
37
+ ```ruby
38
+ gem 'parallel'
39
+ ```
40
+
41
+ Off course you can install with `gem intstall pbt`.
42
+
43
+ ## Basic Usage
44
+
45
+ ### Simple property
46
+
47
+ ```ruby
48
+ # Let's say you have a method that returns just a multiplicative inverse.
49
+ def multiplicative_inverse(number)
50
+ Rational(1, number)
51
+ end
52
+
53
+ Pbt.assert do
54
+ # The given block is executed 100 times with different random numbers.
55
+ # Besides, the block runs in parallel by Ractor.
56
+ Pbt.property(Pbt.integer) do |number|
57
+ result = multiplicative_inverse(number)
58
+ raise "Result should be the multiplicative inverse of the number" if result * number != 1
59
+ end
60
+ end
61
+
62
+ # If the function has a bug, the test fails with a counterexample.
63
+ # For example, the multiplicative_inverse method doesn't work for 0 regardless of the behavior is intended or not.
64
+ #
65
+ # Pbt::PropertyFailure:
66
+ # Property failed after 23 test(s)
67
+ # { seed: 11001296583699917659214176011685741769 }
68
+ # Counterexample: 0
69
+ # Shrunk 3 time(s)
70
+ # Got ZeroDivisionError: divided by 0
71
+ ```
72
+
73
+ ### Explain The Snippet
74
+
75
+ The above snippet is very simple but contains the basic components.
76
+
77
+ #### Runner
78
+
79
+ `Pbt.assert` is the runner. The runner interprets and executes the given property. `Pbt.assert` takes a property and runs it multiple times. If the property fails, it tries to shrink the input that caused the failure.
80
+
81
+ #### Property
82
+
83
+ The snippet above declared a property by calling `Pbt.property`. The property describes the following:
84
+
85
+ 1. What the user wants to evaluate. This corresponds to the block (let's call this `predicate`) enclosed by `do` `end`
86
+ 2. How to generate inputs for the predicate — using `Arbitrary`
87
+
88
+ The `predicate` block is a function that directly asserts, taking values generated by `Arbitrary` as input.
89
+
90
+ #### Arbitrary
91
+
92
+ Arbitrary generates random values. It is also responsible for shrinking those values if asked to shrink a failed value as input.
93
+
94
+ Here, we used only one type of arbitrary, `Pbt.integer`. There are many other built-in arbitraries, and you can create a variety of inputs by combining existing ones.
95
+
96
+ #### Shrink
97
+
98
+ In PBT, If a test fails, it attempts to shrink the case that caused the failure into a form that is easier for humans to understand.
99
+ In other words, instead of stopping the test itself the first time it fails and reporting the failed value, it tries to find the minimal value that causes the error.
100
+
101
+ When there is a test that fails when given an even number, a counterexample of `2` is simpler and easier to understand than `432743417662`.
102
+
103
+ ### Arbitrary
104
+
105
+ There are many built-in arbitraries in `Pbt`. You can use them to generate random values for your tests. Here are some representative arbitraries.
106
+
107
+ #### Primitives
108
+
109
+ ```ruby
110
+ rng = Random.new(
111
+
112
+ Pbt.integer.generate(rng) # => 42
113
+ Pbt.integer(min: -1, max: 8).generate(rng) # => Integer between -1 and 8
114
+
115
+ Pbt.symbol.generate(rng) # => :atq
116
+
117
+ Pbt.ascii_char.generate(rng) # => "a"
118
+ Pbt.ascii_string.generate(rng) # => "aagjZfao"
119
+
120
+ Pbt.boolean.generate(rng) # => true or false
121
+ Pbt.constant(42).generate(rng) # => 42 always
122
+ ```
123
+
124
+ #### Composites
125
+
126
+ ```ruby
127
+ rng = Random.new
128
+
129
+ Pbt.array(Pbt.integer).generate(rng) # => [121, -13141, 9825]
130
+ Pbt.array(Pbt.integer, max: 1, empty: true).generate(rng) # => [] or [42] etc.
131
+
132
+ Pbt.tuple(Pbt.symbol, Pbt.integer).generate(rng) # => [:atq, 42]
133
+
134
+ Pbt.fixed_hash(x: Pbt.symbol, y: Pbt.integer).generate(rng) # => {x: :atq, y: 42}
135
+ Pbt.hash(Pbt.symbol, Pbt.integer).generate(rng) # => {atq: 121, ygab: -1142}
136
+
137
+ Pbt.one_of(:a, 1, 0.1).generate(rng) # => :a or 1 or 0.1
138
+ ````
139
+
140
+ See [ArbitraryMethods](https://github.com/ohbarye/pbt/blob/main/lib/pbt/arbitrary/arbitrary_methods.rb) module for more details.
141
+
142
+ ## Configuration
143
+
144
+ You can configure `Pbt` by calling `Pbt.configure` before running tests.
145
+
146
+ ```ruby
147
+ Pbt.configure do |config|
148
+ # Whether to print verbose output. Default is `false`.
149
+ config.verbose = 100
150
+
151
+ # The concurrency method to use. :ractor`, `:thread`, `:process` and `:none` are supported. Default is `:ractor`.
152
+ config.worker = :ractor
153
+
154
+ # The number of runs to perform. Default is `100`.
155
+ config.num_runs = 100
156
+
157
+ # The seed to use for random number generation.
158
+ # It's useful to reproduce failed test with the seed you'd pick up from failure messages. Default is a random seed.
159
+ config.seed = 42
160
+
161
+ # Whether to report exceptions in threads.
162
+ # It's useful to suppress error logs on Ractor that reports many errors. Default is `false`.
163
+ config.thread_report_on_exception = false
164
+ end
165
+ ```
166
+
167
+ Or, you can pass the configuration to `Pbt.assert` as an argument.
168
+
169
+ ```ruby
170
+ Pbt.assert(num_runs: 100, seed: 42) do
171
+ # ...
172
+ end
173
+ ```
174
+
175
+ ## Concurrent methods
176
+
177
+ One of the key features of `Pbt` is its ability to rapidly execute test cases in parallel or concurrently, using a large number of values (by default, `100`) generated by `Arbitrary`.
178
+
179
+ For concurrent processing, you can specify any of the three workers—`:ractor`, `:process`, or `:thread`—using the `worker` option. Alternatively, choose `:none` for serial execution.
180
+
181
+ `Pbt` supports 3 concurrency methods and 1 sequential one. You can choose one of them by setting the `worker` option.
182
+
183
+ ### Ractor
184
+
185
+ ```ruby
186
+ Pbt.assert(worker: :ractor) do
187
+ Pbt.property(Pbt.integer) do |n|
188
+ # ...
189
+ end
190
+ end
17
191
  ```
18
192
 
19
- ## Usage
193
+ #### Limitation
194
+
195
+ Please note that Ractor support is an experimental feature of this gem. Due to Ractor's limitations, you may encounter some issues when using it.
196
+
197
+ For example, you cannot access anything out of block.
20
198
 
21
199
  ```ruby
22
- # Let's say you have a method that returns just even numbers.
23
- def twice(number)
24
- number * 2
200
+ a = 1
201
+
202
+ Pbt.assert(worker: :ractor) do
203
+ Pbt.property(Pbt.integer) do |n|
204
+ # You cannot access `a` here because this block is executed in a Ractor and it doesn't allow implicit sharing of objects.
205
+ a + n # => Ractor::RemoteError (can not share object between ractors)
206
+ end
25
207
  end
208
+ ```
26
209
 
27
- RSpec.describe Pbt do
28
- it "works" do
29
- # The given block is executed 100 times with different random numbers.
30
- # Besides, the block runs in parallel by Ractor.
31
- Pbt.forall(Pbt::Generator.integer) do |number|
32
- result = twice(number)
33
- raise "Result should be even number" if result % 2 != 0
210
+ You cannot use any methods provided by test frameworks like `expect` or `assert` because they are not available in a Ractor.
211
+
212
+ ```ruby
213
+ it do
214
+ Pbt.assert(worker: :ractor) do
215
+ Pbt.property(Pbt.integer) do |n|
216
+ # This is not possible because `self` if a Ractor here.
217
+ expect(n).to be_an(Integer) # => Ractor::RemoteError (cause by NoMethodError for `expect` or `be_an`)
34
218
  end
35
-
36
- # If the function has a bug, the test fails with a counterexample.
37
- # Pbt::CaseFailure:
38
- # RuntimeError:
39
- # Failed on:
40
- # 0.5
219
+ end
220
+ end
221
+ ```
222
+
223
+ ### Process
224
+
225
+ ```ruby
226
+ Pbt.assert(worker: :process) do
227
+ Pbt.property(Pbt.integer) do |n|
228
+ # ...
229
+ end
230
+ end
231
+ ```
232
+
233
+ ### Thread
234
+
235
+ ```ruby
236
+ Pbt.assert(worker: :thread) do
237
+ Pbt.property(Pbt.integer) do |n|
238
+ # ...
239
+ end
240
+ end
241
+ ```
242
+
243
+ ### None
244
+
245
+ ```ruby
246
+ Pbt.assert(worker: :none) do
247
+ Pbt.property(Pbt.integer) do |n|
248
+ # ...
41
249
  end
42
250
  end
43
251
  ```
44
252
 
45
253
  ## TODOs
46
254
 
47
- - [ ] More generators
48
- - https://proper-testing.github.io/apidocs/
49
- - [ ] Enable to combine generators
50
- - e.g. `Pbt::Generator.list(Pbt::Generator.integer)`
51
- - [ ] More sophisticated syntax for property-based testing
52
- - e.g. `forall(integer) { |number| ... }` (Omit `Pbt` module)
53
- - [ ] Support for shrinking
54
- - [ ] Allow to use assertions
55
- - It's hard to pass assertions like `expect`, `assert` to a Ractor?
255
+ Once this project finishes the following, we will release v1.0.0.
256
+
257
+ - [x] Implement basic primitive arbitraries
258
+ - [x] Implement composite arbitraries
259
+ - [x] Support shrinking
260
+ - [x] Support multiple concurrency methods
261
+ - [x] Ractor
262
+ - [x] Process
263
+ - [x] Thread
264
+ - [x] None (Run tests sequentially)
265
+ - [x] Documentation
266
+ - [x] Add better examples
267
+ - [x] Arbitrary usage
268
+ - [x] Configuration
269
+ - [ ] Rich report like verbose mode
270
+ - [ ] Allow to use expectations and matchers provided by test framework in Ractor if possible.
271
+ - It'd be so hard to pass assertions like `expect`, `assert` to a Ractor.
272
+ - [ ] Benchmark
273
+ - [ ] More parallelism or faster execution if possible
56
274
 
57
275
  ## Development
58
276
 
59
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
277
+ ### Setup
278
+
279
+ ```shell
280
+ bin/setup
281
+ bundle exec rake # Run tests and lint at once
282
+ ```
283
+
284
+ ### Test
285
+
286
+ ```shell
287
+ bundle exec rspec
288
+ ```
289
+
290
+ ### Lint
60
291
 
61
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
292
+ ```shell
293
+ bundle exec rake standard:fix
294
+ ```
62
295
 
63
296
  ## Contributing
64
297
 
65
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/pbt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/pbt/blob/master/CODE_OF_CONDUCT.md).
298
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ohbarye/pbt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ohbarye/pbt/blob/master/CODE_OF_CONDUCT.md).
66
299
 
67
300
  ## License
68
301
 
69
302
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
70
303
 
304
+ ## Credits
305
+
306
+ This project draws a lot of inspiration from other testing tools, namely
307
+
308
+ - [fast-check](https://fast-check.dev/)
309
+ - [Loupe](https://github.com/vinistock/loupe)
310
+ - [RSpec](https://github.com/rspec/rspec)
311
+ - [Minitest](https://github.com/seattlerb/minitest)
312
+ - [Parallel](https://github.com/grosser/parallel)
313
+ - [PropCheck for Ruby](https://github.com/Qqwy/ruby-prop_check)
314
+ - [PropCheck for Elixir](https://github.com/alfert/propcheck)
315
+
71
316
  ## Code of Conduct
72
317
 
73
- Everyone interacting in the Pbt project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/pbt/blob/master/CODE_OF_CONDUCT.md).
318
+ Everyone interacting in the Pbt project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ohbarye/pbt/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pbt
4
+ module Arbitrary
5
+ # Abstract class for generating random values on type `T`.
6
+ #
7
+ # @abstract
8
+ class Arbitrary
9
+ # Generate a value of type `T`, based on the provided random number generator.
10
+ #
11
+ # @abstract
12
+ # @param rng [Random] Random number generator.
13
+ # @return [Object] Random value of type `T`.
14
+ def generate(rng)
15
+ raise NotImplementedError
16
+ end
17
+
18
+ # Shrink a value of type `T`.
19
+ # Must never be called with possibly invalid values.
20
+ #
21
+ # @abstract
22
+ # @param current [Object]
23
+ # @return [Enumerator<Object>]
24
+ def shrink(current)
25
+ raise NotImplementedError
26
+ end
27
+
28
+ # Create another arbitrary by applying `mapper` value by value.
29
+ #
30
+ # @example
31
+ # integer_generator = Pbt.integer
32
+ # num_str_generator = integer_arb.map(->(n){ n.to_s }, ->(s) {s.to_i})
33
+ #
34
+ # @param mapper [Proc] Proc to map generated values. Mainly used for generation.
35
+ # @param unmapper [Proc] Proc to unmap generated values. Used for shrinking.
36
+ # @return [MapArbitrary] New arbitrary with mapped elements
37
+ def map(mapper, unmapper)
38
+ MapArbitrary.new(self, mapper, unmapper)
39
+ end
40
+
41
+ # Create another arbitrary by filtering values against `refinement`.
42
+ # All the values produced by the resulting arbitrary satisfy `!!refinement(value) == true`.
43
+ #
44
+ # Be aware that using `filter` may reduce possible valid values and may impact the time required to generate a valid value.
45
+ #
46
+ # @example
47
+ # integer_generator = Pbt.integer
48
+ # even_integer_generator = integer_arb.filter { |x| x.even? }
49
+ # # or `integer_arb.filter(&:even?)`
50
+ #
51
+ # @param refinement [Proc] Predicate proc to test each produced element. Return true to keep the element, false otherwise.
52
+ # @return [FilterArbitrary] New arbitrary filtered using `refinement`.
53
+ def filter(&refinement)
54
+ FilterArbitrary.new(self, &refinement)
55
+ end
56
+ end
57
+ end
58
+ end