aboutsummaryrefslogtreecommitdiffstats
path: root/lib/dialyzer/test/dialyzer_common.erl
diff options
context:
space:
mode:
Diffstat (limited to 'lib/dialyzer/test/dialyzer_common.erl')
-rw-r--r--lib/dialyzer/test/dialyzer_common.erl377
1 files changed, 377 insertions, 0 deletions
diff --git a/lib/dialyzer/test/dialyzer_common.erl b/lib/dialyzer/test/dialyzer_common.erl
new file mode 100644
index 0000000000..51766a4604
--- /dev/null
+++ b/lib/dialyzer/test/dialyzer_common.erl
@@ -0,0 +1,377 @@
+%%% File : dialyzer_common.erl
+%%% Author : Stavros Aronis <[email protected]>
+%%% Description : Generator and common infrastructure for simple dialyzer
+%%% test suites (some options, some input files or directories
+%%% and the relevant results).
+%%% Created : 11 Jun 2010 by Stavros Aronis <stavros@enjoy>
+
+-module(dialyzer_common).
+
+-export([check_plt/1, check/4, create_all_suites/0, new_tests/2]).
+
+-include_lib("kernel/include/file.hrl").
+
+-define(suite_suffix, "_SUITE").
+-define(data_folder, "_data").
+-define(suite_data, ?suite_suffix ++ ?data_folder).
+-define(erlang_extension, ".erl").
+-define(output_file_mode, write).
+-define(dialyzer_option_file, "dialyzer_options").
+-define(input_files_directory, "src").
+-define(result_files_directory, "results").
+-define(plt_filename,"dialyzer_plt").
+-define(home_plt_filename,".dialyzer_plt").
+-define(plt_lockfile,"plt_lock").
+-define(required_modules, [erts, kernel, stdlib]).
+
+-record(suite, {suitename :: string(),
+ outputfile :: file:io_device(),
+ options :: options(),
+ testcases :: [testcase()]}).
+
+-record(options, {time_limit = 1 :: integer(),
+ dialyzer_options = [] :: dialyzer:dial_options()}).
+
+-type options() :: #options{}.
+-type testcase() :: {atom(), 'file' | 'dir'}.
+
+-spec check_plt(string()) -> ok.
+
+check_plt(OutDir) ->
+ io:format("Checking plt:"),
+ PltFilename = filename:join(OutDir, ?plt_filename),
+ case file:read_file_info(PltFilename) of
+ {ok, _} -> dialyzer_check_plt(PltFilename);
+ {error, _ } ->
+ io:format("No plt found in test run directory!"),
+ PltLockFile = filename:join(OutDir, ?plt_lockfile),
+ case file:read_file_info(PltLockFile) of
+ {ok, _} ->
+ explain_fail_with_lock(),
+ fail;
+ {error, _} ->
+ io:format("Locking plt generation."),
+ case file:open(PltLockFile,[?output_file_mode]) of
+ {ok, OutFile} ->
+ io:format(OutFile,"Locking plt generation.",[]),
+ file:close(OutFile);
+ {error, Reason} ->
+ io:format("Couldn't write lock file ~p.",[Reason]),
+ fail
+ end,
+ obtain_plt(PltFilename)
+ end
+ end.
+
+dialyzer_check_plt(PltFilename) ->
+ try dialyzer:run([{analysis_type, plt_check},
+ {init_plt, PltFilename}]) of
+ [] -> ok
+ catch
+ Class:Info ->
+ io:format("Failed. The error was: ~w\n~p",[Class, Info]),
+ io:format("A previously run dialyzer suite failed to generate"
+ " a correct plt."),
+ fail
+ end.
+
+explain_fail_with_lock() ->
+ io:format("Some other suite started creating a plt. It might not have"
+ " finished (Dialyzer's suites shouldn't run in parallel), or"
+ " it reached timeout and was killed (in which case"
+ " plt_timeout, defined in dialyzer_test_constants.hrl"
+ " should be increased), or it failed.").
+
+obtain_plt(PltFilename) ->
+ io:format("Obtaining plt:"),
+ HomeDir = os:getenv("HOME"),
+ HomePlt = filename:join(HomeDir, ?home_plt_filename),
+ io:format("Will try to use ~s as a starting point and add otp apps ~w.",
+ [HomePlt, ?required_modules]),
+ try dialyzer:run([{analysis_type, plt_add},
+ {apps, ?required_modules},
+ {output_plt, PltFilename},
+ {init_plt, HomePlt}]) of
+ [] ->
+ io:format("Successfully added everything!"),
+ ok
+ catch
+ Class:Reason ->
+ io:format("Failed. The error was: ~w\n~p",[Class, Reason]),
+ build_plt(PltFilename)
+ end.
+
+build_plt(PltFilename) ->
+ io:format("Building plt from scratch:"),
+ try dialyzer:run([{analysis_type, plt_build},
+ {apps, ?required_modules},
+ {output_plt, PltFilename}]) of
+ [] ->
+ io:format("Successfully created plt!"),
+ ok
+ catch
+ Class:Reason ->
+ io:format("Failed. The error was: ~w\n~p",[Class, Reason]),
+ fail
+ end.
+
+-spec check(atom(), dialyzer:dial_options(), string(), string()) ->
+ 'same' | {differ, [term()]}.
+
+check(TestCase, Opts, Dir, OutDir) ->
+ PltFilename = filename:join(OutDir, ?plt_filename),
+ SrcDir = filename:join(Dir, ?input_files_directory),
+ ResDir = filename:join(Dir, ?result_files_directory),
+ Filename = filename:join(SrcDir, atom_to_list(TestCase)),
+ Files =
+ case file_utils:file_type(Filename) of
+ {ok, 'directory'} ->
+ {ok, ListFiles} = file_utils:list_dir(Filename, ".erl",
+ false),
+ ListFiles;
+ {error, _} ->
+ FilenameErl = Filename ++ ".erl",
+ case file_utils:file_type(FilenameErl) of
+ {ok, 'regular'} -> [FilenameErl]
+ end
+ end,
+ ResFile = atom_to_list(TestCase),
+ NewResFile = filename:join(OutDir, ResFile),
+ OldResFile = filename:join(ResDir, ResFile),
+ ProperOpts = fix_options(Opts, Dir),
+ try dialyzer:run([{files, Files},{from, src_code},{init_plt, PltFilename},
+ {check_plt, false}|ProperOpts]) of
+ RawWarns ->
+ Warns = lists:sort([dialyzer:format_warning(W) || W <- RawWarns]),
+ case Warns of
+ [] -> ok;
+ _ ->
+ case file:open(NewResFile,[?output_file_mode]) of
+ {ok, OutFile} ->
+ io:format(OutFile,"\n~s",[Warns]),
+ file:close(OutFile);
+ Other -> erlang:error(Other)
+ end
+ end,
+ case file_utils:diff(NewResFile, OldResFile) of
+ 'same' -> file:delete(NewResFile),
+ 'same';
+ Any -> escape_strings(Any)
+ end
+ catch
+ Kind:Error -> {'dialyzer crashed', Kind, Error}
+ end.
+
+fix_options(Opts, Dir) ->
+ fix_options(Opts, Dir, []).
+
+fix_options([], _Dir, Acc) ->
+ Acc;
+fix_options([{pa, Path} | Rest], Dir, Acc) ->
+ case code:add_patha(filename:join(Dir, Path)) of
+ true -> fix_options(Rest, Dir, Acc);
+ {error, _} -> erlang:error("Bad directory for pa: " ++ Path)
+ end;
+fix_options([{DirOption, RelativeDirs} | Rest], Dir, Acc)
+ when DirOption =:= include_dirs ;
+ DirOption =:= files_rec ;
+ DirOption =:= files ->
+ ProperRelativeDirs = [filename:join(Dir,RDir) || RDir <- RelativeDirs],
+ fix_options(Rest, Dir, [{include_dirs, ProperRelativeDirs} | Acc]);
+fix_options([Opt | Rest], Dir, Acc) ->
+ fix_options(Rest, Dir, [Opt | Acc]).
+
+-spec new_tests(string(), [atom()]) -> [atom()].
+
+new_tests(Dirname, DeclaredTestcases) ->
+ SrcDir = filename:join(Dirname, ?input_files_directory),
+ get_testcases(SrcDir) -- DeclaredTestcases.
+
+get_testcases(Dirname) ->
+ {ok, Files} = file_utils:list_dir(Dirname, ".erl", true),
+ [list_to_atom(filename:basename(F,".erl")) || F <-Files].
+
+-spec create_all_suites() -> 'ok'.
+
+create_all_suites() ->
+ {ok, Cwd} = file:get_cwd(),
+ Suites = get_suites(Cwd),
+ lists:foreach(fun create_suite/1, Suites).
+
+escape_strings({differ,List}) ->
+ Map = fun({T,L,S}) -> {T,L,xmerl_lib:export_text(S)} end,
+ {differ, lists:keysort(3, lists:map(Map, List))}.
+
+-spec get_suites(file:filename()) -> [string()].
+
+get_suites(Dir) ->
+ case file:list_dir(Dir) of
+ {error, _} -> [];
+ {ok, Filenames} ->
+ FullFilenames = [filename:join(Dir, F) || F <-Filenames ],
+ Dirs = [suffix(filename:basename(F), ?suite_data) ||
+ F <- FullFilenames,
+ file_utils:file_type(F) =:= {ok, 'directory'}],
+ [S || {yes, S} <- Dirs]
+ end.
+
+suffix(String, Suffix) ->
+ Index = string:rstr(String, Suffix),
+ case string:substr(String, Index) =:= Suffix of
+ true -> {yes, string:sub_string(String,1,Index-1)};
+ false -> no
+ end.
+
+-spec create_suite(string()) -> 'ok'.
+
+create_suite(SuiteName) ->
+ {ok, Cwd} = file:get_cwd(),
+ SuiteDirN = generate_suite_dir_from_name(Cwd, SuiteName),
+ OutputFile = generate_suite_file(Cwd, SuiteName),
+ {OptionsFileN, InputDirN} = check_neccessary_files(SuiteDirN),
+ generate_suite(SuiteName, OutputFile, OptionsFileN, InputDirN).
+
+generate_suite_dir_from_name(Cwd, SuiteName) ->
+ filename:join(Cwd, SuiteName ++ ?suite_data).
+
+generate_suite_file(Cwd, SuiteName) ->
+ OutputFilename =
+ filename:join(Cwd, SuiteName ++ ?suite_suffix ++ ?erlang_extension),
+ case file:open(OutputFilename, [?output_file_mode]) of
+ {ok, IoDevice} -> IoDevice;
+ {error, _} = E -> exit({E, OutputFilename})
+ end.
+
+check_neccessary_files(SuiteDirN) ->
+ InputDirN = filename:join(SuiteDirN, ?input_files_directory),
+ check_file_exists(InputDirN, directory),
+ OptionsFileN = filename:join(SuiteDirN, ?dialyzer_option_file),
+ check_file_exists(OptionsFileN, regular),
+ {OptionsFileN, InputDirN}.
+
+check_file_exists(Filename, Type) ->
+ case file:read_file_info(Filename) of
+ {ok, FileInfo} ->
+ case FileInfo#file_info.type of
+ Type -> ok;
+ Else -> exit({error, {wrong_input_file_type, Else}})
+ end;
+ {error, _} = E -> exit({E, Filename, Type})
+ end.
+
+generate_suite(SuiteName, OutputFile, OptionsFileN, InputDirN) ->
+ Options = read_options(OptionsFileN),
+ TestCases = list_testcases(InputDirN),
+ Suite = #suite{suitename = SuiteName, outputfile = OutputFile,
+ options = Options, testcases = TestCases},
+ write_suite(Suite),
+ file:close(OutputFile).
+
+read_options(OptionsFileN) ->
+ case file:consult(OptionsFileN) of
+ {ok, Opts} -> read_options(Opts, #options{});
+ _ = E -> exit({error, {incorrect_options_file, E}})
+ end.
+
+read_options([List], Options) when is_list(List) ->
+ read_options(List, Options);
+read_options([], Options) ->
+ Options;
+read_options([{time_limit, TimeLimit}|Opts], Options) ->
+ read_options(Opts, Options#options{time_limit = TimeLimit});
+read_options([{dialyzer_options, DialyzerOptions}|Opts], Options) ->
+ read_options(Opts, Options#options{dialyzer_options = DialyzerOptions}).
+
+list_testcases(Dirname) ->
+ {ok, Files} = file_utils:list_dir(Dirname, ".erl", true),
+ [list_to_atom(filename:basename(F,".erl")) || F <-Files].
+
+write_suite(Suite) ->
+ write_header(Suite),
+ write_consistency(Suite),
+ write_testcases(Suite).
+
+write_header(#suite{suitename = SuiteName, outputfile = OutputFile,
+ options = Options, testcases = TestCases}) ->
+ Test_Plus_Consistency =
+ [list_to_atom(SuiteName ++ ?suite_suffix ++ "_consistency")|TestCases],
+ Exports = format_export(Test_Plus_Consistency),
+ TimeLimit = Options#options.time_limit,
+ DialyzerOptions = Options#options.dialyzer_options,
+ io:format(OutputFile,
+ "%% ATTENTION!\n"
+ "%% This is an automatically generated file. Do not edit.\n"
+ "%% Use './remake' script to refresh it if needed.\n"
+ "%% All Dialyzer options should be defined in dialyzer_options\n"
+ "%% file.\n\n"
+ "-module(~s).\n\n"
+ "-include_lib(\"common_test/include/ct.hrl\").\n"
+ "-include(\"dialyzer_test_constants.hrl\").\n\n"
+ "-export([suite/0, init_per_suite/0, init_per_suite/1,\n"
+ " end_per_suite/1, all/0]).\n"
+ "~s\n\n"
+ "suite() ->\n"
+ " [{timetrap, {minutes, ~w}}].\n\n"
+ "init_per_suite() ->\n"
+ " [{timetrap, ?plt_timeout}].\n"
+ "init_per_suite(Config) ->\n"
+ " OutDir = ?config(priv_dir, Config),\n"
+ " case dialyzer_common:check_plt(OutDir) of\n"
+ " fail -> {skip, \"Plt creation/check failed.\"};\n"
+ " ok -> [{dialyzer_options, ~p}|Config]\n"
+ " end.\n\n"
+ "end_per_suite(_Config) ->\n"
+ " ok.\n\n"
+ "all() ->\n"
+ " ~p.\n\n"
+ "dialyze(Config, TestCase) ->\n"
+ " Opts = ?config(dialyzer_options, Config),\n"
+ " Dir = ?config(data_dir, Config),\n"
+ " OutDir = ?config(priv_dir, Config),\n"
+ " dialyzer_common:check(TestCase, Opts, Dir, OutDir)."
+ "\n\n"
+ ,[SuiteName ++ ?suite_suffix, Exports, TimeLimit,
+ DialyzerOptions, Test_Plus_Consistency]).
+
+format_export(TestCases) ->
+ TestCasesArity =
+ [list_to_atom(atom_to_list(N)++"/1") || N <- TestCases],
+ TestCaseString = io_lib:format("-export(~p).", [TestCasesArity]),
+ strip_quotes(lists:flatten(TestCaseString),[]).
+
+strip_quotes([], Result) ->
+ lists:reverse(Result);
+strip_quotes([$' |Rest], Result) ->
+ strip_quotes(Rest, Result);
+strip_quotes([$\, |Rest], Result) ->
+ strip_quotes(Rest, [$\ , $\, |Result]);
+strip_quotes([C|Rest], Result) ->
+ strip_quotes(Rest, [C|Result]).
+
+write_consistency(#suite{suitename = SuiteName, outputfile = OutputFile}) ->
+ write_consistency(SuiteName, OutputFile).
+
+write_consistency(SuiteName, OutputFile) ->
+ io:format(OutputFile,
+ "~s_consistency(Config) ->\n"
+ " Dir = ?config(data_dir, Config),\n"
+ " case dialyzer_common:new_tests(Dir, all()) of\n"
+ " [] -> ok;\n"
+ " New -> ct:fail({missing_tests,New})\n"
+ " end.\n\n",
+ [SuiteName ++ ?suite_suffix]).
+
+write_testcases(#suite{outputfile = OutputFile, testcases = TestCases}) ->
+ write_testcases(OutputFile, TestCases).
+
+write_testcases(OutputFile, [TestCase| Rest]) ->
+ io:format(OutputFile,
+ "~p(Config) ->\n"
+ " case dialyze(Config, ~p) of\n"
+ " 'same' -> 'same';\n"
+ " Error -> ct:fail(Error)\n"
+ " end.\n\n",
+ [TestCase, TestCase]),
+ write_testcases(OutputFile, Rest);
+write_testcases(_OutputFile, []) ->
+ ok.