Pure Elixir concolic testing tool for finding bugs in your code.
ExCuter uses concolic (concrete + symbolic) execution to systematically explore paths through your code and find inputs that trigger errors.
- Elixir 1.14+
- Z3 SMT solver
macOS:
brew install z3Linux (Debian/Ubuntu):
apt install z3Verify installation:
z3 --versionNix
Include the z3 package in your mkShell's nativeBuildInputs, add to home
manager, or nix-env -i z3.
Add to your mix.exs:
def deps do
[
{:ex_cuter, git: "https://github.com/cuter-testing/ex_cuter.git", only: [:dev, :test]}
]
endCRITICAL: ExCuter needs debug_info to analyze your modules. Ensure your project has:
def project do
[
# ... other options
elixirc_options: [debug_info: true]
]
endThen run:
mix deps.get# Basic usage
mix ex_cuter MyModule my_function '[initial_args]'
# With options
mix ex_cuter MyModule my_function '[0]' --depth 15 --timeout 30000The idea is to run the task with the module name, the function name, the list of args in a single expression, and any options.
# Run concolic testing on a function
case ExCuter.run(MyModule, :my_function, [0], depth: 10) do
{:errors, error_inputs} ->
IO.puts("Found bugs with inputs: #{inspect(error_inputs)}")
{:ok, _} ->
IO.puts("No bugs found!")
{:error, reason} ->
IO.puts("Testing failed: #{inspect(reason)}")
endExCuter provides a test DSL for clean integration with ExUnit:
defmodule MyModuleTest do
use ExCuter.Test
# Test that a specific input triggers an error
cuter_test "finds bug at 42", MyModule, :buggy_func, [0] do
assert_error_found [42]
end
# Test that multiple specific inputs trigger errors
cuter_test "finds multiple bugs", MyModule, :buggy_func, [0] do
assert_errors_found [[42], [-42]]
end
# Test that a function has no bugs
cuter_test "safe function has no bugs", MyModule, :safe_func, [0], depth: 10 do
assert_no_errors()
end
# Test that at least one error exists
cuter_test "has bugs", MyModule, :buggy_func, [0] do
assert_any_error()
end
# Test with a predicate on error inputs
cuter_test "finds value > 100", MyModule, :buggy_func, [0] do
assert_error_satisfies fn [x] -> x > 100 end
end
# Custom assertions using get_errors()
cuter_test "custom check", MyModule, :buggy_func, [0] do
errors = get_errors()
assert length(errors) >= 2
end
endWhen using use ExCuter.Test, you can set defaults:
use ExCuter.Test, default_depth: 15, default_timeout: 120_000, async: falseIndividual tests can override with the opts argument:
cuter_test "deep search", MyModule, :func, [0], depth: 50, timeout: 180_000 do
assert_any_error()
endAll cuter tests are tagged with :cuter:
# Run only cuter tests
mix test --only cuter
# Run everything except cuter tests
mix test --exclude cuter- Concrete Execution: Run the function with actual input values
- Symbolic Tracking: Track symbolic constraints along the execution path
- Constraint Solving: Use Z3 to solve for inputs that take different paths
- Path Exploration: Systematically explore new paths by negating constraints
- Error Detection: Report inputs that cause the function to raise errors
- Integer arithmetic and comparisons
- Guards and pattern matching
- Lists, tuples, and binaries
- Recursive functions
- Higher-order functions (map, filter, fold, etc.)
- Multi-process execution (spawn, send, receive)
- Exception handling (try/catch/rescue)
- Binary pattern matching with various types/endianness
Given a buggy function:
defmodule Example do
def divide(x, y) do
if y == 0 do
raise "division by zero"
else
x / y
end
end
endExCuter will find that y = 0 triggers the error:
{:errors, [[_, 0]]} = ExCuter.run(Example, :divide, [10, 5], depth: 10)GPL-3.0 - See LICENSE file for details.