PropertyDamage.Generator (PropertyDamage v0.2.0)

View Source

Generates command sequences for stateful property-based testing.

This module produces PropertyDamage.Sequence structs that can be either:

  • Linear sequences: Commands executed sequentially
  • Branching sequences: Commands with parallel execution branches

Linear Sequence Generation

Generator.generate_sequence(MyModel, max_commands: 20)
# => StreamData producing %Sequence{prefix: [...], branches: nil}

Branching Sequence Generation

Generator.generate_sequence(MyModel,
  max_commands: 20,
  branching: [
    branch_probability: 0.3,  # 30% chance to create branch point
    max_branches: 3,          # Up to 3 parallel branches
    max_branch_length: 5      # Each branch max 5 commands
  ]
)
# => StreamData producing %Sequence{prefix: [...], branches: [[...], [...]], suffix: [...]}

Ref Dependency Rules

When generating branching sequences, the generator enforces ref isolation:

  • Refs created in prefix can be used in any branch
  • Refs created in one branch CANNOT be used in another branch
  • Refs created in branches CAN be used in suffix

Auto-Lifting

Raw values passed as overrides are automatically wrapped in StreamData.constant/1:

merge_overrides(base, %{currency: "USD"})
merge_overrides(base, %{currency: StreamData.member_of(["USD", "EUR"])})

Summary

Functions

List the external placeholders available in projection state.

A seeded generator that picks one external placeholder from state.

Generate a command sequence for a given model.

Deterministically realizes a single value from a StreamData generator.

Merge overrides into base generators, auto-lifting raw values.

Derives the effective seed for a given run number from the base seed.

Check if a value is a StreamData generator.

Types

command()

@type command() :: struct()

state()

@type state() :: map()

weighted_command()

@type weighted_command() :: {pos_integer(), module()}

Functions

available_externals(state, opts \\ [])

@spec available_externals(
  map(),
  keyword()
) :: [struct()]

List the external placeholders available in projection state.

During generation, external() markers in simulated events become %PropertyDamage.Placeholder{} structs embedded in projection state. This surfaces them so a model's with: function can route one into a command that consumes a server-generated value.

Options

  • :event_module - keep only placeholders produced by this event module
  • :path - keep only placeholders at this field path (e.g. [:id])

Example

# In the model's command list:
{ViewOrder, with: fn state ->
  %{order_id: PropertyDamage.Generator.external_from(state, path: [:id])}
end}

external_from(state, opts \\ [])

@spec external_from(
  map(),
  keyword()
) :: StreamData.t(struct() | nil)

A seeded generator that picks one external placeholder from state.

Returns StreamData.constant(nil) when no matching external is available, so a with: function can guard on nil. Accepts the same options as available_externals/2.

generate_sequence(model, opts \\ [])

@spec generate_sequence(
  module(),
  keyword()
) :: StreamData.t(PropertyDamage.Sequence.t())

Generate a command sequence for a given model.

Parameters

  • model - Model module defining commands and state projection
  • opts - Options:
    • :max_commands - Maximum total commands per sequence (default: 50)
    • :branching - Keyword list for branching configuration (see below)

Note: the returned generator is pure; reproducibility comes from consuming it with generate_value/3 and an explicit seed.

Branching Options

Pass branching: [...] to generate branching sequences:

  • :branch_probability - Probability of creating a branch point (default: 0.2)
  • :max_branches - Maximum number of parallel branches (default: 3)
  • :max_branch_length - Maximum commands per branch (default: 5)
  • :min_prefix_length - Minimum commands before branching (default: 3)

If :branching is not provided, generates linear sequences.

Returns

A StreamData generator that produces PropertyDamage.Sequence structs.

Examples

# Linear sequence
Generator.generate_sequence(MyModel, max_commands: 20)

# Branching sequence with 30% branch probability
Generator.generate_sequence(MyModel,
  max_commands: 30,
  branching: [branch_probability: 0.3, max_branches: 2]
)

generate_value(generator, seed, opts \\ [])

@spec generate_value(StreamData.t(val), integer(), keyword()) :: val when val: var

Deterministically realizes a single value from a StreamData generator.

Consuming a generator via Enum/Enumerable seeds from the wall clock (see StreamData docs), which silently breaks seed reproducibility. All framework code MUST realize generated values through this function.

merge_overrides(base, overrides)

@spec merge_overrides(map(), map()) :: map()

Merge overrides into base generators, auto-lifting raw values.

Raw values are automatically wrapped in StreamData.constant/1. StreamData generators are passed through unchanged.

Parameters

  • base - Map of field names to StreamData generators
  • overrides - Map of field names to values or generators to override

Returns

A map suitable for passing to StreamData.fixed_map/1.

Examples

iex> base = %{amount: StreamData.positive_integer(), currency: StreamData.constant("USD")}
iex> result = PropertyDamage.Generator.merge_overrides(base, %{currency: "EUR"})
iex> is_map(result)
true

run_seed(seed, run_number)

@spec run_seed(integer(), non_neg_integer()) :: integer()

Derives the effective seed for a given run number from the base seed.

Run 0 uses the base seed unchanged, so re-running a failure's reported seed with max_runs: 1 regenerates exactly the failing sequence. Later runs get independent, well-mixed sub-seeds.

stream_data?(arg1)

@spec stream_data?(any()) :: boolean()

Check if a value is a StreamData generator.

Examples

iex> PropertyDamage.Generator.stream_data?(StreamData.integer())
true

iex> PropertyDamage.Generator.stream_data?(42)
false