pbt 0.6.0 → 0.7.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f0aceef7e66e5512062e15dc72d8bf97617bf99f8c800b320381ef4e48562ab
4
- data.tar.gz: ff29061f4878170a57e1ce919cee6f065548349c876343c376e22047ef63c5c4
3
+ metadata.gz: 3275eafb6d07373c24f422feafa7300962213568a387cd21fbcd648c85de4193
4
+ data.tar.gz: 7a3b902bb323251050ec023ec8318244d777c0bc2584ad0c8bad2504d93888f0
5
5
  SHA512:
6
- metadata.gz: 68e5b018aaceebd522d8ef3f7e3837bc469b0a4513fd0e8e4080a01b637ca219694d9605c38752f60eb5cde1ee414c5116c0e153a8077ac1d9f8d28aa8ab3ef5
7
- data.tar.gz: 26e75baa90dfe41b0a8b0bcd625e8b39c69480e0a2343daca161ce0100fd6c72386c62d9c0a2afb5072c4baec39b4ccc41b499e650080093fad187791e883c60
6
+ metadata.gz: e72d6278580f540b7d0aad1aefb168c2fbc7a3e64d31a90a4929695b979d3d1c29e3dc06fb22fb0fc6d1aadfc121605176cbee7146e298201f9091c0fa2ea647
7
+ data.tar.gz: 8967245b883d22dfb00d79b0433acc53502e3e9ecdb07e89bae04002ce413fd67d126f695f4a4e12a965fc7adf171b5cd8e1c9fd3e6d8d28e8ff4444113cbea5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0] - 2026-04-04
4
+
5
+ - [Breaking change] Simplify stateful command protocol: `arguments` must now accept `state` parameter, and `applicable?` must now accept both `state` and `args` parameters
6
+ - Make `rng` optional in all arbitrary `generate` methods with default `Random.new` [#44](https://github.com/ohbarye/pbt/pull/44)
7
+ - Mark stateful testing API as stable (no longer experimental)
8
+
3
9
  ## [0.6.0] - 2026-03-15
4
10
 
5
11
  - Add experimental `Pbt.stateful` API for model-based stateful property testing [#38](https://github.com/ohbarye/pbt/pull/38)
data/README.md CHANGED
@@ -34,7 +34,7 @@ Add this line to your application's Gemfile and run `bundle install`.
34
34
  gem 'pbt'
35
35
  ```
36
36
 
37
- Off course you can install with `gem intstall pbt`.
37
+ Of course you can install with `gem install pbt`.
38
38
 
39
39
  ## Basic Usage
40
40
 
@@ -108,34 +108,37 @@ There are many built-in arbitraries in `Pbt`. You can use them to generate rando
108
108
  #### Primitives
109
109
 
110
110
  ```ruby
111
- rng = Random.new
111
+ Pbt.integer.generate # => 42
112
+ Pbt.integer(min: -1, max: 8).generate # => Integer between -1 and 8
112
113
 
113
- Pbt.integer.generate(rng) # => 42
114
- Pbt.integer(min: -1, max: 8).generate(rng) # => Integer between -1 and 8
114
+ Pbt.symbol.generate # => :atq
115
115
 
116
- Pbt.symbol.generate(rng) # => :atq
116
+ Pbt.ascii_char.generate # => "a"
117
+ Pbt.ascii_string.generate # => "aagjZfao"
117
118
 
118
- Pbt.ascii_char.generate(rng) # => "a"
119
- Pbt.ascii_string.generate(rng) # => "aagjZfao"
119
+ Pbt.boolean.generate # => true or false
120
+ Pbt.constant(42).generate # => 42 always
121
+ ```
122
+
123
+ You can also pass a custom random number generator if needed:
120
124
 
121
- Pbt.boolean.generate(rng) # => true or false
122
- Pbt.constant(42).generate(rng) # => 42 always
125
+ ```ruby
126
+ rng = Random.new(42) # with a specific seed for reproducibility
127
+ Pbt.integer.generate(rng)
123
128
  ```
124
129
 
125
130
  #### Composites
126
131
 
127
132
  ```ruby
128
- rng = Random.new
129
-
130
- Pbt.array(Pbt.integer).generate(rng) # => [121, -13141, 9825]
131
- Pbt.array(Pbt.integer, max: 1, empty: true).generate(rng) # => [] or [42] etc.
133
+ Pbt.array(Pbt.integer).generate # => [121, -13141, 9825]
134
+ Pbt.array(Pbt.integer, max: 1, empty: true).generate # => [] or [42] etc.
132
135
 
133
- Pbt.tuple(Pbt.symbol, Pbt.integer).generate(rng) # => [:atq, 42]
136
+ Pbt.tuple(Pbt.symbol, Pbt.integer).generate # => [:atq, 42]
134
137
 
135
- Pbt.fixed_hash(x: Pbt.symbol, y: Pbt.integer).generate(rng) # => {x: :atq, y: 42}
136
- Pbt.hash(Pbt.symbol, Pbt.integer).generate(rng) # => {atq: 121, ygab: -1142}
138
+ Pbt.fixed_hash(x: Pbt.symbol, y: Pbt.integer).generate # => {x: :atq, y: 42}
139
+ Pbt.hash(Pbt.symbol, Pbt.integer).generate # => {atq: 121, ygab: -1142}
137
140
 
138
- Pbt.one_of(:a, 1, 0.1).generate(rng) # => :a or 1 or 0.1
141
+ Pbt.one_of(:a, 1, 0.1).generate # => :a or 1 or 0.1
139
142
  ````
140
143
 
141
144
  See [ArbitraryMethods](https://github.com/ohbarye/pbt/blob/main/lib/pbt/arbitrary/arbitrary_methods.rb) module for more details.
@@ -169,11 +172,11 @@ class IncrementCommand
169
172
  :increment
170
173
  end
171
174
 
172
- def arguments
175
+ def arguments(_state)
173
176
  Pbt.nil
174
177
  end
175
178
 
176
- def applicable?(_state)
179
+ def applicable?(_state, _args)
177
180
  true
178
181
  end
179
182
 
@@ -220,8 +223,8 @@ end
220
223
  - `model.initial_state`
221
224
  - `model.commands(state)` -> `Array<command>`
222
225
  - `command.name`
223
- - `command.arguments` (a `Pbt` arbitrary)
224
- - `command.applicable?(state)` -> `true` / `false`
226
+ - `command.arguments(state)` (a `Pbt` arbitrary, may depend on current model state)
227
+ - `command.applicable?(state, args)` -> `true` / `false`
225
228
  - `command.next_state(state, args)` -> next model state
226
229
  - `command.run!(sut, args)` -> command result
227
230
  - `command.verify!(before_state:, after_state:, args:, result:, sut:)`
@@ -421,7 +424,7 @@ Once this project finishes the following, we will release v1.0.0.
421
424
  - [ ] Statistics feature to aggregate generated values
422
425
  - [ ] Decide DSL
423
426
  - [ ] Try Fiber
424
- - [ ] Stateful property-based testing
427
+ - [x] Stateful property-based testing (experimental, via `Pbt.stateful`)
425
428
 
426
429
  ## Development
427
430
 
@@ -11,9 +11,9 @@ module Pbt
11
11
  # Generate a value of type `T`, based on the provided random number generator.
12
12
  #
13
13
  # @abstract
14
- # @param rng [Random] Random number generator.
14
+ # @param rng [Random] Random number generator. Defaults to a new Random instance.
15
15
  # @return [Object] Random value of type `T`.
16
- def generate(rng)
16
+ def generate(rng = Random.new)
17
17
  raise NotImplementedError
18
18
  end
19
19
 
@@ -20,7 +20,7 @@ module Pbt
20
20
  end
21
21
 
22
22
  # @see Arbitrary#generate
23
- def generate(rng)
23
+ def generate(rng = Random.new)
24
24
  length = @length_arb.generate(rng)
25
25
  length.times.map { @value_arb.generate(rng) }
26
26
  end
@@ -10,7 +10,7 @@ module Pbt
10
10
  end
11
11
 
12
12
  # @see Arbitrary#generate
13
- def generate(rng)
13
+ def generate(rng = Random.new)
14
14
  rng.rand(@range)
15
15
  end
16
16
 
@@ -10,7 +10,7 @@ module Pbt
10
10
  end
11
11
 
12
12
  # @see Arbitrary#generate
13
- def generate(rng)
13
+ def generate(rng = Random.new)
14
14
  @val
15
15
  end
16
16
 
@@ -12,7 +12,7 @@ module Pbt
12
12
  end
13
13
 
14
14
  # @see Arbitrary#generate
15
- def generate(rng)
15
+ def generate(rng = Random.new)
16
16
  loop do
17
17
  val = @arb.generate(rng)
18
18
  return val if @refinement.call(val)
@@ -11,7 +11,7 @@ module Pbt
11
11
  end
12
12
 
13
13
  # @see Arbitrary#generate
14
- def generate(rng)
14
+ def generate(rng = Random.new)
15
15
  values = @arb.generate(rng)
16
16
  @keys.zip(values).to_h
17
17
  end
@@ -15,7 +15,7 @@ module Pbt
15
15
  end
16
16
 
17
17
  # @see Arbitrary#generate
18
- def generate(rng)
18
+ def generate(rng = Random.new)
19
19
  rng.rand(@min..@max)
20
20
  rescue ArgumentError => e
21
21
  raise EmptyDomainError, e.message if @min > @max
@@ -14,7 +14,7 @@ module Pbt
14
14
  end
15
15
 
16
16
  # @see Arbitrary#generate
17
- def generate(rng)
17
+ def generate(rng = Random.new)
18
18
  @mapper.call(@arb.generate(rng))
19
19
  end
20
20
 
@@ -11,7 +11,7 @@ module Pbt
11
11
  end
12
12
 
13
13
  # @see Arbitrary#generate
14
- def generate(rng)
14
+ def generate(rng = Random.new)
15
15
  @choices[@idx_arb.generate(rng)]
16
16
  end
17
17
 
@@ -10,7 +10,7 @@ module Pbt
10
10
  end
11
11
 
12
12
  # @see Arbitrary#generate
13
- def generate(rng)
13
+ def generate(rng = Random.new)
14
14
  @arbs.map { |arb| arb.generate(rng) }
15
15
  end
16
16
 
@@ -16,7 +16,7 @@ module Pbt
16
16
  #
17
17
  # @param rng [Random] Random number generator.
18
18
  # @return [Object]
19
- def generate(rng)
19
+ def generate(rng = Random.new)
20
20
  @arb.generate(rng)
21
21
  end
22
22
 
@@ -50,7 +50,7 @@ module Pbt
50
50
  #
51
51
  # @param rng [Random]
52
52
  # @return [Array<Step>]
53
- def generate(rng)
53
+ def generate(rng = Random.new)
54
54
  length = rng.rand(0..@max_steps)
55
55
  state = @model.initial_state
56
56
  sequence = []
@@ -207,11 +207,6 @@ module Pbt
207
207
  "Pbt.stateful command protocol mismatch for #{command.class} " \
208
208
  "(name=#{safe_command_label(command)}, missing: #{missing_methods.join(", ")}, context=#{context})"
209
209
  end
210
-
211
- validate_command_signature!(command, :arguments, valid_counts: [0, 1], expectation: "arguments or arguments(state)",
212
- context:)
213
- validate_command_signature!(command, :applicable?, valid_counts: [1, 2],
214
- expectation: "applicable?(state) or applicable?(state, args)", context:)
215
210
  end
216
211
 
217
212
  # @param command [Object]
@@ -252,12 +247,6 @@ module Pbt
252
247
  # @param context [String]
253
248
  # @return [Object]
254
249
  def generate_applicable_args(command, state, rng, context:)
255
- unless arg_aware_command?(command)
256
- return NoApplicableArgs unless applicable?(command, state, nil, context:)
257
-
258
- return arbitrary_for(command, state, context:).generate(rng)
259
- end
260
-
261
250
  arbitrary = arbitrary_for(command, state, context:)
262
251
 
263
252
  ARG_AWARE_GENERATION_ATTEMPTS.times do
@@ -275,15 +264,7 @@ module Pbt
275
264
  # @param context [String]
276
265
  # @return [Object]
277
266
  def arguments_for(command, state, context:)
278
- method = command.method(:arguments)
279
-
280
- if supports_argument_count?(method, 1)
281
- command.arguments(state)
282
- elsif supports_argument_count?(method, 0)
283
- command.arguments
284
- else
285
- raise_invalid_signature!(command, :arguments, "arguments or arguments(state)", context)
286
- end
267
+ command.arguments(state)
287
268
  end
288
269
 
289
270
  # @param command [Object]
@@ -303,8 +284,6 @@ module Pbt
303
284
  def generate_args_for(command, arbitrary, rng)
304
285
  arbitrary.generate(rng)
305
286
  rescue Pbt::Arbitrary::EmptyDomainError
306
- raise unless state_aware_arg_aware_command?(command)
307
-
308
287
  NoApplicableArgs
309
288
  end
310
289
 
@@ -314,84 +293,7 @@ module Pbt
314
293
  # @param context [String]
315
294
  # @return [Boolean]
316
295
  def applicable?(command, state, args, context:)
317
- method = command.method(:applicable?)
318
-
319
- if supports_argument_count?(method, 2)
320
- command.applicable?(state, args)
321
- elsif supports_argument_count?(method, 1)
322
- command.applicable?(state)
323
- else
324
- raise_invalid_signature!(command, :applicable?, "applicable?(state) or applicable?(state, args)", context)
325
- end
326
- end
327
-
328
- # @param command [Object]
329
- # @return [Boolean]
330
- def arg_aware_command?(command)
331
- supports_argument_count?(command.method(:applicable?), 2)
332
- end
333
-
334
- # @param command [Object]
335
- # @return [Boolean]
336
- def state_aware_arg_aware_command?(command)
337
- arg_aware_command?(command) && supports_argument_count?(command.method(:arguments), 1)
338
- end
339
-
340
- # @param command [Object]
341
- # @param method_name [Symbol]
342
- # @param valid_counts [Array<Integer>]
343
- # @param expectation [String]
344
- # @param context [String]
345
- # @return [void]
346
- def validate_command_signature!(command, method_name, valid_counts:, expectation:, context:)
347
- method = command.method(method_name)
348
- return if valid_counts.any? { |count| supports_argument_count?(method, count) }
349
-
350
- raise_invalid_signature!(command, method_name, expectation, context)
351
- end
352
-
353
- # @param command [Object]
354
- # @param method_name [Symbol]
355
- # @param expectation [String]
356
- # @param context [String]
357
- # @return [void]
358
- def raise_invalid_signature!(command, method_name, expectation, context)
359
- raise Pbt::InvalidConfiguration,
360
- "Pbt.stateful command protocol mismatch for #{command.class} " \
361
- "(name=#{safe_command_label(command)}, invalid #{method_name} signature; expected #{expectation}, context=#{context})"
362
- end
363
-
364
- # @param method [Method]
365
- # @param count [Integer]
366
- # @return [Boolean]
367
- def supports_argument_count?(method, count)
368
- return false if method.parameters.any? { |kind, _name| keyword_parameter?(kind) }
369
-
370
- required = 0
371
- optional = 0
372
- rest = false
373
-
374
- method.parameters.each do |kind, _name|
375
- case kind
376
- when :req
377
- required += 1
378
- when :opt
379
- optional += 1
380
- when :rest
381
- rest = true
382
- end
383
- end
384
-
385
- return false if count < required
386
- return true if rest
387
-
388
- count <= required + optional
389
- end
390
-
391
- # @param kind [Symbol]
392
- # @return [Boolean]
393
- def keyword_parameter?(kind)
394
- %i[keyreq key keyrest].include?(kind)
296
+ command.applicable?(state, args)
395
297
  end
396
298
 
397
299
  # @param sequence [Array<Hash, Step>]
data/lib/pbt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pbt
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pbt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ohbarye