Class: Pbt::Stateful::Property
- Inherits:
-
Object
- Object
- Pbt::Stateful::Property
- Defined in:
- lib/pbt/stateful/property.rb
Overview
Property-compatible wrapper for command-based stateful testing.
It provides generate, shrink and run, so existing runners can execute it.
Defined Under Namespace
Classes: Step
Constant Summary collapse
- ARG_AWARE_GENERATION_ATTEMPTS =
5- REQUIRED_COMMAND_METHODS =
%i[ name arguments applicable? next_state run! verify! ].freeze
Instance Method Summary collapse
-
#generate(rng = Random.new) ⇒ Array<Step>
Generate a sequence of commands valid for the current model state.
-
#initialize(model:, sut:, max_steps:) ⇒ Property
constructor
A new instance of Property.
-
#run(sequence) ⇒ void
Run the command sequence against a fresh SUT and verify each step.
-
#run_in_ractor(_sequence) ⇒ Object
Stateful properties currently require sequential execution because the model, commands and SUT factory are ordinary Ruby objects and are not guaranteed to be Ractor-shareable.
-
#shrink(sequence) ⇒ Enumerator<Array<Hash, Step>>
Shrink a sequence by trying shorter prefixes first.
- #stateful? ⇒ Boolean
Constructor Details
#initialize(model:, sut:, max_steps:) ⇒ Property
Returns a new instance of Property.
38 39 40 41 42 43 44 45 46 47 |
# File 'lib/pbt/stateful/property.rb', line 38 def initialize(model:, sut:, max_steps:) validate_model!(model) raise Pbt::InvalidConfiguration, "sut must be callable" unless sut.respond_to?(:call) raise Pbt::InvalidConfiguration, "max_steps must be an Integer" unless max_steps.is_a?(Integer) raise Pbt::InvalidConfiguration, "max_steps must be non-negative" if max_steps.negative? @model = model @sut_factory = sut @max_steps = max_steps end |
Instance Method Details
#generate(rng = Random.new) ⇒ Array<Step>
Generate a sequence of commands valid for the current model state.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/pbt/stateful/property.rb', line 53 def generate(rng = Random.new) length = rng.rand(0..@max_steps) state = @model.initial_state sequence = [] length.times do candidates = generate_candidates_for(state, rng, context: "generate") break if candidates.empty? command, args = candidates[rng.rand(candidates.length)] sequence << Step.new(command:, args:) state = command.next_state(state, args) end sequence end |
#run(sequence) ⇒ void
This method returns an undefined value.
Run the command sequence against a fresh SUT and verify each step.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/pbt/stateful/property.rb', line 119 def run(sequence) state = @model.initial_state sut = @sut_factory.call sequence.each_with_index do |step, index| command, args = unpack_step(step) validate_command_protocol!(command, state:, context: "run step #{index}") unless applicable?(command, state, args, context: "run step #{index}") raise "invalid stateful sequence at step #{index}: #{command_name(command)}" end before_state = state begin after_state = command.next_state(before_state, args) result = command.run!(sut, args) command.verify!( before_state:, after_state:, args:, result:, sut: ) rescue Exception => e # standard:disable Lint/RescueException: raise e.class, "stateful step #{index} (#{command_name(command)}): #{e.} [args=#{args.inspect}]", e.backtrace end state = after_state end end |
#run_in_ractor(_sequence) ⇒ Object
Stateful properties currently require sequential execution because the model, commands and SUT factory are ordinary Ruby objects and are not guaranteed to be Ractor-shareable.
111 112 113 |
# File 'lib/pbt/stateful/property.rb', line 111 def run_in_ractor(_sequence) raise Pbt::InvalidConfiguration, "Pbt.stateful does not support worker: :ractor yet; use worker: :none" end |
#shrink(sequence) ⇒ Enumerator<Array<Hash, Step>>
Shrink a sequence by trying shorter prefixes first.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/pbt/stateful/property.rb', line 74 def shrink(sequence) Enumerator.new do |y| seen = {} state = @model.initial_state (sequence.length - 1).downto(0) do |length| yield_shrink_candidate(y, seen, sequence.first(length)) end sequence.each_with_index do |step, index| command, args = unpack_step(step) validate_command_protocol!(command, state:, context: "shrink step #{index}") break unless applicable?(command, state, args, context: "shrink step #{index}") arbitrary_for(command, state, context: "shrink step #{index}").shrink(args).each do |shrunk_args| candidate = replace_step(sequence, index, command:, args: shrunk_args) next unless valid_sequence?(candidate) yield_shrink_candidate(y, seen, candidate) end state = command.next_state(state, args) end end end |
#stateful? ⇒ Boolean
101 102 103 |
# File 'lib/pbt/stateful/property.rb', line 101 def stateful? true end |