Packages

AST-based analyzer for identifying property-based testing candidates in Elixir codebases. Detects pure functions, identifies testable patterns, finds inverse function pairs, and generates concrete property-based test suggestions.

Current section

Files

Jump to
propwise lib prop_wise suggestion_generator.ex
Raw

lib/prop_wise/suggestion_generator.ex

defmodule PropWise.SuggestionGenerator do
@moduledoc """
Generates property-based testing suggestions for different libraries.
Supports stream_data and PropEr.
"""
@doc """
Generates testing suggestions based on detected patterns and library.
"""
def generate(patterns, function_info, library) do
func_name = function_info.name
module_name = function_info.module |> String.split(".") |> List.last()
patterns
|> Enum.flat_map(fn {type, _reason} ->
generate_for_pattern(type, module_name, func_name, library)
end)
|> Enum.uniq()
end
defp generate_for_pattern(:collection_operation, module_name, func_name, :stream_data) do
[
"""
property "preserves input size" do
check all list <- list_of(term()) do
assert length(#{module_name}.#{func_name}(list)) == length(list)
end
end
""",
"""
property "contains all original elements" do
check all list <- list_of(term()) do
result = #{module_name}.#{func_name}(list)
assert Enum.all?(list, &(&1 in result))
end
end
"""
]
end
defp generate_for_pattern(:collection_operation, module_name, func_name, :proper) do
[
"""
property "preserves input size" do
forall list <- list(term()) do
length(#{module_name}.#{func_name}(list)) == length(list)
end
end
""",
"""
property "contains all original elements" do
forall list <- list(term()) do
result = #{module_name}.#{func_name}(list)
Enum.all?(list, &(&1 in result))
end
end
"""
]
end
defp generate_for_pattern(:transformation, module_name, func_name, :stream_data) do
[
"""
property "maintains structural invariants" do
check all input <- term() do
result = #{module_name}.#{func_name}(input)
# Add your invariant checks here
assert valid_structure?(result)
end
end
""",
"""
property "handles edge cases" do
check all input <- one_of([constant(nil), constant([]), constant(%{}), term()]) do
result = #{module_name}.#{func_name}(input)
assert is_valid_result?(result)
end
end
"""
]
end
defp generate_for_pattern(:transformation, module_name, func_name, :proper) do
[
"""
property "maintains structural invariants" do
forall input <- term() do
result = #{module_name}.#{func_name}(input)
# Add your invariant checks here
valid_structure?(result)
end
end
""",
"""
property "handles edge cases" do
forall input <- oneof([nil, [], %{}, term()]) do
result = #{module_name}.#{func_name}(input)
is_valid_result?(result)
end
end
"""
]
end
defp generate_for_pattern(:validation, module_name, func_name, :stream_data) do
[
"""
property "consistent validation results" do
check all input <- term() do
result1 = #{module_name}.#{func_name}(input)
result2 = #{module_name}.#{func_name}(input)
assert result1 == result2
end
end
""",
"""
property "boolean return type" do
check all input <- term() do
result = #{module_name}.#{func_name}(input)
assert is_boolean(result)
end
end
"""
]
end
defp generate_for_pattern(:validation, module_name, func_name, :proper) do
[
"""
property "consistent validation results" do
forall input <- term() do
result1 = #{module_name}.#{func_name}(input)
result2 = #{module_name}.#{func_name}(input)
result1 == result2
end
end
""",
"""
property "boolean return type" do
forall input <- term() do
result = #{module_name}.#{func_name}(input)
is_boolean(result)
end
end
"""
]
end
defp generate_for_pattern(:algebraic, module_name, func_name, :stream_data) do
[
"""
property "associativity" do
check all a <- term(), b <- term(), c <- term() do
assert #{module_name}.#{func_name}(#{module_name}.#{func_name}(a, b), c) ==
#{module_name}.#{func_name}(a, #{module_name}.#{func_name}(b, c))
end
end
""",
"""
property "commutativity" do
check all a <- term(), b <- term() do
assert #{module_name}.#{func_name}(a, b) == #{module_name}.#{func_name}(b, a)
end
end
""",
"""
property "identity element" do
check all a <- term() do
identity = identity_value()
assert #{module_name}.#{func_name}(a, identity) == a
end
end
"""
]
end
defp generate_for_pattern(:algebraic, module_name, func_name, :proper) do
[
"""
property "associativity" do
forall {a, b, c} <- {term(), term(), term()} do
#{module_name}.#{func_name}(#{module_name}.#{func_name}(a, b), c) ==
#{module_name}.#{func_name}(a, #{module_name}.#{func_name}(b, c))
end
end
""",
"""
property "commutativity" do
forall {a, b} <- {term(), term()} do
#{module_name}.#{func_name}(a, b) == #{module_name}.#{func_name}(b, a)
end
end
""",
"""
property "identity element" do
forall a <- term() do
identity = identity_value()
#{module_name}.#{func_name}(a, identity) == a
end
end
"""
]
end
defp generate_for_pattern(:encoder_decoder, module_name, _func_name, :stream_data) do
[
"""
property "encode/decode round-trip" do
check all data <- term() do
encoded = #{module_name}.encode(data)
assert #{module_name}.decode(encoded) == {:ok, data}
end
end
""",
"""
property "decode handles invalid input" do
check all invalid <- binary() do
case #{module_name}.decode(invalid) do
{:ok, _} -> true
{:error, _} -> true
_ -> false
end
end
end
"""
]
end
defp generate_for_pattern(:encoder_decoder, module_name, _func_name, :proper) do
[
"""
property "encode/decode round-trip" do
forall data <- term() do
encoded = #{module_name}.encode(data)
#{module_name}.decode(encoded) == {:ok, data}
end
end
""",
"""
property "decode handles invalid input" do
forall invalid <- binary() do
case #{module_name}.decode(invalid) do
{:ok, _} -> true
{:error, _} -> true
_ -> false
end
end
end
"""
]
end
defp generate_for_pattern(:parser, module_name, func_name, :stream_data) do
[
"""
property "parse returns expected structure" do
check all input <- string(:alphanumeric) do
case #{module_name}.#{func_name}(input) do
{:ok, result} -> assert valid_parsed_structure?(result)
{:error, _} -> true
end
end
end
""",
"""
property "parse/format round-trip" do
check all data <- valid_data_generator() do
formatted = #{module_name}.format(data)
assert #{module_name}.#{func_name}(formatted) == {:ok, data}
end
end
"""
]
end
defp generate_for_pattern(:parser, module_name, func_name, :proper) do
[
"""
property "parse returns expected structure" do
forall input <- list(range(?a, ?z)) do
case #{module_name}.#{func_name}(to_string(input)) do
{:ok, result} -> valid_parsed_structure?(result)
{:error, _} -> true
end
end
end
""",
"""
property "parse/format round-trip" do
forall data <- valid_data_generator() do
formatted = #{module_name}.format(data)
#{module_name}.#{func_name}(formatted) == {:ok, data}
end
end
"""
]
end
defp generate_for_pattern(:numeric, module_name, func_name, :stream_data) do
[
"""
property "handles numeric boundaries" do
check all n <- one_of([integer(), float()]) do
result = #{module_name}.#{func_name}(n)
assert is_number(result)
end
end
""",
"""
property "handles special numeric values" do
check all n <- member_of([0, -1, 1, :math.pi(), -:math.pi()]) do
result = #{module_name}.#{func_name}(n)
assert is_valid_numeric?(result)
end
end
"""
]
end
defp generate_for_pattern(:numeric, module_name, func_name, :proper) do
[
"""
property "handles numeric boundaries" do
forall n <- oneof([integer(), float()]) do
result = #{module_name}.#{func_name}(n)
is_number(result)
end
end
""",
"""
property "handles special numeric values" do
forall n <- oneof([0, -1, 1]) do
result = #{module_name}.#{func_name}(n)
is_valid_numeric?(result)
end
end
"""
]
end
defp generate_for_pattern(_type, _module_name, _func_name, _library), do: []
end