Current section
Files
Jump to
Current section
Files
lib/property_generator/generators.ex
defmodule PropertyGenerator.Generators do
@moduledoc """
Functions for generating valid test data from type specifications.
"""
use ExUnitProperties
@doc """
Creates input generators from type specifications.
Returns a generator that produces lists of arguments.
"""
def create_input_generator(input_types) do
generators = Enum.map(input_types, &type_to_generator/1)
case generators do
[single_generator] ->
StreamData.bind(single_generator, fn value ->
StreamData.constant([value])
end)
multiple_generators ->
StreamData.bind(StreamData.tuple(List.to_tuple(multiple_generators)), fn tuple ->
StreamData.constant(Tuple.to_list(tuple))
end)
end
end
@doc """
Converts a type specification to a StreamData generator.
"""
def type_to_generator({:type, _, :integer, []}), do: StreamData.integer()
def type_to_generator({:type, _, :float, []}), do: StreamData.float()
def type_to_generator({:type, _, :boolean, []}), do: StreamData.boolean()
def type_to_generator({:type, _, :binary, []}), do: StreamData.binary()
def type_to_generator({:type, _, :bitstring, []}), do: StreamData.bitstring()
def type_to_generator({:type, _, :atom, []}), do: StreamData.atom(:alphanumeric)
def type_to_generator({:type, _, :string, []}), do: StreamData.string(:printable)
def type_to_generator({:type, _, :any, []}), do: StreamData.term()
def type_to_generator({:type, _, :term, []}), do: StreamData.term()
def type_to_generator({:type, _, nil, []}), do: StreamData.constant(nil)
def type_to_generator({:atom, _, atom_value}), do: StreamData.constant(atom_value)
def type_to_generator({:integer, _, int_value}), do: StreamData.constant(int_value)
def type_to_generator({:type, _, :charlist, []}) do
StreamData.list_of(StreamData.integer(0..1_114_111))
end
def type_to_generator({:type, _, :non_neg_integer, []}) do
StreamData.integer(0..1000)
end
def type_to_generator({:type, _, :pos_integer, []}) do
StreamData.integer(1..1000)
end
def type_to_generator({:type, _, :neg_integer, []}) do
StreamData.integer(-1000..-1)
end
def type_to_generator({:type, _, :range, [min, max]}) do
min_val = extract_integer_value(min, 0)
max_val = extract_integer_value(max, 100)
StreamData.integer(min_val..max_val)
end
def type_to_generator({:type, _, :list, [element_type]}) do
case element_type do
{:type, _, :tuple, [{:atom, _, key}, value_type]} ->
# Keyword list
value_gen = type_to_generator(value_type)
StreamData.map(value_gen, fn value -> [{key, value}] end)
_ ->
# Regular list
StreamData.list_of(type_to_generator(element_type))
end
end
def type_to_generator({:type, _, :list, []}), do: StreamData.list_of(StreamData.term())
def type_to_generator({:type, _, :tuple, element_types}) do
element_generators = Enum.map(element_types, &type_to_generator/1)
StreamData.tuple(List.to_tuple(element_generators))
end
def type_to_generator({:type, _, :map, []}),
do: StreamData.map_of(StreamData.atom(:alphanumeric), StreamData.term())
def type_to_generator({:type, _, :map, :any}),
do: StreamData.map_of(StreamData.atom(:alphanumeric), StreamData.term())
def type_to_generator({:type, _, :map, field_types}) when is_list(field_types) do
struct_field = find_struct_field(field_types)
case struct_field do
{:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, module_name}]} ->
generate_struct(module_name, field_types)
nil ->
generate_map(field_types)
end
end
def type_to_generator({:type, _, :union, types}) do
generators = Enum.map(types, &type_to_generator/1)
StreamData.one_of(generators)
end
def type_to_generator({:remote_type, _, [{:atom, _, String}, {:atom, _, :t}, []]}) do
StreamData.string(:printable)
end
def type_to_generator({:remote_type, _, [{:atom, _, module}, {:atom, _, :t}, []]}) do
# Handle remote type references like User.t()
case Code.ensure_loaded(module) do
{:module, ^module} ->
case Code.Typespec.fetch_types(module) do
{:ok, types} ->
# Find the @type t definition
type_def =
Enum.find_value(types, fn
{:type, {:t, type_ast, []}} -> type_ast
_ -> nil
end)
case type_def do
{:type, _, :map, field_types} ->
# It's a struct type, generate it
type_to_generator({:type, 0, :map, field_types})
nil ->
IO.warn("Could not find @type t for module #{module}, using StreamData.term()")
StreamData.term()
other_type ->
# It's some other type, generate it
type_to_generator(other_type)
end
_ ->
IO.warn("Could not fetch types for module #{module}, using StreamData.term()")
StreamData.term()
end
_ ->
IO.warn("Could not load module #{module}, using StreamData.term()")
StreamData.term()
end
end
def type_to_generator(type) do
IO.warn("Unknown type #{inspect(type)}, using StreamData.term()")
StreamData.term()
end
# Private helper functions
defp extract_integer_value({:integer, _, val}, _default), do: val
defp extract_integer_value(val, _default) when is_integer(val), do: val
defp extract_integer_value(_val, default), do: default
defp find_struct_field(field_types) do
Enum.find(field_types, fn
{:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, _module}]} -> true
_ -> false
end)
end
defp generate_struct(module_name, field_types) do
other_fields =
Enum.reject(field_types, fn
{:type, _, :map_field_exact, [{:atom, _, :__struct__}, _]} -> true
_ -> false
end)
field_values = Enum.map(other_fields, &generate_field_value/1)
case field_values do
[] ->
StreamData.constant(struct(module_name, %{}))
_ ->
StreamData.bind(StreamData.tuple(List.to_tuple(field_values)), fn field_tuple ->
field_map = Map.new(Tuple.to_list(field_tuple))
StreamData.constant(struct(module_name, field_map))
end)
end
end
defp generate_field_value({:type, _, field_type, [{:atom, _, field_name}, value_type]})
when field_type in [:map_field_exact, :map_field_assoc] do
value_gen = type_to_generator(value_type)
StreamData.map(value_gen, fn value -> {field_name, value} end)
end
defp generate_map(field_types) do
field_generators =
Enum.map(field_types, fn
{:type, _, field_type, [key_type, value_type]}
when field_type in [:map_field_exact, :map_field_assoc] ->
key_gen = type_to_generator(key_type)
value_gen = type_to_generator(value_type)
StreamData.tuple({key_gen, value_gen})
end)
StreamData.map(
StreamData.list_of(StreamData.one_of(field_generators), min_length: 1),
fn pairs -> Map.new(pairs) end
)
end
end