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/command_line.ex
defmodule PropWise.CommandLine do
@moduledoc """
Shared command-line argument parsing and analysis logic for both
the escript CLI and the Mix task.
"""
alias PropWise.{Analyzer, Reporter}
@parse_opts [
strict: [
min_score: :integer,
format: :string,
output: :string,
library: :string,
no_fail: :boolean,
help: :boolean
],
aliases: [
m: :min_score,
f: :format,
o: :output,
l: :library,
h: :help
]
]
@spec parse_args([String.t()]) :: {keyword(), [String.t()]}
def parse_args(args) do
{opts, paths, _} = OptionParser.parse(args, @parse_opts)
{opts, paths}
end
@spec run_analysis(String.t(), keyword(), keyword()) ::
:ok | {:suggestions_found, non_neg_integer()} | {:error, String.t()}
def run_analysis(path, opts, callbacks \\ []) do
do_run_analysis(path, opts, callbacks)
catch
{:invalid_path, p} -> {:error, "#{p} is not a valid directory"}
end
defp do_run_analysis(path, opts, callbacks) do
format = Keyword.get(opts, :format, "text") |> String.to_atom()
error_fn = Keyword.get(callbacks, :error, &default_error/1)
info_fn = Keyword.get(callbacks, :info, &default_info/1)
unless File.dir?(path) do
error_fn.("Error: #{path} is not a valid directory")
throw({:invalid_path, path})
end
min_score = Keyword.get(opts, :min_score, 4)
library = parse_library(opts, format, error_fn)
if format == :text do
info_fn.("Analyzing #{path}...")
end
analyzer_opts = [min_score: min_score]
analyzer_opts =
if library, do: Keyword.put(analyzer_opts, :library, library), else: analyzer_opts
result = Analyzer.analyze_project(path, analyzer_opts)
output_file = Keyword.get(opts, :output)
if output_file do
output = Reporter.format_report(result, format: format)
File.write!(output_file, output)
else
Reporter.print_report(result, format: format)
end
no_fail = Keyword.get(opts, :no_fail, false)
if format != :json and result.candidates_count > 0 and not no_fail do
{:suggestions_found, result.candidates_count}
else
:ok
end
end
@spec help_text() :: String.t()
def help_text do
"""
PropWise - Property-Based Testing Candidate Detector
Usage:
propwise [OPTIONS] [PATH]
Arguments:
[PATH] Path to the Elixir project to analyze (default: .)
Options:
-m, --min-score NUM Minimum score for candidates (default: 4)
-f, --format FORMAT Output format: text or json (default: text)
-o, --output FILE Write output to file instead of stdout
-l, --library LIB Property testing library: stream_data or proper (default: stream_data)
--no-fail Exit with code 0 even when suggestions are found
-h, --help Show this help message
Examples:
propwise
propwise --min-score 5
propwise --format json
propwise --library proper
propwise ./my_project
The tool analyzes your Elixir codebase to find functions that are good
candidates for property-based testing. It looks for:
- Pure functions (no side effects)
- Collection operations
- Data transformations
- Encoders/decoders and inverse function pairs
- Validation functions
- Algebraic structures
Each candidate is scored based on multiple factors and includes
suggestions for what properties to test.
Exit codes:
0 - No suggestions found
1 - Suggestions found or error occurred
"""
end
defp parse_library(opts, format, error_fn) do
case Keyword.get(opts, :library) do
nil ->
nil
"stream_data" ->
:stream_data
"proper" ->
:proper
other ->
if format == :text do
error_fn.("Warning: Unknown library '#{other}', using default")
end
nil
end
end
defp default_error(msg), do: IO.puts(:stderr, msg)
defp default_info(msg), do: IO.puts(msg)
end