Packages

Automatically compare two implementations of the same problem with property-based testing and performance benchmarks. Perfect for refactoring, algorithm comparison, and validating that different implementations produce identical results.

Current section

Files

Jump to
ab lib property_generator invalid_generators.ex
Raw

lib/property_generator/invalid_generators.ex

defmodule PropertyGenerator.InvalidGenerators do
@moduledoc """
Functions for generating invalid test data that does NOT match type specifications.
Used for robustness testing to ensure functions properly reject invalid input.
"""
use ExUnitProperties
@doc """
Creates invalid input generators from type specifications.
Returns a generator that produces lists of invalid arguments.
"""
def create_invalid_input_generator(input_types) do
invalid_generators = Enum.map(input_types, &type_to_invalid_generator/1)
case invalid_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 an invalid StreamData generator.
"""
def type_to_invalid_generator({:type, _, :integer, []}) do
non_integer_types()
end
def type_to_invalid_generator({:type, _, :float, []}) do
StreamData.one_of([
StreamData.integer(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.float()),
generic_map()
])
end
def type_to_invalid_generator({:type, _, :boolean, []}) do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.list_of(StreamData.atom(:alphanumeric))
])
end
def type_to_invalid_generator({:type, _, :binary, []}) do
non_binary_types()
end
def type_to_invalid_generator({:type, _, :bitstring, []}) do
StreamData.one_of([
StreamData.integer(),
StreamData.atom(:alphanumeric),
generic_map()
])
end
def type_to_invalid_generator({:type, _, :atom, []}) do
non_atom_types()
end
def type_to_invalid_generator({:type, _, :string, []}) do
non_string_types()
end
def type_to_invalid_generator({:type, _, :list, _}) do
non_list_types()
end
def type_to_invalid_generator({:type, _, :tuple, _}) do
non_tuple_types()
end
def type_to_invalid_generator({:type, _, :map, _}) do
non_map_types()
end
def type_to_invalid_generator({:type, _, nil, []}) do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.term())
])
end
def type_to_invalid_generator({:atom, _, atom_value}) do
StreamData.one_of([
StreamData.atom(:alphanumeric),
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable)
])
|> StreamData.filter(fn value -> value != atom_value end)
end
def type_to_invalid_generator({:integer, _, int_value}) do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric)
])
|> StreamData.filter(fn value -> value != int_value end)
end
def type_to_invalid_generator({:type, _, :charlist, []}) do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
generic_map()
])
end
def type_to_invalid_generator({:type, _, :any, []}), do: StreamData.term()
def type_to_invalid_generator({:type, _, :term, []}), do: StreamData.term()
def type_to_invalid_generator({:type, _, :range, _}) do
StreamData.one_of([
StreamData.integer(-1000..-1),
StreamData.integer(1001..2000),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric)
])
end
def type_to_invalid_generator({:type, _, :non_neg_integer, []}) do
StreamData.one_of([
StreamData.integer(-1000..-1),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric)
])
end
def type_to_invalid_generator({:type, _, :pos_integer, []}) do
StreamData.one_of([
StreamData.integer(-1000..0),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric)
])
end
def type_to_invalid_generator({:type, _, :neg_integer, []}) do
StreamData.one_of([
StreamData.integer(0..1000),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric)
])
end
def type_to_invalid_generator({:type, _, :union, _types}) do
generic_invalid_types()
end
def type_to_invalid_generator({:remote_type, _, [{:atom, _, String}, {:atom, _, :t}, []]}) do
non_string_types()
end
def type_to_invalid_generator({:remote_type, _, [{:atom, _, _module}, {:atom, _, :t}, []]}) do
# For custom types like User.t(), generate invalid types (anything except the struct itself)
# This will generate non-struct types that should fail the function
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.term()),
StreamData.map_of(StreamData.atom(:alphanumeric), StreamData.term())
])
end
def type_to_invalid_generator({:type, _, :fun, [{:type, _, :product, arg_types}, return_type]}) do
# For function types, generate both non-functions AND functions with wrong arity
expected_arity = length(arg_types)
wrong_arities = [0, 1, 2, 3, 4] -- [expected_arity]
# Get the return type generator for making wrong-arity functions
return_gen = PropertyGenerator.Generators.type_to_generator(return_type)
wrong_arity_generators =
Enum.map(wrong_arities, fn arity ->
StreamData.map(return_gen, fn return_value ->
case arity do
0 -> fn -> return_value end
1 -> fn _ -> return_value end
2 -> fn _, _ -> return_value end
3 -> fn _, _, _ -> return_value end
4 -> fn _, _, _, _ -> return_value end
end
end)
end)
# Mix wrong-arity functions with non-functions
StreamData.one_of([
# Non-functions (most common invalid case)
StreamData.one_of([
StreamData.integer(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.integer()),
generic_map()
]),
# Functions with wrong arity
StreamData.one_of(wrong_arity_generators)
])
end
def type_to_invalid_generator(type) do
IO.warn("Unknown type #{inspect(type)}, using generic invalid generator")
generic_invalid_types()
end
# Private helper functions for common invalid type generators
defp non_integer_types do
StreamData.one_of([
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.integer()),
generic_map()
])
end
defp non_binary_types do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.term()),
generic_map()
])
end
defp non_atom_types do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.list_of(StreamData.term()),
generic_map()
])
end
defp non_string_types do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.term()),
generic_map()
])
end
defp non_list_types do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
generic_map()
])
end
defp non_tuple_types do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.term()),
generic_map()
])
end
defp non_map_types do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.term())
])
end
defp generic_invalid_types do
StreamData.one_of([
StreamData.integer(),
StreamData.float(),
StreamData.string(:printable),
StreamData.atom(:alphanumeric),
StreamData.list_of(StreamData.term()),
generic_map()
])
end
defp generic_map do
StreamData.map_of(StreamData.atom(:alphanumeric), StreamData.term())
end
end