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
Current section
Files
lib/prop_wise/reporter.ex
defmodule PropWise.Reporter do
@moduledoc """
Formats and outputs analysis results.
"""
@doc """
Prints analysis results in a human-readable format.
"""
def print_report(analysis_result, opts \\ []) do
format = Keyword.get(opts, :format, :text)
case format do
:text -> print_text_report(analysis_result)
:json -> print_json_report(analysis_result)
_ -> print_text_report(analysis_result)
end
end
defp print_text_report(%{
candidates: candidates,
inverse_pairs: inverse_pairs,
total_functions: total,
candidates_count: count,
dropped_count: dropped
}) do
IO.puts("\n" <> String.duplicate("=", 80))
IO.puts("PropWise Analysis Report")
IO.puts(String.duplicate("=", 80))
IO.puts("\nSummary:")
IO.puts(" Total functions analyzed: #{total}")
IO.puts(" Property test candidates: #{count}")
IO.puts(" Candidates dropped (below threshold): #{dropped}")
if count > 0 do
percentage = Float.round(count / total * 100, 1)
IO.puts(" Coverage: #{percentage}%")
end
if not Enum.empty?(inverse_pairs) do
IO.puts("\n" <> String.duplicate("-", 80))
IO.puts("Inverse Function Pairs Detected:")
IO.puts(String.duplicate("-", 80))
for pair <- inverse_pairs do
{mod, name1, arity1} = pair.forward
{_mod, name2, arity2} = pair.inverse
IO.puts("\n #{mod}.#{name1}/#{arity1} <-> #{name2}/#{arity2}")
IO.puts(" Suggestion: #{pair.suggestion}")
end
end
if Enum.empty?(candidates) do
IO.puts("\nNo strong candidates found. Consider lowering the min_score threshold.")
else
IO.puts("\n" <> String.duplicate("-", 80))
IO.puts("Top Candidates (sorted by score):")
IO.puts(String.duplicate("-", 80))
candidates
|> Enum.take(20)
|> Enum.each(&print_candidate/1)
end
IO.puts("\n" <> String.duplicate("=", 80))
IO.puts("")
end
defp print_candidate(candidate) do
IO.puts("\n#{candidate.module}.#{candidate.name}/#{candidate.arity}")
IO.puts(" Score: #{candidate.score}")
IO.puts(" Location: #{relative_path(candidate.file)}:#{candidate.line}")
IO.puts(" Type: #{candidate.type}")
if not Enum.empty?(candidate.patterns) do
IO.puts(" Patterns:")
for {type, reason} <- candidate.patterns do
IO.puts(" - #{format_pattern(type)}: #{reason}")
end
end
if not Enum.empty?(candidate.suggestions) do
IO.puts(" Testing suggestions:")
for suggestion <- candidate.suggestions do
IO.puts(" - #{suggestion}")
end
end
end
defp print_json_report(analysis_result) do
json =
analysis_result
|> Map.update!(:candidates, fn candidates ->
Enum.map(candidates, &serialize_candidate/1)
end)
|> Map.update!(:inverse_pairs, fn pairs ->
Enum.map(pairs, &serialize_inverse_pair/1)
end)
|> Jason.encode!(pretty: true)
IO.puts(json)
end
defp serialize_candidate(candidate) do
%{
module: candidate.module,
name: to_string(candidate.name),
arity: candidate.arity,
file: candidate.file,
line: candidate.line,
type: candidate.type,
score: candidate.score,
patterns:
Enum.map(candidate.patterns, fn {type, reason} -> %{type: type, reason: reason} end),
suggestions: candidate.suggestions
}
end
defp serialize_inverse_pair(pair) do
{mod1, name1, arity1} = pair.forward
{mod2, name2, arity2} = pair.inverse
%{
forward: %{
module: mod1,
name: to_string(name1),
arity: arity1
},
inverse: %{
module: mod2,
name: to_string(name2),
arity: arity2
},
suggestion: pair.suggestion
}
end
defp format_pattern(:collection_operation), do: "Collection Operation"
defp format_pattern(:transformation), do: "Data Transformation"
defp format_pattern(:validation), do: "Validation"
defp format_pattern(:algebraic), do: "Algebraic Structure"
defp format_pattern(:encoder_decoder), do: "Encoder/Decoder"
defp format_pattern(:parser), do: "Parser"
defp format_pattern(:numeric), do: "Numeric Algorithm"
defp format_pattern(other), do: to_string(other)
defp relative_path(path) do
cwd = File.cwd!()
if String.starts_with?(path, cwd) do
String.replace_prefix(path, cwd <> "/", "")
else
path
end
end
end