Class: Pbt::Stateful::Property

Inherits:
Object
  • Object
show all
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

Constructor Details

#initialize(model:, sut:, max_steps:) ⇒ Property

Returns a new instance of Property.

Parameters:

  • model (Object)
  • sut (Proc)
  • max_steps (Integer)

Raises:



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.

Parameters:

  • rng (Random) (defaults to: Random.new)

Returns:



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.

Parameters:

  • sequence (Array<Hash, 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.message} [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.

Parameters:

  • _sequence (Array<Hash, Step>)

Raises:



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.

Parameters:

  • sequence (Array<Hash, Step>)

Returns:

  • (Enumerator<Array<Hash, Step>>)


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

Returns:

  • (Boolean)


101
102
103
# File 'lib/pbt/stateful/property.rb', line 101

def stateful?
  true
end