Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/CI.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Erlang CI

on:
push:
branches: [ "develop" ]
pull_request:
branches: [ "develop" ]
workflow_dispatch:

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest

container:
image: erlang:26

steps:
- uses: actions/checkout@v4
- name: Compile
run: rebar3 compile
- name: Run tests
run: rebar3 eunit
22 changes: 4 additions & 18 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,34 +1,20 @@
TARGET_DIR=ebin

COMPILE_OPTIONS=+debug_info +export_all
TEST_COMPILE_OPTIONS=+debug_info +export_all

REBAR3=/usr/bin/env rebar3

default: compile

all: compile test

compile:
$(RM) ebin
$(RM) ${TARGET_DIR}
$(REBAR3) escriptize
ln -s _build/default/bin/ ebin
ln -s _build/default/bin/ ${TARGET_DIR}

clean:
$(REBAR3) clean

test: FORCE
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/check_equiv.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/equivchecker_utils.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/typing.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/slicing.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/functions.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} test/scoping_tests.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} test/typing_tests.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/config.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/diff.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/equivchecker_testing.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/repo.erl
erlc ${TEST_COMPILE_OPTIONS} -I include -o ${TARGET_DIR} src/cli.erl
erl -eval 'scoping_tests:test(), typing_tests:test(), init:stop()' -noshell
$(REBAR3) eunit

FORCE:
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
- Uses property based testing to check the equivalence of code refactored by Wrangler
- Compares folders or commits of the same codebase with the original and refactored code (git)

## Disclaimer

Randomized testing in itself can never be sufficient for eliminating the possibility of bugs.
EquivcheckEr will never report false positives, but its entirely possible for it to omit true positives,
so it cannot replace engineering discipline.
As Dijkstra once said:
> "Testing shows the presence, not the absence of bugs"

## Dependencies

- [Wrangler](https://refactoringtools.github.io/docs/wrangler/)
Expand Down
4 changes: 2 additions & 2 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
{deps, [
jsone,
proper,
{wrangler, {git, "git@github.com:RefactoringTools/wrangler.git", {branch, "master"}}},
{parse_trans, {git, "git@github.com:uwiger/parse_trans.git", {branch, "master"}}}
{wrangler, {git, "https://github.com/RefactoringTools/wrangler.git", {branch, "master"}}},
{parse_trans, {git, "https://github.com/uwiger/parse_trans.git", {branch, "master"}}}
]
}.

Expand Down
14 changes: 7 additions & 7 deletions rebar.lock
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{"1.2.0",
[{<<"jsone">>,{pkg,<<"jsone">>,<<"1.8.0">>},0},
[{<<"jsone">>,{pkg,<<"jsone">>,<<"1.8.1">>},0},
{<<"parse_trans">>,
{git,"git@github.com:uwiger/parse_trans.git",
{ref,"cdb01ba260ba9a00b2aafa17affead0f6fac081c"}},
{git,"https://github.com/uwiger/parse_trans.git",
{ref,"d99fb36755c813a5db23e6f93741aa58323ef911"}},
0},
{<<"proper">>,{pkg,<<"proper">>,<<"1.4.0">>},0},
{<<"wrangler">>,
{git,"git@github.com:RefactoringTools/wrangler.git",
{ref,"e8b0159e136768f81a5ffb6d6d6ddd79ae55b007"}},
{git,"https://github.com/RefactoringTools/wrangler.git",
{ref,"0fac2d415952bc764dd4f283b89fa3c6fd2b39f1"}},
0}]}.
[
{pkg_hash,[
{<<"jsone">>, <<"347FF1FA700E182E1F9C5012FA6D737B12C854313B9AE6954CA75D3987D6C06D">>},
{<<"jsone">>, <<"6BC74D3863D55D420077346DA97C601711017A057F2FD1DF65D6D65DD562FBAB">>},
{<<"proper">>, <<"89A44B8C39D28BB9B4BE8E4D715D534905B325470F2E0EC5E004D12484A79434">>}]},
{pkg_hash_ext,[
{<<"jsone">>, <<"08560B78624A12E0B5E7EC0271EC8CA38EF51F63D84D84843473E14D9B12618C">>},
{<<"jsone">>, <<"C78918124148C51A7A84C678E39BBC6281F8CB582F1D88584628A98468E99738">>},
{<<"proper">>, <<"18285842185BD33EFBDA97D134A5CB5A0884384DB36119FEE0E3CFA488568CBB">>}]}
].
14 changes: 12 additions & 2 deletions src/check_equiv.erl
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,16 @@ get_typeinfo(Dir) ->
TyperOut = os:cmd("typer -I include -r " ++ Dir),
typing:types(TyperOut).

check_equiv(OrigDir, RefacDir) ->
check_equiv(OrigDir, RefacDir, IsVerbose) ->
Configs = config:load_config(),
equivchecker_utils:log(IsVerbose, "Loaded config", Configs),
typing:ensure_plt(Configs),

DiffOutput = os:cmd("diff -x '.git' -u0 -br " ++ OrigDir ++ " " ++ RefacDir),
Diffs = diff:diff(DiffOutput),
equivchecker_utils:log(IsVerbose, "Diffs found", Diffs),
ModFiles = diff:modified_files(Diffs),
equivchecker_utils:log(IsVerbose, "Modified files", ModFiles),
% TODO Compile everything for now
% Files = string:split(string:trim(os:cmd("find -name '*.erl'")), "\n", all),

Expand All @@ -71,12 +74,15 @@ check_equiv(OrigDir, RefacDir) ->
ModFiles),
{OrigTypeInfo, RefacTypeInfo} = {get_typeinfo(OrigDir), get_typeinfo(RefacDir)},
{OrigModFuns, RefacModFuns} = functions:modified_functions(Diffs, FileInfos),
equivchecker_utils:log(IsVerbose, "Original functions", OrigModFuns),
equivchecker_utils:log(IsVerbose, "Refactored functions", RefacModFuns),

CallGraph = functions:callgraph(OrigDir, RefacDir),
Types = typing:add_types(OrigTypeInfo, RefacTypeInfo),

% Gets back the functions that have to be tested
FunsToTest = slicing:scope(OrigModFuns, RefacModFuns, CallGraph, Types),
equivchecker_utils:log(IsVerbose, "Testing functions", RefacModFuns),

% Compile the necessary modules into two separate folders
% This is needed because QuickCheck has to evaluate to old and the new
Expand All @@ -87,19 +93,23 @@ check_equiv(OrigDir, RefacDir) ->
RefacFiles = lists:map(fun(File) -> filename:join([RefacDir, File]) end, ModFiles),

Seed = os:timestamp(), % seed for the PropEr generator
equivchecker_utils:log(IsVerbose, "Seed used for test generation", Seed),

compile(OrigFiles, ?ORIGINAL_BIN_FOLDER, Seed),
compile(RefacFiles, ?REFACTORED_BIN_FOLDER, Seed),

unzip_modules(),

{OrigNode, RefacNode} = start_nodes(),
equivchecker_utils:log(IsVerbose, "Started testing nodes"),

% This is needed because PropEr needs the source for constructing the generator
{ok, Dir} = file:get_cwd(),
file:set_cwd(OrigDir),

Result = equivchecker_testing:run_tests(FunsToTest, OrigNode, RefacNode, Types, CallGraph),
Result = equivchecker_testing:run_tests(FunsToTest, OrigNode, RefacNode, Types, CallGraph, IsVerbose),

file:set_cwd(Dir),
stop_nodes(OrigNode, RefacNode),
equivchecker_utils:log(IsVerbose, "Stopped testing nodes"),
Result.
113 changes: 73 additions & 40 deletions src/cli.erl
Original file line number Diff line number Diff line change
Expand Up @@ -5,62 +5,91 @@
-export([run/1]).

-spec handler(map()) -> none().
handler(#{target := Target, source := Source, json := Json, commit := Commit, stats := Stats}) when Commit ->
commit_to_commit(Source, Target, Json, Stats);
handler(#{target := Target, source := Source, json := Json, commit := Commit, stats := Stats}) when not Commit ->
folder_to_folder(Source, Target, Json, Stats);
handler(#{target := Target, json := Json, commit := Commit, stats := Stats}) when Commit ->
folder_to_commit(Target,Json,Stats);
handler(#{target := Target, json := Json, commit := Commit, stats := Stats}) when not Commit ->
folder_to_folder(Target, Json, Stats);
handler(#{json := Json, commit := _, stats := Stats}) ->
folder_to_commit(Json,Stats).

-spec commit_to_commit(commit(), commit(), boolean(), boolean()) -> none().
commit_to_commit(RefacCommit, OrigCommit, Json, Stats) ->
not Json andalso io:format("Checking commit ~p against commit ~p~n", [OrigCommit, RefacCommit]),
handler(#{target := Target,
source := Source,
json := Json,
commit := Commit,
stats := Stats,
verbose := IsVerbose})
when Commit ->
commit_to_commit(Source, Target, Json, Stats, IsVerbose);
handler(#{target := Target,
source := Source,
json := Json,
commit := Commit,
stats := Stats,
verbose := IsVerbose})
when not Commit ->
folder_to_folder(Source, Target, Json, Stats, IsVerbose);
handler(#{target := Target,
json := Json,
commit := Commit,
stats := Stats,
verbose := IsVerbose})
when Commit ->
folder_to_commit(Target, Json, Stats, IsVerbose);
handler(#{target := Target,
json := Json,
commit := Commit,
stats := Stats,
verbose := IsVerbose})
when not Commit ->
folder_to_folder(Target, Json, Stats, IsVerbose);
handler(#{json := Json,
commit := _,
stats := Stats,
verbose := IsVerbose}) ->
folder_to_commit(Json, Stats, IsVerbose).

-spec commit_to_commit(commit(), commit(), boolean(), boolean(), boolean()) -> none().
commit_to_commit(RefacCommit, OrigCommit, Json, Stats, IsVerbose) ->
not Json
andalso io:format("Checking commit ~p against commit ~p~n", [OrigCommit, RefacCommit]),
{ok, ProjFolder} = file:get_cwd(),
Original = repo:copy(ProjFolder, ?ORIGINAL_SOURCE_FOLDER),
repo:checkout(Original, OrigCommit),
Refactored = repo:copy(ProjFolder, ?REFACTORED_SOURCE_FOLDER),
repo:checkout(Refactored, RefacCommit),
run_check(Original, Refactored, Json, Stats).
run_check(Original, Refactored, Json, Stats, IsVerbose).

-spec folder_to_folder(filename(), filename(), boolean(), boolean()) -> none().
folder_to_folder(Refactored, Original, Json, Stats) ->
not Json andalso io:format("Checking folder ~p against folder ~p~n", [Original, Refactored]),
run_check(Original, Refactored, Json, Stats).
-spec folder_to_folder(filename(), filename(), boolean(), boolean(), boolean()) -> none().
folder_to_folder(Refactored, Original, Json, Stats, IsVerbose) ->
not Json
andalso io:format("Checking folder ~p against folder ~p~n", [Original, Refactored]),
run_check(Original, Refactored, Json, Stats, IsVerbose).

-spec folder_to_folder(filename(), boolean(), boolean()) -> none().
folder_to_folder(Original, Json, Stats) ->
-spec folder_to_folder(filename(), boolean(), boolean(), boolean()) -> none().
folder_to_folder(Original, Json, Stats, IsVerbose) ->
not Json andalso io:format("Checking current folder against ~p~n", [Original]),
{ok, Refactored} = file:get_cwd(),
run_check(Original, Refactored, Json, Stats).
run_check(Original, Refactored, Json, Stats, IsVerbose).

-spec folder_to_commit(commit(), boolean(), boolean()) -> none().
folder_to_commit(Commit, Json, Stats) ->
-spec folder_to_commit(commit(), boolean(), boolean(), boolean()) -> none().
folder_to_commit(Commit, Json, Stats, IsVerbose) ->
not Json andalso io:format("Checking current folder against commit ~p~n", [Commit]),
{ok, Refactored} = file:get_cwd(),
Original = repo:copy(Refactored, ?ORIGINAL_SOURCE_FOLDER),
repo:checkout(Original, Commit),
run_check(Original, Refactored, Json, Stats).
run_check(Original, Refactored, Json, Stats, IsVerbose).

-spec folder_to_commit(boolean(), boolean()) -> none().
folder_to_commit(Json, Stats) ->
-spec folder_to_commit(boolean(), boolean(), boolean()) -> none().
folder_to_commit(Json, Stats, IsVerbose) ->
not Json andalso io:format("Checking current folder against current commit~n"),
{ok, Refactored} = file:get_cwd(),
Commit = repo:current_commit(),
Original = repo:copy(Refactored, ?ORIGINAL_SOURCE_FOLDER),
repo:checkout(Original, Commit),
run_check(Original, Refactored, Json, Stats).
run_check(Original, Refactored, Json, Stats, IsVerbose).

-spec run_check(filename(), filename(), boolean(), boolean()) -> none().
run_check(Original, Refactored, Json, Stats) ->
Res = check_equiv:check_equiv(filename:absname(Original), filename:absname(Refactored)),
-spec run_check(filename(), filename(), boolean(), boolean(), boolean()) -> none().
run_check(Original, Refactored, Json, Stats, IsVerbose) ->
Res = check_equiv:check_equiv(
filename:absname(Original), filename:absname(Refactored), IsVerbose),
show_result(Res, Json, Stats).

setup() ->
% Sets the name of the master node
os:cmd("epmd"),
net_kernel:start(master, #{name_domain => shortnames}),

% These are needed for storing statistics
Expand Down Expand Up @@ -88,28 +117,30 @@ run(Args) ->
% Second arg turns on json output, third shows statistics
-spec show_result([{filename(), mfa(), [any()]}], boolean(), boolean()) -> none().
show_result(Result, false, false) ->
Formatted = format_results(Result,false),
Formatted = format_results(Result, false),
io:format("Results: ~p~n", [Formatted]);
show_result(Result, false, true) ->
{FailCounts, Average} = equivchecker_utils:statistics(),
Formatted = format_results(Result,false),
Formatted = format_results(Result, false),
io:format("Results: ~p~n", [Formatted]),
io:format("Number of functions that failed: ~p~n", [length(FailCounts)]),
io:format("Average no. tries before counterexample is found: ~p~n", [Average]);
show_result(Result, true, false) ->
Formatted = format_results(Result,true),
io:format("~s\n", [jsone:encode(Formatted,[{indent, 2}, {space, 1}])]);
Formatted = format_results(Result, true),
io:format("~s\n", [jsone:encode(Formatted, [{indent, 2}, {space, 1}])]);
show_result(Result, true, true) ->
{FailCounts, Average} = equivchecker_utils:statistics(),
Stats = #{failed_count => length(FailCounts), average_test_count => Average},
Output = #{statistics => Stats, results => format_results(Result,true)},
io:format("~s\n", [jsone:encode(Output,[{indent, 2}, {space, 1}])]).
Output = #{statistics => Stats, results => format_results(Result, true)},
io:format("~s\n", [jsone:encode(Output, [{indent, 2}, {space, 1}])]).

-spec format_results([{filename(), mfa(), [any()]}], boolean()) -> none().
format_results(Results, Json) ->
case Json of
true -> lists:map(fun format_json/1, Results);
false -> lists:map(fun format_stdout/1, Results)
true ->
lists:map(fun format_json/1, Results);
false ->
lists:map(fun format_stdout/1, Results)
end.

% Default formatting
Expand All @@ -120,4 +151,6 @@ format_stdout({FileName, MFA, [CounterExample]}) ->
% Format the output to json
-spec format_json({filename(), mfa(), [any()]}) -> map().
format_json({FileName, MFA, [CounterExample]}) ->
#{filename => erlang:list_to_atom(FileName), mfa => MFA, counterexample => CounterExample}.
#{filename => erlang:list_to_atom(FileName),
mfa => MFA,
counterexample => CounterExample}.
6 changes: 3 additions & 3 deletions src/diff.erl
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
%% This module is for extracting information from the diff, and presenting it
%% The main unit of the differences between two versions is a hunk, which contains
%% all the information about a change that is needed for finding out what has to be testsd
%% all the information about a change that is needed for finding out what has to be tested
-module(diff).

-include("equivchecker.hrl").
Expand Down Expand Up @@ -74,13 +74,13 @@ parse_diff(DiffStr) ->
-spec extract_file(string()) -> filename().
extract_file(DiffLine) ->
Options = [global, {capture, [1,2], list}],
{match, [[OrigFile, RefacFile]]} = re:run(DiffLine, ".*?(/.*?\.erl).*?(/.*?\.erl)", Options),
{match, [[OrigFile, RefacFile]]} = re:run(DiffLine, ".*?(/.*?\\.erl).*?(/.*?\\.erl)", Options),
equivchecker_utils:common_filename_postfix(OrigFile, RefacFile).

% Checks if the given file in the diff output is erlang source code
-spec is_erl_source([diff_line()]) -> boolean().
is_erl_source(Header) ->
case re:run(Header, ".*(.*\.erl).*") of
case re:run(Header, ".*(.*\\.erl).*") of
{match,_} -> true;
nomatch -> false
end.
3 changes: 2 additions & 1 deletion src/equivchecker.erl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ cli() ->
#{name => source, required => false},
#{name => json, type => boolean, short => $j, long => "-json", default => false},
#{name => commit, type => boolean, short => $c, long => "-commit", default => false},
#{name => stats, type => boolean, short => $s, long => "-statistics", default => false}
#{name => stats, type => boolean, short => $s, long => "-statistics", default => false},
#{name => verbose, type => boolean, short => $v, long => "-verbose", default => false}
],
handler => fun cli:run/1
}.
Expand Down
Loading