%%% File : dialyzer_common.erl %%% Author : Stavros Aronis <aronisstav@gmail.com> %%% 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.