cover
diff --git ebin/rebar.app ebin/rebar.app | |
index 710f8b2..b683ffb 100644 | |
--- ebin/rebar.app | |
+++ ebin/rebar.app | |
@@ -46,7 +46,8 @@ | |
rebar_metacmds, | |
rebar_getopt, | |
rebar_mustache, | |
- rmemo ]}, | |
+ rmemo, | |
+ cover ]}, | |
{registered, []}, | |
{applications, | |
[ | |
diff --git src/cover.erl src/cover.erl | |
new file mode 100644 | |
index 0000000..366d6bc | |
--- /dev/null | |
+++ src/cover.erl | |
@@ -0,0 +1,2758 @@ | |
+%% | |
+%% %CopyrightBegin% | |
+%% | |
+%% Copyright Ericsson AB 2001-2015. All Rights Reserved. | |
+%% | |
+%% Licensed under the Apache License, Version 2.0 (the "License"); | |
+%% you may not use this file except in compliance with the License. | |
+%% You may obtain a copy of the License at | |
+%% | |
+%% http://www.apache.org/licenses/LICENSE-2.0 | |
+%% | |
+%% Unless required by applicable law or agreed to in writing, software | |
+%% distributed under the License is distributed on an "AS IS" BASIS, | |
+%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
+%% See the License for the specific language governing permissions and | |
+%% limitations under the License. | |
+%% | |
+%% %CopyrightEnd% | |
+%% | |
+-module(cover). | |
+ | |
+%% | |
+%% This module implements the Erlang coverage tool. The module named | |
+%% cover_web implements a user interface for the coverage tool to run | |
+%% under webtool. | |
+%% | |
+%% ARCHITECTURE | |
+%% The coverage tool consists of one process on each node involved in | |
+%% coverage analysis. The process is registered as 'cover_server' | |
+%% (?SERVER). The cover_server on the 'main' node is in charge, and | |
+%% it monitors the cover_servers on all remote nodes. When it gets a | |
+%% 'DOWN' message for another cover_server, it marks the node as | |
+%% 'lost'. If a nodeup is received for a lost node the main node | |
+%% ensures that the cover compiled modules are loaded again. If the | |
+%% remote node was alive during the disconnected periode, cover data | |
+%% for this periode will also be included in the analysis. | |
+%% | |
+%% The cover_server process on the main node is implemented by the | |
+%% functions init_main/1 and main_process_loop/1. The cover_server on | |
+%% the remote nodes are implemented by the functions init_remote/2 and | |
+%% remote_process_loop/1. | |
+%% | |
+%% TABLES | |
+%% Each nodes has two tables: cover_internal_data_table (?COVER_TABLE) and. | |
+%% cover_internal_clause_table (?COVER_CLAUSE_TABLE). | |
+%% ?COVER_TABLE contains the bump data i.e. the data about which lines | |
+%% have been executed how many times. | |
+%% ?COVER_CLAUSE_TABLE contains information about which clauses in which modules | |
+%% cover is currently collecting statistics. | |
+%% | |
+%% The main node owns tables named | |
+%% 'cover_collected_remote_data_table' (?COLLECTION_TABLE) and | |
+%% 'cover_collected_remote_clause_table' (?COLLECTION_CLAUSE_TABLE). | |
+%% These tables contain data which is collected from remote nodes (either when a | |
+%% remote node is stopped with cover:stop/1 or when analysing). When | |
+%% analysing, data is even moved from the COVER tables on the main | |
+%% node to the COLLECTION tables. | |
+%% | |
+%% The main node also has a table named 'cover_binary_code_table' | |
+%% (?BINARY_TABLE). This table contains the binary code for each cover | |
+%% compiled module. This is necessary so that the code can be loaded | |
+%% on remote nodes that are started after the compilation. | |
+%% | |
+%% PARALLELISM | |
+%% To take advantage of SMP when doing the cover analysis both the data | |
+%% collection and analysis has been parallelized. One process is spawned for | |
+%% each node when collecting data, and on the remote node when collecting data | |
+%% one process is spawned per module. | |
+%% | |
+%% When analyzing data it is possible to issue multiple analyse(_to_file)/X | |
+%% calls at once. They are however all calls (for backwards compatibility | |
+%% reasons) so the user of cover will have to spawn several processes to to the | |
+%% calls ( or use async_analyse_to_file ). | |
+%% | |
+ | |
+%% External exports | |
+-export([start/0, start/1, | |
+ compile/1, compile/2, compile_module/1, compile_module/2, | |
+ compile_directory/0, compile_directory/1, compile_directory/2, | |
+ compile_beam/1, compile_beam_directory/0, compile_beam_directory/1, | |
+ analyse/0, analyse/1, analyse/2, analyse/3, | |
+ analyze/0, analyze/1, analyze/2, analyze/3, | |
+ analyse_to_file/0, | |
+ analyse_to_file/1, analyse_to_file/2, analyse_to_file/3, | |
+ analyze_to_file/0, | |
+ analyze_to_file/1, analyze_to_file/2, analyze_to_file/3, | |
+ async_analyse_to_file/1,async_analyse_to_file/2, | |
+ async_analyse_to_file/3, async_analyze_to_file/1, | |
+ async_analyze_to_file/2, async_analyze_to_file/3, | |
+ export/1, export/2, import/1, | |
+ modules/0, imported/0, imported_modules/0, which_nodes/0, is_compiled/1, | |
+ reset/1, reset/0, | |
+ flush/1, | |
+ stop/0, stop/1]). | |
+-export([remote_start/1,get_main_node/0]). | |
+ | |
+%% Used internally to ensure we upgrade the code to the latest version. | |
+-export([main_process_loop/1,remote_process_loop/1]). | |
+ | |
+-record(main_state, {compiled=[], % [{Module,File}] | |
+ imported=[], % [{Module,File,ImportFile}] | |
+ stopper, % undefined | pid() | |
+ nodes=[], % [Node] | |
+ lost_nodes=[]}). % [Node] | |
+ | |
+-record(remote_state, {compiled=[], % [{Module,File}] | |
+ main_node}). % atom() | |
+ | |
+-record(bump, {module = '_', % atom() | |
+ function = '_', % atom() | |
+ arity = '_', % integer() | |
+ clause = '_', % integer() | |
+ line = '_' % integer() | |
+ }). | |
+-define(BUMP_REC_NAME,bump). | |
+-define(CHUNK_SIZE, 20000). | |
+ | |
+-record(vars, {module, % atom() Module name | |
+ | |
+ init_info=[], % [{M,F,A,C,L}] | |
+ | |
+ function, % atom() | |
+ arity, % int() | |
+ clause, % int() | |
+ lines, % [int()] | |
+ no_bump_lines, % [int()] | |
+ depth, % int() | |
+ is_guard=false % boolean | |
+ }). | |
+ | |
+-define(COVER_TABLE, 'cover_internal_data_table'). | |
+-define(COVER_CLAUSE_TABLE, 'cover_internal_clause_table'). | |
+-define(BINARY_TABLE, 'cover_binary_code_table'). | |
+-define(COLLECTION_TABLE, 'cover_collected_remote_data_table'). | |
+-define(COLLECTION_CLAUSE_TABLE, 'cover_collected_remote_clause_table'). | |
+-define(TAG, cover_compiled). | |
+-define(SERVER, cover_server). | |
+ | |
+%% Line doesn't matter. | |
+-define(BLOCK(Expr), {block,erl_anno:new(0),[Expr]}). | |
+-define(BLOCK1(Expr), | |
+ if | |
+ element(1, Expr) =:= block -> | |
+ Expr; | |
+ true -> ?BLOCK(Expr) | |
+ end). | |
+ | |
+-define(SPAWN_DBG(Tag,Value),put(Tag,Value)). | |
+ | |
+-include_lib("stdlib/include/ms_transform.hrl"). | |
+ | |
+%%%---------------------------------------------------------------------- | |
+%%% External exports | |
+%%%---------------------------------------------------------------------- | |
+ | |
+%% start() -> {ok,Pid} | {error,Reason} | |
+%% Pid = pid() | |
+%% Reason = {already_started,Pid} | term() | |
+start() -> | |
+ case whereis(?SERVER) of | |
+ undefined -> | |
+ Starter = self(), | |
+ Pid = spawn(fun() -> | |
+ ?SPAWN_DBG(start,[]), | |
+ init_main(Starter) | |
+ end), | |
+ Ref = erlang:monitor(process,Pid), | |
+ Return = | |
+ receive | |
+ {?SERVER,started} -> | |
+ {ok,Pid}; | |
+ {'DOWN', Ref, _Type, _Object, Info} -> | |
+ {error,Info} | |
+ end, | |
+ erlang:demonitor(Ref), | |
+ Return; | |
+ Pid -> | |
+ {error,{already_started,Pid}} | |
+ end. | |
+ | |
+%% start(Nodes) -> {ok,StartedNodes} | |
+%% Nodes = Node | [Node,...] | |
+%% Node = atom() | |
+start(Node) when is_atom(Node) -> | |
+ start([Node]); | |
+start(Nodes) -> | |
+ call({start_nodes,remove_myself(Nodes,[])}). | |
+ | |
+%% compile(ModFiles) -> | |
+%% compile(ModFiles, Options) -> | |
+%% compile_module(ModFiles) -> Result | |
+%% compile_module(ModFiles, Options) -> Result | |
+%% ModFiles = ModFile | [ModFile] | |
+%% ModFile = Module | File | |
+%% Module = atom() | |
+%% File = string() | |
+%% Options = [Option] | |
+%% Option = {i,Dir} | {d,Macro} | {d,Macro,Value} | |
+%% Result = {ok,Module} | {error,File} | |
+compile(ModFile) -> | |
+ compile_module(ModFile, []). | |
+compile(ModFile, Options) -> | |
+ compile_module(ModFile, Options). | |
+compile_module(ModFile) when is_atom(ModFile); | |
+ is_list(ModFile) -> | |
+ compile_module(ModFile, []). | |
+compile_module(ModFile, Options) when is_atom(ModFile); | |
+ is_list(ModFile), is_integer(hd(ModFile)) -> | |
+ [R] = compile_module([ModFile], Options), | |
+ R; | |
+compile_module(ModFiles, Options) when is_list(Options) -> | |
+ AbsFiles = | |
+ [begin | |
+ File = | |
+ case ModFile of | |
+ _ when is_atom(ModFile) -> atom_to_list(ModFile); | |
+ _ when is_list(ModFile) -> ModFile | |
+ end, | |
+ WithExt = case filename:extension(File) of | |
+ ".erl" -> | |
+ File; | |
+ _ -> | |
+ File++".erl" | |
+ end, | |
+ filename:absname(WithExt) | |
+ end || ModFile <- ModFiles], | |
+ compile_modules(AbsFiles, Options). | |
+ | |
+%% compile_directory() -> | |
+%% compile_directory(Dir) -> | |
+%% compile_directory(Dir, Options) -> [Result] | {error,Reason} | |
+%% Dir = string() | |
+%% Options - see compile/1 | |
+%% Result - see compile/1 | |
+%% Reason = eacces | enoent | |
+compile_directory() -> | |
+ case file:get_cwd() of | |
+ {ok, Dir} -> | |
+ compile_directory(Dir, []); | |
+ Error -> | |
+ Error | |
+ end. | |
+compile_directory(Dir) when is_list(Dir) -> | |
+ compile_directory(Dir, []). | |
+compile_directory(Dir, Options) when is_list(Dir), is_list(Options) -> | |
+ case file:list_dir(Dir) of | |
+ {ok, Files} -> | |
+ ErlFiles = [filename:join(Dir, File) || | |
+ File <- Files, | |
+ filename:extension(File) =:= ".erl"], | |
+ compile_modules(ErlFiles, Options); | |
+ Error -> | |
+ Error | |
+ end. | |
+ | |
+compile_modules(Files,Options) -> | |
+ Options2 = filter_options(Options), | |
+ %% compile_modules(Files,Options2,[]). | |
+ call({compile, Files, Options2}). | |
+ | |
+%% compile_modules([File|Files], Options, Result) -> | |
+%% R = call({compile, File, Options}), | |
+%% compile_modules(Files,Options,[R|Result]); | |
+%% compile_modules([],_Opts,Result) -> | |
+%% lists:reverse(Result). | |
+ | |
+filter_options(Options) -> | |
+ lists:filter(fun(Option) -> | |
+ case Option of | |
+ {i, Dir} when is_list(Dir) -> true; | |
+ {d, _Macro} -> true; | |
+ {d, _Macro, _Value} -> true; | |
+ export_all -> true; | |
+ _ -> false | |
+ end | |
+ end, | |
+ Options). | |
+ | |
+%% compile_beam(ModFile) -> Result | {error,Reason} | |
+%% ModFile - see compile/1 | |
+%% Result - see compile/1 | |
+%% Reason = non_existing | already_cover_compiled | |
+compile_beam(ModFile0) when is_atom(ModFile0); | |
+ is_list(ModFile0), is_integer(hd(ModFile0)) -> | |
+ case compile_beams([ModFile0]) of | |
+ [{error,{non_existing,_}}] -> | |
+ %% Backwards compatibility | |
+ {error,non_existing}; | |
+ [Result] -> | |
+ Result | |
+ end; | |
+compile_beam(ModFiles) when is_list(ModFiles) -> | |
+ compile_beams(ModFiles). | |
+ | |
+ | |
+%% compile_beam_directory(Dir) -> [Result] | {error,Reason} | |
+%% Dir - see compile_directory/1 | |
+%% Result - see compile/1 | |
+%% Reason = eacces | enoent | |
+compile_beam_directory() -> | |
+ case file:get_cwd() of | |
+ {ok, Dir} -> | |
+ compile_beam_directory(Dir); | |
+ Error -> | |
+ Error | |
+ end. | |
+compile_beam_directory(Dir) when is_list(Dir) -> | |
+ case file:list_dir(Dir) of | |
+ {ok, Files} -> | |
+ BeamFiles = [filename:join(Dir, File) || | |
+ File <- Files, | |
+ filename:extension(File) =:= ".beam"], | |
+ compile_beams(BeamFiles); | |
+ Error -> | |
+ Error | |
+ end. | |
+ | |
+compile_beams(ModFiles0) -> | |
+ ModFiles = get_mods_and_beams(ModFiles0,[]), | |
+ call({compile_beams,ModFiles}). | |
+ | |
+get_mods_and_beams([Module|ModFiles],Acc) when is_atom(Module) -> | |
+ case code:which(Module) of | |
+ non_existing -> | |
+ get_mods_and_beams(ModFiles,[{error,{non_existing,Module}}|Acc]); | |
+ File -> | |
+ get_mods_and_beams([{Module,File}|ModFiles],Acc) | |
+ end; | |
+get_mods_and_beams([File|ModFiles],Acc) when is_list(File) -> | |
+ {WithExt,WithoutExt} | |
+ = case filename:rootname(File,".beam") of | |
+ File -> | |
+ {File++".beam",File}; | |
+ Rootname -> | |
+ {File,Rootname} | |
+ end, | |
+ AbsFile = filename:absname(WithExt), | |
+ Module = list_to_atom(filename:basename(WithoutExt)), | |
+ get_mods_and_beams([{Module,AbsFile}|ModFiles],Acc); | |
+get_mods_and_beams([{Module,File}|ModFiles],Acc) -> | |
+ %% Check for duplicates | |
+ case lists:keyfind(Module,2,Acc) of | |
+ {ok,Module,File} -> | |
+ %% Duplicate, but same file so ignore | |
+ get_mods_and_beams(ModFiles,Acc); | |
+ {ok,Module,_OtherFile} -> | |
+ %% Duplicate and differnet file - error | |
+ get_mods_and_beams(ModFiles,[{error,{duplicate,Module}}|Acc]); | |
+ _ -> | |
+ get_mods_and_beams(ModFiles,[{ok,Module,File}|Acc]) | |
+ end; | |
+get_mods_and_beams([],Acc) -> | |
+ lists:reverse(Acc). | |
+ | |
+ | |
+%% analyse(Modules) -> | |
+%% analyse(Analysis) -> | |
+%% analyse(Level) -> | |
+%% analyse(Modules, Analysis) -> | |
+%% analyse(Modules, Level) -> | |
+%% analyse(Analysis, Level) | |
+%% analyse(Modules, Analysis, Level) -> {ok,Answer} | {error,Error} | |
+%% Modules = Module | [Module] | |
+%% Module = atom() | |
+%% Analysis = coverage | calls | |
+%% Level = line | clause | function | module | |
+%% Answer = {Module,Value} | [{Item,Value}] | |
+%% Item = Line | Clause | Function | |
+%% Line = {M,N} | |
+%% Clause = {M,F,A,C} | |
+%% Function = {M,F,A} | |
+%% M = F = atom() | |
+%% N = A = C = integer() | |
+%% Value = {Cov,NotCov} | Calls | |
+%% Cov = NotCov = Calls = integer() | |
+%% Error = {not_cover_compiled,Module} | not_main_node | |
+-define(is_analysis(__A__), | |
+ (__A__=:=coverage orelse __A__=:=calls)). | |
+-define(is_level(__L__), | |
+ (__L__=:=line orelse __L__=:=clause orelse | |
+ __L__=:=function orelse __L__=:=module)). | |
+analyse() -> | |
+ analyse('_'). | |
+ | |
+analyse(Analysis) when ?is_analysis(Analysis) -> | |
+ analyse('_', Analysis); | |
+analyse(Level) when ?is_level(Level) -> | |
+ analyse('_', Level); | |
+analyse(Module) -> | |
+ analyse(Module, coverage). | |
+ | |
+analyse(Analysis, Level) when ?is_analysis(Analysis) andalso | |
+ ?is_level(Level) -> | |
+ analyse('_', Analysis, Level); | |
+analyse(Module, Analysis) when ?is_analysis(Analysis) -> | |
+ analyse(Module, Analysis, function); | |
+analyse(Module, Level) when ?is_level(Level) -> | |
+ analyse(Module, coverage, Level). | |
+ | |
+analyse(Module, Analysis, Level) when ?is_analysis(Analysis), | |
+ ?is_level(Level) -> | |
+ call({{analyse, Analysis, Level}, Module}). | |
+ | |
+analyze() -> analyse( ). | |
+analyze(Module) -> analyse(Module). | |
+analyze(Module, Analysis) -> analyse(Module, Analysis). | |
+analyze(Module, Analysis, Level) -> analyse(Module, Analysis, Level). | |
+ | |
+%% analyse_to_file() -> | |
+%% analyse_to_file(Modules) -> | |
+%% analyse_to_file(Modules, Options) -> | |
+%% Modules = Module | [Module] | |
+%% Module = atom() | |
+%% OutFile = string() | |
+%% Options = [Option] | |
+%% Option = html | {outfile,filename()} | {outdir,dirname()} | |
+%% Error = {not_cover_compiled,Module} | no_source_code_found | | |
+%% {file,File,Reason} | |
+%% File = string() | |
+%% Reason = term() | |
+%% | |
+%% Kept for backwards compatibility: | |
+%% analyse_to_file(Modules, OutFile) -> | |
+%% analyse_to_file(Modules, OutFile, Options) -> {ok,OutFile} | {error,Error} | |
+analyse_to_file() -> | |
+ analyse_to_file('_'). | |
+analyse_to_file(Arg) -> | |
+ case is_options(Arg) of | |
+ true -> | |
+ analyse_to_file('_',Arg); | |
+ false -> | |
+ analyse_to_file(Arg,[]) | |
+ end. | |
+analyse_to_file(Module, OutFile) when is_list(OutFile), is_integer(hd(OutFile)) -> | |
+ %% Kept for backwards compatibility | |
+ analyse_to_file(Module, [{outfile,OutFile}]); | |
+analyse_to_file(Module, Options) when is_list(Options) -> | |
+ call({{analyse_to_file, Options}, Module}). | |
+analyse_to_file(Module, OutFile, Options) when is_list(OutFile) -> | |
+ %% Kept for backwards compatibility | |
+ analyse_to_file(Module,[{outfile,OutFile}|Options]). | |
+ | |
+analyze_to_file() -> analyse_to_file(). | |
+analyze_to_file(Module) -> analyse_to_file(Module). | |
+analyze_to_file(Module, OptOrOut) -> analyse_to_file(Module, OptOrOut). | |
+analyze_to_file(Module, OutFile, Options) -> | |
+ analyse_to_file(Module, OutFile, Options). | |
+ | |
+async_analyse_to_file(Module) -> | |
+ do_spawn(?MODULE, analyse_to_file, [Module]). | |
+async_analyse_to_file(Module, OutFileOrOpts) -> | |
+ do_spawn(?MODULE, analyse_to_file, [Module, OutFileOrOpts]). | |
+async_analyse_to_file(Module, OutFile, Options) -> | |
+ do_spawn(?MODULE, analyse_to_file, [Module, OutFile, Options]). | |
+ | |
+is_options([html]) -> | |
+ true; % this is not 100% safe - could be a module named html... | |
+is_options([html|Opts]) -> | |
+ is_options(Opts); | |
+is_options([{Opt,_}|_]) when Opt==outfile; Opt==outdir -> | |
+ true; | |
+is_options(_) -> | |
+ false. | |
+ | |
+do_spawn(M,F,A) -> | |
+ spawn_link(fun() -> | |
+ case apply(M,F,A) of | |
+ {ok, _} -> | |
+ ok; | |
+ {error, Reason} -> | |
+ exit(Reason) | |
+ end | |
+ end). | |
+ | |
+async_analyze_to_file(Module) -> | |
+ async_analyse_to_file(Module). | |
+async_analyze_to_file(Module, OutFileOrOpts) -> | |
+ async_analyse_to_file(Module, OutFileOrOpts). | |
+async_analyze_to_file(Module, OutFile, Options) -> | |
+ async_analyse_to_file(Module, OutFile, Options). | |
+ | |
+outfilename(undefined, Module, HTML) -> | |
+ outfilename(Module, HTML); | |
+outfilename(OutDir, Module, HTML) -> | |
+ filename:join(OutDir, outfilename(Module, HTML)). | |
+ | |
+outfilename(Module, true) -> | |
+ atom_to_list(Module)++".COVER.html"; | |
+outfilename(Module, false) -> | |
+ atom_to_list(Module)++".COVER.out". | |
+ | |
+ | |
+%% export(File) | |
+%% export(File,Module) -> ok | {error,Reason} | |
+%% File = string(); file to write the exported data to | |
+%% Module = atom() | |
+export(File) -> | |
+ export(File, '_'). | |
+export(File, Module) -> | |
+ call({export,File,Module}). | |
+ | |
+%% import(File) -> ok | {error, Reason} | |
+%% File = string(); file created with cover:export/1,2 | |
+import(File) -> | |
+ call({import,File}). | |
+ | |
+%% modules() -> [Module] | |
+%% Module = atom() | |
+modules() -> | |
+ call(modules). | |
+ | |
+%% imported_modules() -> [Module] | |
+%% Module = atom() | |
+imported_modules() -> | |
+ call(imported_modules). | |
+ | |
+%% imported() -> [ImportFile] | |
+%% ImportFile = string() | |
+imported() -> | |
+ call(imported). | |
+ | |
+%% which_nodes() -> [Node] | |
+%% Node = atom() | |
+which_nodes() -> | |
+ call(which_nodes). | |
+ | |
+%% is_compiled(Module) -> {file,File} | false | |
+%% Module = atom() | |
+%% File = string() | |
+is_compiled(Module) when is_atom(Module) -> | |
+ call({is_compiled, Module}). | |
+ | |
+%% reset(Module) -> ok | {error,Error} | |
+%% reset() -> ok | |
+%% Module = atom() | |
+%% Error = {not_cover_compiled,Module} | |
+reset(Module) when is_atom(Module) -> | |
+ call({reset, Module}). | |
+reset() -> | |
+ call(reset). | |
+ | |
+%% stop() -> ok | |
+stop() -> | |
+ call(stop). | |
+ | |
+stop(Node) when is_atom(Node) -> | |
+ stop([Node]); | |
+stop(Nodes) -> | |
+ call({stop,remove_myself(Nodes,[])}). | |
+ | |
+%% flush(Nodes) -> ok | {error,not_main_node} | |
+%% Nodes = [Node] | Node | |
+%% Node = atom() | |
+%% Error = {not_cover_compiled,Module} | |
+flush(Node) when is_atom(Node) -> | |
+ flush([Node]); | |
+flush(Nodes) -> | |
+ call({flush,remove_myself(Nodes,[])}). | |
+ | |
+%% Used by test_server only. Not documented. | |
+get_main_node() -> | |
+ call(get_main_node). | |
+ | |
+%% bump(Module, Function, Arity, Clause, Line) | |
+%% Module = Function = atom() | |
+%% Arity = Clause = Line = integer() | |
+%% This function is inserted into Cover compiled modules, once for each | |
+%% executable line. | |
+%bump(Module, Function, Arity, Clause, Line) -> | |
+% Key = #bump{module=Module, function=Function, arity=Arity, clause=Clause, | |
+% line=Line}, | |
+% ets:update_counter(?COVER_TABLE, Key, 1). | |
+ | |
+call(Request) -> | |
+ Ref = erlang:monitor(process,?SERVER), | |
+ receive {'DOWN', Ref, _Type, _Object, noproc} -> | |
+ erlang:demonitor(Ref), | |
+ start(), | |
+ call(Request) | |
+ after 0 -> | |
+ ?SERVER ! {self(),Request}, | |
+ Return = | |
+ receive | |
+ {'DOWN', Ref, _Type, _Object, Info} -> | |
+ exit(Info); | |
+ {?SERVER,Reply} -> | |
+ Reply | |
+ end, | |
+ erlang:demonitor(Ref, [flush]), | |
+ Return | |
+ end. | |
+ | |
+reply(From, Reply) -> | |
+ From ! {?SERVER,Reply}. | |
+is_from(From) -> | |
+ is_pid(From). | |
+ | |
+remote_call(Node,Request) -> | |
+ Ref = erlang:monitor(process,{?SERVER,Node}), | |
+ receive {'DOWN', Ref, _Type, _Object, noproc} -> | |
+ erlang:demonitor(Ref), | |
+ {error,node_dead} | |
+ after 0 -> | |
+ {?SERVER,Node} ! Request, | |
+ Return = | |
+ receive | |
+ {'DOWN', Ref, _Type, _Object, _Info} -> | |
+ case Request of | |
+ {remote,stop} -> ok; | |
+ _ -> {error,node_dead} | |
+ end; | |
+ {?SERVER,Reply} -> | |
+ Reply | |
+ end, | |
+ erlang:demonitor(Ref, [flush]), | |
+ Return | |
+ end. | |
+ | |
+remote_reply(Proc,Reply) when is_pid(Proc) -> | |
+ Proc ! {?SERVER,Reply}; | |
+remote_reply(MainNode,Reply) -> | |
+ {?SERVER,MainNode} ! {?SERVER,Reply}. | |
+ | |
+%%%---------------------------------------------------------------------- | |
+%%% cover_server on main node | |
+%%%---------------------------------------------------------------------- | |
+ | |
+init_main(Starter) -> | |
+ register(?SERVER,self()), | |
+ %% Having write concurrancy here gives a 40% performance boost | |
+ %% when collect/1 is called. | |
+ ets:new(?COVER_TABLE, [set, public, named_table | |
+ ,{write_concurrency, true} | |
+ ]), | |
+ ets:new(?COVER_CLAUSE_TABLE, [set, public, named_table]), | |
+ ets:new(?BINARY_TABLE, [set, public, named_table]), | |
+ ets:new(?COLLECTION_TABLE, [set, public, named_table]), | |
+ ets:new(?COLLECTION_CLAUSE_TABLE, [set, public, named_table]), | |
+ net_kernel:monitor_nodes(true), | |
+ Starter ! {?SERVER,started}, | |
+ main_process_loop(#main_state{}). | |
+ | |
+main_process_loop(State) -> | |
+ receive | |
+ {From, {start_nodes,Nodes}} -> | |
+ {StartedNodes,State1} = do_start_nodes(Nodes, State), | |
+ reply(From, {ok,StartedNodes}), | |
+ main_process_loop(State1); | |
+ | |
+ {From, {compile, Files, Options}} -> | |
+ {R,S} = do_compile(Files, Options, State), | |
+ reply(From,R), | |
+ %% This module (cover) could have been reloaded. Make | |
+ %% sure we run the new code. | |
+ ?MODULE:main_process_loop(S); | |
+ | |
+ {From, {compile_beams, ModsAndFiles}} -> | |
+ {R,S} = do_compile_beams(ModsAndFiles,State), | |
+ reply(From,R), | |
+ %% This module (cover) could have been reloaded. Make | |
+ %% sure we run the new code. | |
+ ?MODULE:main_process_loop(S); | |
+ | |
+ {From, {export,OutFile,Module}} -> | |
+ spawn(fun() -> | |
+ ?SPAWN_DBG(export,{OutFile, Module}), | |
+ do_export(Module, OutFile, From, State) | |
+ end), | |
+ main_process_loop(State); | |
+ | |
+ {From, {import,File}} -> | |
+ case file:open(File,[read,binary,raw]) of | |
+ {ok,Fd} -> | |
+ Imported = do_import_to_table(Fd,File, | |
+ State#main_state.imported), | |
+ reply(From, ok), | |
+ file:close(Fd), | |
+ main_process_loop(State#main_state{imported=Imported}); | |
+ {error,Reason} -> | |
+ reply(From, {error, {cant_open_file,File,Reason}}), | |
+ main_process_loop(State) | |
+ end; | |
+ | |
+ {From, modules} -> | |
+ %% Get all compiled modules which are still loaded | |
+ {LoadedModules,Compiled} = | |
+ get_compiled_still_loaded(State#main_state.nodes, | |
+ State#main_state.compiled), | |
+ | |
+ reply(From, LoadedModules), | |
+ main_process_loop(State#main_state{compiled=Compiled}); | |
+ | |
+ {From, imported_modules} -> | |
+ %% Get all modules with imported data | |
+ ImportedModules = lists:map(fun({Mod,_File,_ImportFile}) -> Mod end, | |
+ State#main_state.imported), | |
+ reply(From, ImportedModules), | |
+ main_process_loop(State); | |
+ | |
+ {From, imported} -> | |
+ %% List all imported files | |
+ reply(From, get_all_importfiles(State#main_state.imported,[])), | |
+ main_process_loop(State); | |
+ | |
+ {From, which_nodes} -> | |
+ %% List all imported files | |
+ reply(From, State#main_state.nodes), | |
+ main_process_loop(State); | |
+ | |
+ {From, reset} -> | |
+ lists:foreach( | |
+ fun({Module,_File}) -> | |
+ do_reset_main_node(Module,State#main_state.nodes) | |
+ end, | |
+ State#main_state.compiled), | |
+ reply(From, ok), | |
+ main_process_loop(State#main_state{imported=[]}); | |
+ | |
+ {From, {stop,Nodes}} -> | |
+ remote_collect('_',Nodes,true), | |
+ reply(From, ok), | |
+ Nodes1 = State#main_state.nodes--Nodes, | |
+ LostNodes1 = State#main_state.lost_nodes--Nodes, | |
+ main_process_loop(State#main_state{nodes=Nodes1, | |
+ lost_nodes=LostNodes1}); | |
+ | |
+ {From, {flush,Nodes}} -> | |
+ remote_collect('_',Nodes,false), | |
+ reply(From, ok), | |
+ main_process_loop(State); | |
+ | |
+ {From, stop} -> | |
+ lists:foreach( | |
+ fun(Node) -> | |
+ remote_call(Node,{remote,stop}) | |
+ end, | |
+ State#main_state.nodes), | |
+ reload_originals(State#main_state.compiled), | |
+ ets:delete(?COVER_TABLE), | |
+ ets:delete(?COVER_CLAUSE_TABLE), | |
+ ets:delete(?BINARY_TABLE), | |
+ ets:delete(?COLLECTION_TABLE), | |
+ ets:delete(?COLLECTION_CLAUSE_TABLE), | |
+ unregister(?SERVER), | |
+ reply(From, ok); | |
+ | |
+ {From, {{analyse, Analysis, Level}, '_'}} -> | |
+ R = analyse_all(Analysis, Level, State), | |
+ reply(From, R), | |
+ main_process_loop(State); | |
+ | |
+ {From, {{analyse, Analysis, Level}, Modules}} when is_list(Modules) -> | |
+ R = analyse_list(Modules, Analysis, Level, State), | |
+ reply(From, R), | |
+ main_process_loop(State); | |
+ | |
+ {From, {{analyse, Analysis, Level}, Module}} -> | |
+ S = try | |
+ Loaded = is_loaded(Module, State), | |
+ spawn(fun() -> | |
+ ?SPAWN_DBG(analyse,{Module,Analysis, Level}), | |
+ do_parallel_analysis( | |
+ Module, Analysis, Level, | |
+ Loaded, From, State) | |
+ end), | |
+ State | |
+ catch throw:Reason -> | |
+ reply(From,{error, {not_cover_compiled,Module}}), | |
+ not_loaded(Module, Reason, State) | |
+ end, | |
+ main_process_loop(S); | |
+ | |
+ {From, {{analyse_to_file, Opts},'_'}} -> | |
+ R = analyse_all_to_file(Opts, State), | |
+ reply(From,R), | |
+ main_process_loop(State); | |
+ | |
+ {From, {{analyse_to_file, Opts},Modules}} when is_list(Modules) -> | |
+ R = analyse_list_to_file(Modules, Opts, State), | |
+ reply(From,R), | |
+ main_process_loop(State); | |
+ | |
+ {From, {{analyse_to_file, Opts},Module}} -> | |
+ S = try | |
+ Loaded = is_loaded(Module, State), | |
+ spawn_link(fun() -> | |
+ ?SPAWN_DBG(analyse_to_file,{Module,Opts}), | |
+ do_parallel_analysis_to_file( | |
+ Module, Opts, Loaded, From, State) | |
+ end), | |
+ State | |
+ catch throw:Reason -> | |
+ reply(From,{error, {not_cover_compiled,Module}}), | |
+ not_loaded(Module, Reason, State) | |
+ end, | |
+ main_process_loop(S); | |
+ | |
+ {From, {is_compiled, Module}} -> | |
+ S = try is_loaded(Module, State) of | |
+ {loaded, File} -> | |
+ reply(From,{file, File}), | |
+ State; | |
+ {imported,_File,_ImportFiles} -> | |
+ reply(From,false), | |
+ State | |
+ catch throw:Reason -> | |
+ reply(From,false), | |
+ not_loaded(Module, Reason, State) | |
+ end, | |
+ main_process_loop(S); | |
+ | |
+ {From, {reset, Module}} -> | |
+ S = try | |
+ Loaded = is_loaded(Module,State), | |
+ R = case Loaded of | |
+ {loaded, _File} -> | |
+ do_reset_main_node( | |
+ Module, State#main_state.nodes); | |
+ {imported, _File, _} -> | |
+ do_reset_collection_table(Module) | |
+ end, | |
+ Imported = | |
+ remove_imported(Module, | |
+ State#main_state.imported), | |
+ reply(From, R), | |
+ State#main_state{imported=Imported} | |
+ catch throw:Reason -> | |
+ reply(From,{error, {not_cover_compiled,Module}}), | |
+ not_loaded(Module, Reason, State) | |
+ end, | |
+ main_process_loop(S); | |
+ | |
+ {'DOWN', _MRef, process, {?SERVER,Node}, _Info} -> | |
+ %% A remote cover_server is down, mark as lost | |
+ {Nodes,Lost} = | |
+ case lists:member(Node,State#main_state.nodes) of | |
+ true -> | |
+ N = State#main_state.nodes--[Node], | |
+ L = [Node|State#main_state.lost_nodes], | |
+ {N,L}; | |
+ false -> % node stopped | |
+ {State#main_state.nodes,State#main_state.lost_nodes} | |
+ end, | |
+ main_process_loop(State#main_state{nodes=Nodes,lost_nodes=Lost}); | |
+ | |
+ {nodeup,Node} -> | |
+ State1 = | |
+ case lists:member(Node,State#main_state.lost_nodes) of | |
+ true -> | |
+ sync_compiled(Node,State); | |
+ false -> | |
+ State | |
+ end, | |
+ main_process_loop(State1); | |
+ | |
+ {nodedown,_} -> | |
+ %% Will be taken care of when 'DOWN' message arrives | |
+ main_process_loop(State); | |
+ | |
+ {From, get_main_node} -> | |
+ reply(From, node()), | |
+ main_process_loop(State); | |
+ | |
+ get_status -> | |
+ io:format("~tp~n",[State]), | |
+ main_process_loop(State) | |
+ end. | |
+ | |
+%%%---------------------------------------------------------------------- | |
+%%% cover_server on remote node | |
+%%%---------------------------------------------------------------------- | |
+ | |
+init_remote(Starter,MainNode) -> | |
+ register(?SERVER,self()), | |
+ ets:new(?COVER_TABLE, [set, public, named_table | |
+ %% write_concurrency here makes otp_8270 break :( | |
+ %,{write_concurrency, true} | |
+ ]), | |
+ ets:new(?COVER_CLAUSE_TABLE, [set, public, named_table]), | |
+ Starter ! {self(),started}, | |
+ remote_process_loop(#remote_state{main_node=MainNode}). | |
+ | |
+ | |
+ | |
+remote_process_loop(State) -> | |
+ receive | |
+ {remote,load_compiled,Compiled} -> | |
+ Compiled1 = load_compiled(Compiled,State#remote_state.compiled), | |
+ remote_reply(State#remote_state.main_node, ok), | |
+ ?MODULE:remote_process_loop(State#remote_state{compiled=Compiled1}); | |
+ | |
+ {remote,unload,UnloadedModules} -> | |
+ unload(UnloadedModules), | |
+ Compiled = | |
+ update_compiled(UnloadedModules, State#remote_state.compiled), | |
+ remote_reply(State#remote_state.main_node, ok), | |
+ remote_process_loop(State#remote_state{compiled=Compiled}); | |
+ | |
+ {remote,reset,Module} -> | |
+ do_reset(Module), | |
+ remote_reply(State#remote_state.main_node, ok), | |
+ remote_process_loop(State); | |
+ | |
+ {remote,collect,Module,CollectorPid} -> | |
+ self() ! {remote,collect,Module,CollectorPid, ?SERVER}; | |
+ | |
+ {remote,collect,Modules0,CollectorPid,From} -> | |
+ Modules = case Modules0 of | |
+ '_' -> [M || {M,_} <- State#remote_state.compiled]; | |
+ _ -> Modules0 | |
+ end, | |
+ spawn(fun() -> | |
+ ?SPAWN_DBG(remote_collect, | |
+ {Modules, CollectorPid, From}), | |
+ do_collect(Modules, CollectorPid, From) | |
+ end), | |
+ remote_process_loop(State); | |
+ | |
+ {remote,stop} -> | |
+ reload_originals(State#remote_state.compiled), | |
+ ets:delete(?COVER_TABLE), | |
+ ets:delete(?COVER_CLAUSE_TABLE), | |
+ unregister(?SERVER), | |
+ ok; % not replying since 'DOWN' message will be received anyway | |
+ | |
+ {remote,get_compiled} -> | |
+ remote_reply(State#remote_state.main_node, | |
+ State#remote_state.compiled), | |
+ remote_process_loop(State); | |
+ | |
+ {From, get_main_node} -> | |
+ remote_reply(From, State#remote_state.main_node), | |
+ remote_process_loop(State); | |
+ | |
+ get_status -> | |
+ io:format("~tp~n",[State]), | |
+ remote_process_loop(State); | |
+ | |
+ M -> | |
+ io:format("WARNING: remote cover_server received\n~p\n",[M]), | |
+ case M of | |
+ {From,_} -> | |
+ case is_from(From) of | |
+ true -> | |
+ reply(From,{error,not_main_node}); | |
+ false -> | |
+ ok | |
+ end; | |
+ _ -> | |
+ ok | |
+ end, | |
+ remote_process_loop(State) | |
+ | |
+ end. | |
+ | |
+do_collect(Modules, CollectorPid, From) -> | |
+ pmap( | |
+ fun(Module) -> | |
+ Pattern = {#bump{module=Module, _='_'}, '$1'}, | |
+ MatchSpec = [{Pattern,[{'=/=','$1',0}],['$_']}], | |
+ Match = ets:select(?COVER_TABLE,MatchSpec,?CHUNK_SIZE), | |
+ send_chunks(Match, CollectorPid, []) | |
+ end,Modules), | |
+ CollectorPid ! done, | |
+ remote_reply(From, ok). | |
+ | |
+send_chunks('$end_of_table', _CollectorPid, Mons) -> | |
+ get_downs(Mons); | |
+send_chunks({Chunk,Continuation}, CollectorPid, Mons) -> | |
+ Mon = spawn_monitor( | |
+ fun() -> | |
+ lists:foreach(fun({Bump,_N}) -> | |
+ ets:insert(?COVER_TABLE, {Bump,0}) | |
+ end, | |
+ Chunk) end), | |
+ send_chunk(CollectorPid,Chunk), | |
+ send_chunks(ets:select(Continuation), CollectorPid, [Mon|Mons]). | |
+ | |
+send_chunk(CollectorPid,Chunk) -> | |
+ CollectorPid ! {chunk,Chunk,self()}, | |
+ receive continue -> ok end. | |
+ | |
+get_downs([]) -> | |
+ ok; | |
+get_downs(Mons) -> | |
+ receive | |
+ {'DOWN', Ref, _Type, Pid, _Reason} = Down -> | |
+ case lists:member({Pid,Ref},Mons) of | |
+ true -> | |
+ get_downs(lists:delete({Pid,Ref},Mons)); | |
+ false -> | |
+ %% This should be handled somewhere else | |
+ self() ! Down, | |
+ get_downs(Mons) | |
+ end | |
+ end. | |
+ | |
+reload_originals(Compiled) -> | |
+ Modules = [M || {M,_} <- Compiled], | |
+ pmap(fun do_reload_original/1, Modules). | |
+ | |
+do_reload_original(Module) -> | |
+ case code:which(Module) of | |
+ ?TAG -> | |
+ code:purge(Module), % remove code marked as 'old' | |
+ code:delete(Module), % mark cover compiled code as 'old' | |
+ %% Note: original beam code must be loaded before the cover | |
+ %% compiled code is purged, in order to for references to | |
+ %% 'fun M:F/A' and %% 'fun F/A' funs to be correct (they | |
+ %% refer to (M:)F/A in the *latest* version of the module) | |
+ code:load_file(Module), % load original code | |
+ code:purge(Module); % remove cover compiled code | |
+ _ -> | |
+ ignore | |
+ end. | |
+ | |
+load_compiled([{Module,File,Binary,InitialTable}|Compiled],Acc) -> | |
+ %% Make sure the #bump{} records are available *before* the | |
+ %% module is loaded. | |
+ insert_initial_data(InitialTable), | |
+ Sticky = case code:is_sticky(Module) of | |
+ true -> | |
+ code:unstick_mod(Module), | |
+ true; | |
+ false -> | |
+ false | |
+ end, | |
+ NewAcc = case code:load_binary(Module, ?TAG, Binary) of | |
+ {module,Module} -> | |
+ add_compiled(Module, File, Acc); | |
+ _ -> | |
+ do_clear(Module), | |
+ Acc | |
+ end, | |
+ case Sticky of | |
+ true -> code:stick_mod(Module); | |
+ false -> ok | |
+ end, | |
+ load_compiled(Compiled,NewAcc); | |
+load_compiled([],Acc) -> | |
+ Acc. | |
+ | |
+insert_initial_data([Item|Items]) when is_atom(element(1,Item)) -> | |
+ ets:insert(?COVER_CLAUSE_TABLE, Item), | |
+ insert_initial_data(Items); | |
+insert_initial_data([Item|Items]) -> | |
+ ets:insert(?COVER_TABLE, Item), | |
+ insert_initial_data(Items); | |
+insert_initial_data([]) -> | |
+ ok. | |
+ | |
+ | |
+unload([Module|Modules]) -> | |
+ do_clear(Module), | |
+ do_reload_original(Module), | |
+ unload(Modules); | |
+unload([]) -> | |
+ ok. | |
+ | |
+%%%---------------------------------------------------------------------- | |
+%%% Internal functions | |
+%%%---------------------------------------------------------------------- | |
+ | |
+%%%--Handling of remote nodes-------------------------------------------- | |
+ | |
+do_start_nodes(Nodes, State) -> | |
+ ThisNode = node(), | |
+ StartedNodes = | |
+ lists:foldl( | |
+ fun(Node,Acc) -> | |
+ case rpc:call(Node,cover,remote_start,[ThisNode]) of | |
+ {ok,_RPid} -> | |
+ erlang:monitor(process,{?SERVER,Node}), | |
+ [Node|Acc]; | |
+ Error -> | |
+ io:format("Could not start cover on ~w: ~tp\n", | |
+ [Node,Error]), | |
+ Acc | |
+ end | |
+ end, | |
+ [], | |
+ Nodes), | |
+ | |
+ %% In case some of the compiled modules have been unloaded they | |
+ %% should not be loaded on the new node. | |
+ {_LoadedModules,Compiled} = | |
+ get_compiled_still_loaded(State#main_state.nodes, | |
+ State#main_state.compiled), | |
+ remote_load_compiled(StartedNodes,Compiled), | |
+ | |
+ State1 = | |
+ State#main_state{nodes = State#main_state.nodes ++ StartedNodes, | |
+ compiled = Compiled}, | |
+ {StartedNodes, State1}. | |
+ | |
+%% start the cover_server on a remote node | |
+remote_start(MainNode) -> | |
+ case whereis(?SERVER) of | |
+ undefined -> | |
+ Starter = self(), | |
+ Pid = spawn(fun() -> | |
+ ?SPAWN_DBG(remote_start,{MainNode}), | |
+ init_remote(Starter,MainNode) | |
+ end), | |
+ Ref = erlang:monitor(process,Pid), | |
+ Return = | |
+ receive | |
+ {Pid,started} -> | |
+ {ok,Pid}; | |
+ {'DOWN', Ref, _Type, _Object, Info} -> | |
+ {error,Info} | |
+ end, | |
+ erlang:demonitor(Ref), | |
+ Return; | |
+ Pid -> | |
+ {error,{already_started,Pid}} | |
+ end. | |
+ | |
+%% If a lost node comes back, ensure that main and remote node has the | |
+%% same cover compiled modules. Note that no action is taken if the | |
+%% same {Mod,File} eksists on both, i.e. code change is not handled! | |
+sync_compiled(Node,State) -> | |
+ #main_state{compiled=Compiled0,nodes=Nodes,lost_nodes=Lost}=State, | |
+ State1 = | |
+ case remote_call(Node,{remote,get_compiled}) of | |
+ {error,node_dead} -> | |
+ {_,S} = do_start_nodes([Node],State), | |
+ S; | |
+ {error,_} -> | |
+ State; | |
+ RemoteCompiled -> | |
+ {_,Compiled} = get_compiled_still_loaded(Nodes,Compiled0), | |
+ Unload = [UM || {UM,_}=U <- RemoteCompiled, | |
+ false == lists:member(U,Compiled)], | |
+ remote_unload([Node],Unload), | |
+ Load = [L || L <- Compiled, | |
+ false == lists:member(L,RemoteCompiled)], | |
+ remote_load_compiled([Node],Load), | |
+ State#main_state{compiled=Compiled, nodes=[Node|Nodes]} | |
+ end, | |
+ State1#main_state{lost_nodes=Lost--[Node]}. | |
+ | |
+%% Load a set of cover compiled modules on remote nodes, | |
+%% We do it ?MAX_MODS modules at a time so that we don't | |
+%% run out of memory on the cover_server node. | |
+-define(MAX_MODS, 10). | |
+remote_load_compiled(Nodes,Compiled) -> | |
+ remote_load_compiled(Nodes, Compiled, [], 0). | |
+remote_load_compiled(_Nodes, [], [], _ModNum) -> | |
+ ok; | |
+remote_load_compiled(Nodes, Compiled, Acc, ModNum) | |
+ when Compiled == []; ModNum == ?MAX_MODS -> | |
+ RemoteLoadData = get_downs_r(Acc), | |
+ lists:foreach( | |
+ fun(Node) -> | |
+ remote_call(Node,{remote,load_compiled,RemoteLoadData}) | |
+ end, | |
+ Nodes), | |
+ remote_load_compiled(Nodes, Compiled, [], 0); | |
+remote_load_compiled(Nodes, [MF | Rest], Acc, ModNum) -> | |
+ remote_load_compiled( | |
+ Nodes, Rest, | |
+ [spawn_job_r(fun() -> get_data_for_remote_loading(MF) end) | Acc], | |
+ ModNum + 1). | |
+ | |
+spawn_job_r(Fun) -> | |
+ spawn_monitor(fun() -> exit(Fun()) end). | |
+ | |
+get_downs_r([]) -> | |
+ []; | |
+get_downs_r(Mons) -> | |
+ receive | |
+ {'DOWN', Ref, _Type, Pid, R={_,_,_,_}} -> | |
+ [R|get_downs_r(lists:delete({Pid,Ref},Mons))]; | |
+ {'DOWN', Ref, _Type, Pid, Reason} = Down -> | |
+ case lists:member({Pid,Ref},Mons) of | |
+ true -> | |
+ %% Something went really wrong - don't hang! | |
+ exit(Reason); | |
+ false -> | |
+ %% This should be handled somewhere else | |
+ self() ! Down, | |
+ get_downs_r(Mons) | |
+ end | |
+ end. | |
+ | |
+ | |
+%% Read all data needed for loading a cover compiled module on a remote node | |
+%% Binary is the beam code for the module and InitialTable is the initial | |
+%% data to insert in ?COVER_TABLE. | |
+get_data_for_remote_loading({Module,File}) -> | |
+ [{Module,Binary}] = ets:lookup(?BINARY_TABLE,Module), | |
+ %%! The InitialTable list will be long if the module is big - what to do?? | |
+ InitialBumps = ets:select(?COVER_TABLE,ms(Module)), | |
+ InitialClauses = ets:lookup(?COVER_CLAUSE_TABLE,Module), | |
+ | |
+ {Module,File,Binary,InitialBumps ++ InitialClauses}. | |
+ | |
+%% Create a match spec which returns the clause info {Module,InitInfo} and | |
+%% all #bump keys for the given module with 0 number of calls. | |
+ms(Module) -> | |
+ ets:fun2ms(fun({Key,_}) when Key#bump.module=:=Module -> | |
+ {Key,0} | |
+ end). | |
+ | |
+%% Unload modules on remote nodes | |
+remote_unload(Nodes,UnloadedModules) -> | |
+ lists:foreach( | |
+ fun(Node) -> | |
+ remote_call(Node,{remote,unload,UnloadedModules}) | |
+ end, | |
+ Nodes). | |
+ | |
+%% Reset one or all modules on remote nodes | |
+remote_reset(Module,Nodes) -> | |
+ lists:foreach( | |
+ fun(Node) -> | |
+ remote_call(Node,{remote,reset,Module}) | |
+ end, | |
+ Nodes). | |
+ | |
+%% Collect data from remote nodes - used for analyse or stop(Node) | |
+remote_collect(Modules,Nodes,Stop) -> | |
+ pmap(fun(Node) -> | |
+ ?SPAWN_DBG(remote_collect, | |
+ {Modules, Nodes, Stop}), | |
+ do_collection(Node, Modules, Stop) | |
+ end, | |
+ Nodes). | |
+ | |
+do_collection(Node, Module, Stop) -> | |
+ CollectorPid = spawn(fun collector_proc/0), | |
+ case remote_call(Node,{remote,collect,Module,CollectorPid, self()}) of | |
+ {error,node_dead} -> | |
+ CollectorPid ! done, | |
+ ok; | |
+ ok when Stop -> | |
+ remote_call(Node,{remote,stop}); | |
+ ok -> | |
+ ok | |
+ end. | |
+ | |
+%% Process which receives chunks of data from remote nodes - either when | |
+%% analysing or when stopping cover on the remote nodes. | |
+collector_proc() -> | |
+ ?SPAWN_DBG(collector_proc, []), | |
+ receive | |
+ {chunk,Chunk,From} -> | |
+ insert_in_collection_table(Chunk), | |
+ From ! continue, | |
+ collector_proc(); | |
+ done -> | |
+ ok | |
+ end. | |
+ | |
+insert_in_collection_table([{Key,Val}|Chunk]) -> | |
+ insert_in_collection_table(Key,Val), | |
+ insert_in_collection_table(Chunk); | |
+insert_in_collection_table([]) -> | |
+ ok. | |
+ | |
+insert_in_collection_table(Key,Val) -> | |
+ case ets:member(?COLLECTION_TABLE,Key) of | |
+ true -> | |
+ ets:update_counter(?COLLECTION_TABLE, | |
+ Key,Val); | |
+ false -> | |
+ %% Make sure that there are no race conditions from ets:member | |
+ case ets:insert_new(?COLLECTION_TABLE,{Key,Val}) of | |
+ false -> | |
+ insert_in_collection_table(Key,Val); | |
+ _ -> | |
+ ok | |
+ end | |
+ end. | |
+ | |
+ | |
+remove_myself([Node|Nodes],Acc) when Node=:=node() -> | |
+ remove_myself(Nodes,Acc); | |
+remove_myself([Node|Nodes],Acc) -> | |
+ remove_myself(Nodes,[Node|Acc]); | |
+remove_myself([],Acc) -> | |
+ Acc. | |
+ | |
+%%%--Handling of modules state data-------------------------------------- | |
+ | |
+analyse_info(_Module,[]) -> | |
+ ok; | |
+analyse_info(Module,Imported) -> | |
+ imported_info("Analysis",Module,Imported). | |
+ | |
+export_info(_Module,[]) -> | |
+ ok; | |
+export_info(_Module,_Imported) -> | |
+ %% Do not print that the export includes imported modules | |
+ ok. | |
+ | |
+export_info([]) -> | |
+ ok; | |
+export_info(_Imported) -> | |
+ %% Do not print that the export includes imported modules | |
+ ok. | |
+ | |
+get_all_importfiles([{_M,_F,ImportFiles}|Imported],Acc) -> | |
+ NewAcc = do_get_all_importfiles(ImportFiles,Acc), | |
+ get_all_importfiles(Imported,NewAcc); | |
+get_all_importfiles([],Acc) -> | |
+ Acc. | |
+ | |
+do_get_all_importfiles([ImportFile|ImportFiles],Acc) -> | |
+ case lists:member(ImportFile,Acc) of | |
+ true -> | |
+ do_get_all_importfiles(ImportFiles,Acc); | |
+ false -> | |
+ do_get_all_importfiles(ImportFiles,[ImportFile|Acc]) | |
+ end; | |
+do_get_all_importfiles([],Acc) -> | |
+ Acc. | |
+ | |
+imported_info(Text,Module,Imported) -> | |
+ case lists:keysearch(Module,1,Imported) of | |
+ {value,{Module,_File,ImportFiles}} -> | |
+ io:format("~ts includes data from imported files\n~tp\n", | |
+ [Text,ImportFiles]); | |
+ false -> | |
+ ok | |
+ end. | |
+ | |
+ | |
+ | |
+add_imported(Module, File, ImportFile, Imported) -> | |
+ add_imported(Module, File, filename:absname(ImportFile), Imported, []). | |
+ | |
+add_imported(M, F1, ImportFile, [{M,_F2,ImportFiles}|Imported], Acc) -> | |
+ case lists:member(ImportFile,ImportFiles) of | |
+ true -> | |
+ io:fwrite("WARNING: Module ~w already imported from ~tp~n" | |
+ "Not importing again!~n",[M,ImportFile]), | |
+ dont_import; | |
+ false -> | |
+ NewEntry = {M, F1, [ImportFile | ImportFiles]}, | |
+ {ok, lists:reverse([NewEntry | Acc]) ++ Imported} | |
+ end; | |
+add_imported(M, F, ImportFile, [H|Imported], Acc) -> | |
+ add_imported(M, F, ImportFile, Imported, [H|Acc]); | |
+add_imported(M, F, ImportFile, [], Acc) -> | |
+ {ok, lists:reverse([{M, F, [ImportFile]} | Acc])}. | |
+ | |
+%% Removes a module from the list of imported modules and writes a warning | |
+%% This is done when a module is compiled. | |
+remove_imported(Module,Imported) -> | |
+ case lists:keysearch(Module,1,Imported) of | |
+ {value,{Module,_,ImportFiles}} -> | |
+ io:fwrite("WARNING: Deleting data for module ~w imported from~n" | |
+ "~tp~n",[Module,ImportFiles]), | |
+ lists:keydelete(Module,1,Imported); | |
+ false -> | |
+ Imported | |
+ end. | |
+ | |
+%% Adds information to the list of compiled modules, preserving time order | |
+%% and without adding duplicate entries. | |
+add_compiled(Module, File1, [{Module,_File2}|Compiled]) -> | |
+ [{Module,File1}|Compiled]; | |
+add_compiled(Module, File, [H|Compiled]) -> | |
+ [H|add_compiled(Module, File, Compiled)]; | |
+add_compiled(Module, File, []) -> | |
+ [{Module,File}]. | |
+ | |
+are_loaded([Module|Modules], State, Loaded, Imported, Error) -> | |
+ try is_loaded(Module,State) of | |
+ {loaded,File} -> | |
+ are_loaded(Modules, State, [{Module,File}|Loaded], Imported, Error); | |
+ {imported,File,_} -> | |
+ are_loaded(Modules, State, Loaded, [{Module,File}|Imported], Error) | |
+ catch throw:_ -> | |
+ are_loaded(Modules, State, Loaded, Imported, | |
+ [{not_cover_compiled,Module}|Error]) | |
+ end; | |
+are_loaded([], _State, Loaded, Imported, Error) -> | |
+ {Loaded, Imported, Error}. | |
+ | |
+is_loaded(Module, State) -> | |
+ case get_file(Module, State#main_state.compiled) of | |
+ {ok, File} -> | |
+ case code:which(Module) of | |
+ ?TAG -> {loaded, File}; | |
+ _ -> throw(unloaded) | |
+ end; | |
+ false -> | |
+ case get_file(Module,State#main_state.imported) of | |
+ {ok,File,ImportFiles} -> | |
+ {imported, File, ImportFiles}; | |
+ false -> | |
+ throw(not_loaded) | |
+ end | |
+ end. | |
+ | |
+get_file(Module, [{Module, File}|_T]) -> | |
+ {ok, File}; | |
+get_file(Module, [{Module, File, ImportFiles}|_T]) -> | |
+ {ok, File, ImportFiles}; | |
+get_file(Module, [_H|T]) -> | |
+ get_file(Module, T); | |
+get_file(_Module, []) -> | |
+ false. | |
+ | |
+get_beam_file(Module,?TAG,Compiled) -> | |
+ {value,{Module,File}} = lists:keysearch(Module,1,Compiled), | |
+ case filename:extension(File) of | |
+ ".erl" -> {error,no_beam}; | |
+ ".beam" -> {ok,File} | |
+ end; | |
+get_beam_file(_Module,BeamFile,_Compiled) -> | |
+ {ok,BeamFile}. | |
+ | |
+get_modules(Compiled) -> | |
+ lists:map(fun({Module, _File}) -> Module end, Compiled). | |
+ | |
+update_compiled([Module|Modules], [{Module,_File}|Compiled]) -> | |
+ update_compiled(Modules, Compiled); | |
+update_compiled(Modules, [H|Compiled]) -> | |
+ [H|update_compiled(Modules, Compiled)]; | |
+update_compiled(_Modules, []) -> | |
+ []. | |
+ | |
+%% Get all compiled modules which are still loaded, and possibly an | |
+%% updated version of the Compiled list. | |
+get_compiled_still_loaded(Nodes,Compiled0) -> | |
+ %% Find all Cover compiled modules which are still loaded | |
+ CompiledModules = get_modules(Compiled0), | |
+ LoadedModules = lists:filter(fun(Module) -> | |
+ case code:which(Module) of | |
+ ?TAG -> true; | |
+ _ -> false | |
+ end | |
+ end, | |
+ CompiledModules), | |
+ | |
+ %% If some Cover compiled modules have been unloaded, update the database. | |
+ UnloadedModules = CompiledModules--LoadedModules, | |
+ Compiled = | |
+ case UnloadedModules of | |
+ [] -> | |
+ Compiled0; | |
+ _ -> | |
+ lists:foreach(fun(Module) -> do_clear(Module) end, | |
+ UnloadedModules), | |
+ remote_unload(Nodes,UnloadedModules), | |
+ update_compiled(UnloadedModules, Compiled0) | |
+ end, | |
+ {LoadedModules,Compiled}. | |
+ | |
+ | |
+%%%--Compilation--------------------------------------------------------- | |
+ | |
+do_compile_beams(ModsAndFiles, State) -> | |
+ Result0 = pmap(fun({ok,Module,File}) -> | |
+ do_compile_beam(Module,File,State); | |
+ (Error) -> | |
+ Error | |
+ end, | |
+ ModsAndFiles), | |
+ Compiled = [{M,F} || {ok,M,F} <- Result0], | |
+ remote_load_compiled(State#main_state.nodes,Compiled), | |
+ fix_state_and_result(Result0,State,[]). | |
+ | |
+do_compile_beam(Module,BeamFile0,State) -> | |
+ case get_beam_file(Module,BeamFile0,State#main_state.compiled) of | |
+ {ok,BeamFile} -> | |
+ UserOptions = get_compile_options(Module,BeamFile), | |
+ case do_compile_beam1(Module,BeamFile,UserOptions) of | |
+ {ok, Module} -> | |
+ {ok,Module,BeamFile}; | |
+ error -> | |
+ {error, BeamFile}; | |
+ {error,Reason} -> % no abstract code | |
+ {error, {Reason, BeamFile}} | |
+ end; | |
+ {error,no_beam} -> | |
+ %% The module has first been compiled from .erl, and now | |
+ %% someone tries to compile it from .beam | |
+ {error,{already_cover_compiled,no_beam_found,Module}} | |
+ end. | |
+ | |
+fix_state_and_result([{ok,Module,BeamFile}|Rest],State,Acc) -> | |
+ Compiled = add_compiled(Module,BeamFile,State#main_state.compiled), | |
+ Imported = remove_imported(Module,State#main_state.imported), | |
+ NewState = State#main_state{compiled=Compiled,imported=Imported}, | |
+ fix_state_and_result(Rest,NewState,[{ok,Module}|Acc]); | |
+fix_state_and_result([Error|Rest],State,Acc) -> | |
+ fix_state_and_result(Rest,State,[Error|Acc]); | |
+fix_state_and_result([],State,Acc) -> | |
+ {lists:reverse(Acc),State}. | |
+ | |
+ | |
+do_compile(Files, Options, State) -> | |
+ Result0 = pmap(fun(File) -> | |
+ do_compile(File, Options) | |
+ end, | |
+ Files), | |
+ Compiled = [{M,F} || {ok,M,F} <- Result0], | |
+ remote_load_compiled(State#main_state.nodes,Compiled), | |
+ fix_state_and_result(Result0,State,[]). | |
+ | |
+do_compile(File, Options) -> | |
+ case do_compile1(File, Options) of | |
+ {ok, Module} -> | |
+ {ok,Module,File}; | |
+ error -> | |
+ {error,File} | |
+ end. | |
+ | |
+%% do_compile1(File, Options) -> {ok,Module} | error | |
+do_compile1(File, UserOptions) -> | |
+ Options = [debug_info,binary,report_errors,report_warnings] ++ UserOptions, | |
+ case compile:file(File, Options) of | |
+ {ok, Module, Binary} -> | |
+ do_compile_beam1(Module,Binary,UserOptions); | |
+ error -> | |
+ error | |
+ end. | |
+ | |
+%% Beam is a binary or a .beam file name | |
+do_compile_beam1(Module,Beam,UserOptions) -> | |
+ %% Clear database | |
+ do_clear(Module), | |
+ | |
+ %% Extract the abstract format and insert calls to bump/6 at | |
+ %% every executable line and, as a side effect, initiate | |
+ %% the database | |
+ | |
+ case get_abstract_code(Module, Beam) of | |
+ no_abstract_code=E -> | |
+ {error,E}; | |
+ encrypted_abstract_code=E -> | |
+ {error,E}; | |
+ {raw_abstract_v1,Code} -> | |
+ Forms0 = epp:interpret_file_attribute(Code), | |
+ {Forms,Vars} = transform(Forms0, Module), | |
+ | |
+ %% We need to recover the source from the compilation | |
+ %% info otherwise the newly compiled module will have | |
+ %% source pointing to the current directory | |
+ SourceInfo = get_source_info(Module, Beam), | |
+ | |
+ %% Compile and load the result | |
+ %% It's necessary to check the result of loading since it may | |
+ %% fail, for example if Module resides in a sticky directory | |
+ {ok, Module, Binary} = compile:forms(Forms, SourceInfo ++ UserOptions), | |
+ case code:load_binary(Module, ?TAG, Binary) of | |
+ {module, Module} -> | |
+ | |
+ %% Store info about all function clauses in database | |
+ InitInfo = lists:reverse(Vars#vars.init_info), | |
+ ets:insert(?COVER_CLAUSE_TABLE, {Module, InitInfo}), | |
+ | |
+ %% Store binary code so it can be loaded on remote nodes | |
+ ets:insert(?BINARY_TABLE, {Module, Binary}), | |
+ | |
+ {ok, Module}; | |
+ | |
+ _Error -> | |
+ do_clear(Module), | |
+ error | |
+ end; | |
+ {_VSN,_Code} -> | |
+ %% Wrong version of abstract code. Just report that there | |
+ %% is no abstract code. | |
+ {error,no_abstract_code} | |
+ end. | |
+ | |
+get_abstract_code(Module, Beam) -> | |
+ case beam_lib:chunks(Beam, [abstract_code]) of | |
+ {ok, {Module, [{abstract_code, AbstractCode}]}} -> | |
+ AbstractCode; | |
+ {error,beam_lib,{key_missing_or_invalid,_,_}} -> | |
+ encrypted_abstract_code; | |
+ Error -> Error | |
+ end. | |
+ | |
+get_source_info(Module, Beam) -> | |
+ Compile = get_compile_info(Module, Beam), | |
+ case lists:keyfind(source, 1, Compile) of | |
+ { source, _ } = Tuple -> [Tuple]; | |
+ false -> [] | |
+ end. | |
+ | |
+get_compile_options(Module, Beam) -> | |
+ Compile = get_compile_info(Module, Beam), | |
+ case lists:keyfind(options, 1, Compile) of | |
+ {options, Options } -> filter_options(Options); | |
+ false -> [] | |
+ end. | |
+ | |
+get_compile_info(Module, Beam) -> | |
+ case beam_lib:chunks(Beam, [compile_info]) of | |
+ {ok, {Module, [{compile_info, Compile}]}} -> | |
+ Compile; | |
+ _ -> | |
+ [] | |
+ end. | |
+ | |
+transform(Code, Module) -> | |
+ MainFile=find_main_filename(Code), | |
+ Vars0 = #vars{module=Module}, | |
+ {ok,MungedForms,Vars} = transform_2(Code,[],Vars0,MainFile,on), | |
+ {MungedForms,Vars}. | |
+ | |
+%% Helpfunction which returns the first found file-attribute, which can | |
+%% be interpreted as the name of the main erlang source file. | |
+find_main_filename([{attribute,_,file,{MainFile,_}}|_]) -> | |
+ MainFile; | |
+find_main_filename([_|Rest]) -> | |
+ find_main_filename(Rest). | |
+ | |
+transform_2([Form0|Forms],MungedForms,Vars,MainFile,Switch) -> | |
+ Form = expand(Form0), | |
+ case munge(Form,Vars,MainFile,Switch) of | |
+ ignore -> | |
+ transform_2(Forms,MungedForms,Vars,MainFile,Switch); | |
+ {MungedForm,Vars2,NewSwitch} -> | |
+ transform_2(Forms,[MungedForm|MungedForms],Vars2,MainFile,NewSwitch) | |
+ end; | |
+transform_2([],MungedForms,Vars,_,_) -> | |
+ {ok, lists:reverse(MungedForms), Vars}. | |
+ | |
+%% Expand short-circuit Boolean expressions. | |
+expand(Expr) -> | |
+ AllVars = sets:from_list(ordsets:to_list(vars([], Expr))), | |
+ {Expr1,_} = expand(Expr, AllVars, 1), | |
+ Expr1. | |
+ | |
+expand({clause,Line,Pattern,Guards,Body}, Vs, N) -> | |
+ {ExpandedBody,N2} = expand(Body, Vs, N), | |
+ {{clause,Line,Pattern,Guards,ExpandedBody},N2}; | |
+expand({op,_Line,'andalso',ExprL,ExprR}, Vs, N) -> | |
+ {ExpandedExprL,N2} = expand(ExprL, Vs, N), | |
+ {ExpandedExprR,N3} = expand(ExprR, Vs, N2), | |
+ Anno = element(2, ExpandedExprL), | |
+ {bool_switch(ExpandedExprL, | |
+ ExpandedExprR, | |
+ {atom,Anno,false}, | |
+ Vs, N3), | |
+ N3 + 1}; | |
+expand({op,_Line,'orelse',ExprL,ExprR}, Vs, N) -> | |
+ {ExpandedExprL,N2} = expand(ExprL, Vs, N), | |
+ {ExpandedExprR,N3} = expand(ExprR, Vs, N2), | |
+ Anno = element(2, ExpandedExprL), | |
+ {bool_switch(ExpandedExprL, | |
+ {atom,Anno,true}, | |
+ ExpandedExprR, | |
+ Vs, N3), | |
+ N3 + 1}; | |
+expand(T, Vs, N) when is_tuple(T) -> | |
+ {TL,N2} = expand(tuple_to_list(T), Vs, N), | |
+ {list_to_tuple(TL),N2}; | |
+expand([E|Es], Vs, N) -> | |
+ {E2,N2} = expand(E, Vs, N), | |
+ {Es2,N3} = expand(Es, Vs, N2), | |
+ {[E2|Es2],N3}; | |
+expand(T, _Vs, N) -> | |
+ {T,N}. | |
+ | |
+vars(A, {var,_,V}) when V =/= '_' -> | |
+ [V|A]; | |
+vars(A, T) when is_tuple(T) -> | |
+ vars(A, tuple_to_list(T)); | |
+vars(A, [E|Es]) -> | |
+ vars(vars(A, E), Es); | |
+vars(A, _T) -> | |
+ A. | |
+ | |
+bool_switch(E, T, F, AllVars, AuxVarN) -> | |
+ Line = element(2, E), | |
+ AuxVar = {var,Line,aux_var(AllVars, AuxVarN)}, | |
+ {'case',Line,E, | |
+ [{clause,Line,[{atom,Line,true}],[],[T]}, | |
+ {clause,Line,[{atom,Line,false}],[],[F]}, | |
+ {clause,Line,[AuxVar],[], | |
+ [{call,Line, | |
+ {remote,Line,{atom,Line,erlang},{atom,Line,error}}, | |
+ [{tuple,Line,[{atom,Line,badarg},AuxVar]}]}]}]}. | |
+ | |
+aux_var(Vars, N) -> | |
+ Name = list_to_atom(lists:concat(['_', N])), | |
+ case sets:is_element(Name, Vars) of | |
+ true -> aux_var(Vars, N + 1); | |
+ false -> Name | |
+ end. | |
+ | |
+%% This code traverses the abstract code, stored as the abstract_code | |
+%% chunk in the BEAM file, as described in absform(3). | |
+%% The switch is turned off when we encounter other files than the main file. | |
+%% This way we will be able to exclude functions defined in include files. | |
+munge({function,Line,Function,Arity,Clauses},Vars,_MainFile,on) -> | |
+ Vars2 = Vars#vars{function=Function, | |
+ arity=Arity, | |
+ clause=1, | |
+ lines=[], | |
+ no_bump_lines=[], | |
+ depth=1}, | |
+ {MungedClauses, Vars3} = munge_clauses(Clauses, Vars2), | |
+ {{function,Line,Function,Arity,MungedClauses},Vars3,on}; | |
+munge(Form={attribute,_,file,{MainFile,_}},Vars,MainFile,_Switch) -> | |
+ {Form,Vars,on}; % Switch on tranformation! | |
+munge(Form={attribute,_,file,{_InclFile,_}},Vars,_MainFile,_Switch) -> | |
+ {Form,Vars,off}; % Switch off transformation! | |
+munge({attribute,_,compile,{parse_transform,_}},_Vars,_MainFile,_Switch) -> | |
+ %% Don't want to run parse transforms more than once. | |
+ ignore; | |
+munge(Form,Vars,_MainFile,Switch) -> % Other attributes and skipped includes. | |
+ {Form,Vars,Switch}. | |
+ | |
+munge_clauses(Clauses, Vars) -> | |
+ munge_clauses(Clauses, Vars, Vars#vars.lines, []). | |
+ | |
+munge_clauses([Clause|Clauses], Vars, Lines, MClauses) -> | |
+ {clause,Line,Pattern,Guards,Body} = Clause, | |
+ {MungedGuards, _Vars} = munge_exprs(Guards, Vars#vars{is_guard=true},[]), | |
+ | |
+ case Vars#vars.depth of | |
+ 1 -> % function clause | |
+ {MungedBody, Vars2} = munge_body(Body, Vars#vars{depth=2}), | |
+ ClauseInfo = {Vars2#vars.module, | |
+ Vars2#vars.function, | |
+ Vars2#vars.arity, | |
+ Vars2#vars.clause, | |
+ length(Vars2#vars.lines)}, % Not used? | |
+ InitInfo = [ClauseInfo | Vars2#vars.init_info], | |
+ Vars3 = Vars2#vars{init_info=InitInfo, | |
+ clause=(Vars2#vars.clause)+1, | |
+ lines=[], | |
+ no_bump_lines=[], | |
+ depth=1}, | |
+ NewBumps = Vars2#vars.lines, | |
+ NewLines = NewBumps ++ Lines, | |
+ munge_clauses(Clauses, Vars3, NewLines, | |
+ [{clause,Line,Pattern,MungedGuards,MungedBody}| | |
+ MClauses]); | |
+ | |
+ 2 -> % receive-, case-, if-, or try-clause | |
+ Lines0 = Vars#vars.lines, | |
+ {MungedBody, Vars2} = munge_body(Body, Vars), | |
+ NewBumps = new_bumps(Vars2, Vars), | |
+ NewLines = NewBumps ++ Lines, | |
+ munge_clauses(Clauses, Vars2#vars{lines=Lines0}, | |
+ NewLines, | |
+ [{clause,Line,Pattern,MungedGuards,MungedBody}| | |
+ MClauses]) | |
+ end; | |
+munge_clauses([], Vars, Lines, MungedClauses) -> | |
+ {lists:reverse(MungedClauses), Vars#vars{lines = Lines}}. | |
+ | |
+munge_body(Expr, Vars) -> | |
+ munge_body(Expr, Vars, [], []). | |
+ | |
+munge_body([Expr|Body], Vars, MungedBody, LastExprBumpLines) -> | |
+ %% Here is the place to add a call to cover:bump/6! | |
+ Line = erl_anno:line(element(2, Expr)), | |
+ Lines = Vars#vars.lines, | |
+ case lists:member(Line,Lines) of | |
+ true -> % already a bump at this line | |
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars), | |
+ NewBumps = new_bumps(Vars2, Vars), | |
+ NoBumpLines = [Line|Vars#vars.no_bump_lines], | |
+ Vars3 = Vars2#vars{no_bump_lines = NoBumpLines}, | |
+ MungedBody1 = | |
+ maybe_fix_last_expr(MungedBody, Vars3, LastExprBumpLines), | |
+ MungedExprs1 = [MungedExpr|MungedBody1], | |
+ munge_body(Body, Vars3, MungedExprs1, NewBumps); | |
+ false -> | |
+ ets:insert(?COVER_TABLE, {#bump{module = Vars#vars.module, | |
+ function = Vars#vars.function, | |
+ arity = Vars#vars.arity, | |
+ clause = Vars#vars.clause, | |
+ line = Line}, | |
+ 0}), | |
+ Bump = bump_call(Vars, Line), | |
+% Bump = {call, 0, {remote, 0, {atom,0,cover}, {atom,0,bump}}, | |
+% [{atom, 0, Vars#vars.module}, | |
+% {atom, 0, Vars#vars.function}, | |
+% {integer, 0, Vars#vars.arity}, | |
+% {integer, 0, Vars#vars.clause}, | |
+% {integer, 0, Line}]}, | |
+ Lines2 = [Line|Lines], | |
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars#vars{lines=Lines2}), | |
+ NewBumps = new_bumps(Vars2, Vars), | |
+ NoBumpLines = subtract(Vars2#vars.no_bump_lines, NewBumps), | |
+ Vars3 = Vars2#vars{no_bump_lines = NoBumpLines}, | |
+ MungedBody1 = | |
+ maybe_fix_last_expr(MungedBody, Vars3, LastExprBumpLines), | |
+ MungedExprs1 = [MungedExpr,Bump|MungedBody1], | |
+ munge_body(Body, Vars3, MungedExprs1, NewBumps) | |
+ end; | |
+munge_body([], Vars, MungedBody, _LastExprBumpLines) -> | |
+ {lists:reverse(MungedBody), Vars}. | |
+ | |
+%%% Fix last expression (OTP-8188). A typical example: | |
+%%% | |
+%%% 3: case X of | |
+%%% 4: 1 -> a; % Bump line 5 after "a" has been evaluated! | |
+%%% 5: 2 -> b; 3 -> c end, F() | |
+%%% | |
+%%% Line 5 wasn't bumped just before "F()" since it was already bumped | |
+%%% before "b" (and before "c") (one mustn't bump a line more than | |
+%%% once in a single "evaluation"). The expression "case X ... end" is | |
+%%% now traversed again ("fixed"), this time adding bumps of line 5 | |
+%%% where appropriate, in this case when X matches 1. | |
+%%% | |
+%%% This doesn't solve all problems with expressions on the same line, | |
+%%% though. 'case' and 'try' are tricky. An example: | |
+%%% | |
+%%% 7: case case X of 1 -> foo(); % ? | |
+%%% 8: 2 -> bar() end of a -> 1; | |
+%%% 9: b -> 2 end. | |
+%%% | |
+%%% If X matches 1 and foo() evaluates to a then line 8 should be | |
+%%% bumped, but not if foo() evaluates to b. In other words, line 8 | |
+%%% cannot be bumped after "foo()" on line 7, so one has to bump line | |
+%%% 8 before "begin 1 end". But if X matches 2 and bar evaluates to a | |
+%%% then line 8 would be bumped twice (there has to be a bump before | |
+%%% "bar()". It is like one would have to have two copies of the inner | |
+%%% clauses, one for each outer clause. Maybe the munging should be | |
+%%% done on some of the compiler's "lower level" format. | |
+%%% | |
+%%% 'fun' is also problematic since a bump inside the body "shadows" | |
+%%% the rest of the line. | |
+ | |
+maybe_fix_last_expr(MungedExprs, Vars, LastExprBumpLines) -> | |
+ case last_expr_needs_fixing(Vars, LastExprBumpLines) of | |
+ {yes, Line} -> | |
+ fix_last_expr(MungedExprs, Line, Vars); | |
+ no -> | |
+ MungedExprs | |
+ end. | |
+ | |
+last_expr_needs_fixing(Vars, LastExprBumpLines) -> | |
+ case common_elems(Vars#vars.no_bump_lines, LastExprBumpLines) of | |
+ [Line] -> {yes, Line}; | |
+ _ -> no | |
+ end. | |
+ | |
+fix_last_expr([MungedExpr|MungedExprs], Line, Vars) -> | |
+ %% No need to update ?COVER_TABLE. | |
+ Bump = bump_call(Vars, Line), | |
+ [fix_expr(MungedExpr, Line, Bump)|MungedExprs]. | |
+ | |
+fix_expr({'if',L,Clauses}, Line, Bump) -> | |
+ FixedClauses = fix_clauses(Clauses, Line, Bump), | |
+ {'if',L,FixedClauses}; | |
+fix_expr({'case',L,Expr,Clauses}, Line, Bump) -> | |
+ FixedExpr = fix_expr(Expr, Line, Bump), | |
+ FixedClauses = fix_clauses(Clauses, Line, Bump), | |
+ {'case',L,FixedExpr,FixedClauses}; | |
+fix_expr({'receive',L,Clauses}, Line, Bump) -> | |
+ FixedClauses = fix_clauses(Clauses, Line, Bump), | |
+ {'receive',L,FixedClauses}; | |
+fix_expr({'receive',L,Clauses,Expr,Body}, Line, Bump) -> | |
+ FixedClauses = fix_clauses(Clauses, Line, Bump), | |
+ FixedExpr = fix_expr(Expr, Line, Bump), | |
+ FixedBody = fix_expr(Body, Line, Bump), | |
+ {'receive',L,FixedClauses,FixedExpr,FixedBody}; | |
+fix_expr({'try',L,Exprs,Clauses,CatchClauses,After}, Line, Bump) -> | |
+ FixedExprs = fix_expr(Exprs, Line, Bump), | |
+ FixedClauses = fix_clauses(Clauses, Line, Bump), | |
+ FixedCatchClauses = fix_clauses(CatchClauses, Line, Bump), | |
+ FixedAfter = fix_expr(After, Line, Bump), | |
+ {'try',L,FixedExprs,FixedClauses,FixedCatchClauses,FixedAfter}; | |
+fix_expr([E | Es], Line, Bump) -> | |
+ [fix_expr(E, Line, Bump) | fix_expr(Es, Line, Bump)]; | |
+fix_expr(T, Line, Bump) when is_tuple(T) -> | |
+ list_to_tuple(fix_expr(tuple_to_list(T), Line, Bump)); | |
+fix_expr(E, _Line, _Bump) -> | |
+ E. | |
+ | |
+fix_clauses([], _Line, _Bump) -> | |
+ []; | |
+fix_clauses(Cs, Line, Bump) -> | |
+ case bumps_line(lists:last(Cs), Line) of | |
+ true -> | |
+ fix_cls(Cs, Line, Bump); | |
+ false -> | |
+ Cs | |
+ end. | |
+ | |
+fix_cls([], _Line, _Bump) -> | |
+ []; | |
+fix_cls([Cl | Cls], Line, Bump) -> | |
+ case bumps_line(Cl, Line) of | |
+ true -> | |
+ [fix_expr(C, Line, Bump) || C <- [Cl | Cls]]; | |
+ false -> | |
+ {clause,CL,P,G,Body} = Cl, | |
+ UniqueVarName = list_to_atom(lists:concat(["$cover$ ",Line])), | |
+ A = erl_anno:new(0), | |
+ V = {var,A,UniqueVarName}, | |
+ [Last|Rest] = lists:reverse(Body), | |
+ Body1 = lists:reverse(Rest, [{match,A,V,Last},Bump,V]), | |
+ [{clause,CL,P,G,Body1} | fix_cls(Cls, Line, Bump)] | |
+ end. | |
+ | |
+bumps_line(E, L) -> | |
+ try bumps_line1(E, L) catch true -> true end. | |
+ | |
+bumps_line1({call,_,{remote,_,{atom,_,ets},{atom,_,update_counter}}, | |
+ [{atom,_,?COVER_TABLE},{tuple,_,[_,_,_,_,_,{integer,_,Line}]},_]}, | |
+ Line) -> | |
+ throw(true); | |
+bumps_line1([E | Es], Line) -> | |
+ bumps_line1(E, Line), | |
+ bumps_line1(Es, Line); | |
+bumps_line1(T, Line) when is_tuple(T) -> | |
+ bumps_line1(tuple_to_list(T), Line); | |
+bumps_line1(_, _) -> | |
+ false. | |
+ | |
+%%% End of fix of last expression. | |
+ | |
+bump_call(Vars, Line) -> | |
+ A = erl_anno:new(0), | |
+ {call,A,{remote,A,{atom,A,ets},{atom,A,update_counter}}, | |
+ [{atom,A,?COVER_TABLE}, | |
+ {tuple,A,[{atom,A,?BUMP_REC_NAME}, | |
+ {atom,A,Vars#vars.module}, | |
+ {atom,A,Vars#vars.function}, | |
+ {integer,A,Vars#vars.arity}, | |
+ {integer,A,Vars#vars.clause}, | |
+ {integer,A,Line}]}, | |
+ {integer,A,1}]}. | |
+ | |
+munge_expr({match,Line,ExprL,ExprR}, Vars) -> | |
+ {MungedExprL, Vars2} = munge_expr(ExprL, Vars), | |
+ {MungedExprR, Vars3} = munge_expr(ExprR, Vars2), | |
+ {{match,Line,MungedExprL,MungedExprR}, Vars3}; | |
+munge_expr({tuple,Line,Exprs}, Vars) -> | |
+ {MungedExprs, Vars2} = munge_exprs(Exprs, Vars, []), | |
+ {{tuple,Line,MungedExprs}, Vars2}; | |
+munge_expr({record,Line,Name,Exprs}, Vars) -> | |
+ {MungedExprFields, Vars2} = munge_exprs(Exprs, Vars, []), | |
+ {{record,Line,Name,MungedExprFields}, Vars2}; | |
+munge_expr({record,Line,Arg,Name,Exprs}, Vars) -> | |
+ {MungedArg, Vars2} = munge_expr(Arg, Vars), | |
+ {MungedExprFields, Vars3} = munge_exprs(Exprs, Vars2, []), | |
+ {{record,Line,MungedArg,Name,MungedExprFields}, Vars3}; | |
+munge_expr({record_field,Line,ExprL,ExprR}, Vars) -> | |
+ {MungedExprR, Vars2} = munge_expr(ExprR, Vars), | |
+ {{record_field,Line,ExprL,MungedExprR}, Vars2}; | |
+munge_expr({map,Line,Fields}, Vars) -> | |
+ %% EEP 43 | |
+ {MungedFields, Vars2} = munge_exprs(Fields, Vars, []), | |
+ {{map,Line,MungedFields}, Vars2}; | |
+munge_expr({map,Line,Arg,Fields}, Vars) -> | |
+ %% EEP 43 | |
+ {MungedArg, Vars2} = munge_expr(Arg, Vars), | |
+ {MungedFields, Vars3} = munge_exprs(Fields, Vars2, []), | |
+ {{map,Line,MungedArg,MungedFields}, Vars3}; | |
+munge_expr({map_field_assoc,Line,Name,Value}, Vars) -> | |
+ %% EEP 43 | |
+ {MungedName, Vars2} = munge_expr(Name, Vars), | |
+ {MungedValue, Vars3} = munge_expr(Value, Vars2), | |
+ {{map_field_assoc,Line,MungedName,MungedValue}, Vars3}; | |
+munge_expr({map_field_exact,Line,Name,Value}, Vars) -> | |
+ %% EEP 43 | |
+ {MungedName, Vars2} = munge_expr(Name, Vars), | |
+ {MungedValue, Vars3} = munge_expr(Value, Vars2), | |
+ {{map_field_exact,Line,MungedName,MungedValue}, Vars3}; | |
+munge_expr({cons,Line,ExprH,ExprT}, Vars) -> | |
+ {MungedExprH, Vars2} = munge_expr(ExprH, Vars), | |
+ {MungedExprT, Vars3} = munge_expr(ExprT, Vars2), | |
+ {{cons,Line,MungedExprH,MungedExprT}, Vars3}; | |
+munge_expr({op,Line,Op,ExprL,ExprR}, Vars) -> | |
+ {MungedExprL, Vars2} = munge_expr(ExprL, Vars), | |
+ {MungedExprR, Vars3} = munge_expr(ExprR, Vars2), | |
+ {{op,Line,Op,MungedExprL,MungedExprR}, Vars3}; | |
+munge_expr({op,Line,Op,Expr}, Vars) -> | |
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars), | |
+ {{op,Line,Op,MungedExpr}, Vars2}; | |
+munge_expr({'catch',Line,Expr}, Vars) -> | |
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars), | |
+ {{'catch',Line,MungedExpr}, Vars2}; | |
+munge_expr({call,Line1,{remote,Line2,ExprM,ExprF},Exprs}, | |
+ Vars) -> | |
+ {MungedExprM, Vars2} = munge_expr(ExprM, Vars), | |
+ {MungedExprF, Vars3} = munge_expr(ExprF, Vars2), | |
+ {MungedExprs, Vars4} = munge_exprs(Exprs, Vars3, []), | |
+ {{call,Line1,{remote,Line2,MungedExprM,MungedExprF},MungedExprs}, Vars4}; | |
+munge_expr({call,Line,Expr,Exprs}, Vars) -> | |
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars), | |
+ {MungedExprs, Vars3} = munge_exprs(Exprs, Vars2, []), | |
+ {{call,Line,MungedExpr,MungedExprs}, Vars3}; | |
+munge_expr({lc,Line,Expr,Qs}, Vars) -> | |
+ {MungedExpr, Vars2} = munge_expr(?BLOCK1(Expr), Vars), | |
+ {MungedQs, Vars3} = munge_qualifiers(Qs, Vars2), | |
+ {{lc,Line,MungedExpr,MungedQs}, Vars3}; | |
+munge_expr({bc,Line,Expr,Qs}, Vars) -> | |
+ {bin,BLine,[{bin_element,EL,Val,Sz,TSL}|Es]} = Expr, | |
+ Expr2 = {bin,BLine,[{bin_element,EL,?BLOCK1(Val),Sz,TSL}|Es]}, | |
+ {MungedExpr,Vars2} = munge_expr(Expr2, Vars), | |
+ {MungedQs, Vars3} = munge_qualifiers(Qs, Vars2), | |
+ {{bc,Line,MungedExpr,MungedQs}, Vars3}; | |
+munge_expr({block,Line,Body}, Vars) -> | |
+ {MungedBody, Vars2} = munge_body(Body, Vars), | |
+ {{block,Line,MungedBody}, Vars2}; | |
+munge_expr({'if',Line,Clauses}, Vars) -> | |
+ {MungedClauses,Vars2} = munge_clauses(Clauses, Vars), | |
+ {{'if',Line,MungedClauses}, Vars2}; | |
+munge_expr({'case',Line,Expr,Clauses}, Vars) -> | |
+ {MungedExpr,Vars2} = munge_expr(Expr, Vars), | |
+ {MungedClauses,Vars3} = munge_clauses(Clauses, Vars2), | |
+ {{'case',Line,MungedExpr,MungedClauses}, Vars3}; | |
+munge_expr({'receive',Line,Clauses}, Vars) -> | |
+ {MungedClauses,Vars2} = munge_clauses(Clauses, Vars), | |
+ {{'receive',Line,MungedClauses}, Vars2}; | |
+munge_expr({'receive',Line,Clauses,Expr,Body}, Vars) -> | |
+ {MungedExpr, Vars1} = munge_expr(Expr, Vars), | |
+ {MungedClauses,Vars2} = munge_clauses(Clauses, Vars1), | |
+ {MungedBody,Vars3} = | |
+ munge_body(Body, Vars2#vars{lines = Vars1#vars.lines}), | |
+ Vars4 = Vars3#vars{lines = Vars2#vars.lines ++ new_bumps(Vars3, Vars2)}, | |
+ {{'receive',Line,MungedClauses,MungedExpr,MungedBody}, Vars4}; | |
+munge_expr({'try',Line,Body,Clauses,CatchClauses,After}, Vars) -> | |
+ {MungedBody, Vars1} = munge_body(Body, Vars), | |
+ {MungedClauses, Vars2} = munge_clauses(Clauses, Vars1), | |
+ {MungedCatchClauses, Vars3} = munge_clauses(CatchClauses, Vars2), | |
+ {MungedAfter, Vars4} = munge_body(After, Vars3), | |
+ {{'try',Line,MungedBody,MungedClauses,MungedCatchClauses,MungedAfter}, | |
+ Vars4}; | |
+munge_expr({'fun',Line,{clauses,Clauses}}, Vars) -> | |
+ {MungedClauses,Vars2}=munge_clauses(Clauses, Vars), | |
+ {{'fun',Line,{clauses,MungedClauses}}, Vars2}; | |
+munge_expr({named_fun,Line,Name,Clauses}, Vars) -> | |
+ {MungedClauses,Vars2}=munge_clauses(Clauses, Vars), | |
+ {{named_fun,Line,Name,MungedClauses}, Vars2}; | |
+munge_expr({bin,Line,BinElements}, Vars) -> | |
+ {MungedBinElements,Vars2} = munge_exprs(BinElements, Vars, []), | |
+ {{bin,Line,MungedBinElements}, Vars2}; | |
+munge_expr({bin_element,Line,Value,Size,TypeSpecifierList}, Vars) -> | |
+ {MungedValue,Vars2} = munge_expr(Value, Vars), | |
+ {MungedSize,Vars3} = munge_expr(Size, Vars2), | |
+ {{bin_element,Line,MungedValue,MungedSize,TypeSpecifierList},Vars3}; | |
+munge_expr(Form, Vars) -> % var|char|integer|float|string|atom|nil|eof|default | |
+ {Form, Vars}. | |
+ | |
+munge_exprs([Expr|Exprs], Vars, MungedExprs) when Vars#vars.is_guard=:=true, | |
+ is_list(Expr) -> | |
+ {MungedExpr, _Vars} = munge_exprs(Expr, Vars, []), | |
+ munge_exprs(Exprs, Vars, [MungedExpr|MungedExprs]); | |
+munge_exprs([Expr|Exprs], Vars, MungedExprs) -> | |
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars), | |
+ munge_exprs(Exprs, Vars2, [MungedExpr|MungedExprs]); | |
+munge_exprs([], Vars, MungedExprs) -> | |
+ {lists:reverse(MungedExprs), Vars}. | |
+ | |
+%% Every qualifier is decorated with a counter. | |
+munge_qualifiers(Qualifiers, Vars) -> | |
+ munge_qs(Qualifiers, Vars, []). | |
+ | |
+munge_qs([{generate,Line,Pattern,Expr}|Qs], Vars, MQs) -> | |
+ L = element(2, Expr), | |
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars), | |
+ munge_qs1(Qs, L, {generate,Line,Pattern,MungedExpr}, Vars, Vars2, MQs); | |
+munge_qs([{b_generate,Line,Pattern,Expr}|Qs], Vars, MQs) -> | |
+ L = element(2, Expr), | |
+ {MExpr, Vars2} = munge_expr(Expr, Vars), | |
+ munge_qs1(Qs, L, {b_generate,Line,Pattern,MExpr}, Vars, Vars2, MQs); | |
+munge_qs([Expr|Qs], Vars, MQs) -> | |
+ L = element(2, Expr), | |
+ {MungedExpr, Vars2} = munge_expr(Expr, Vars), | |
+ munge_qs1(Qs, L, MungedExpr, Vars, Vars2, MQs); | |
+munge_qs([], Vars, MQs) -> | |
+ {lists:reverse(MQs), Vars}. | |
+ | |
+munge_qs1(Qs, Line, NQ, Vars, Vars2, MQs) -> | |
+ case new_bumps(Vars2, Vars) of | |
+ [_] -> | |
+ munge_qs(Qs, Vars2, [NQ | MQs]); | |
+ _ -> | |
+ {MungedTrue, Vars3} = munge_expr(?BLOCK({atom,Line,true}), Vars2), | |
+ munge_qs(Qs, Vars3, [NQ, MungedTrue | MQs]) | |
+ end. | |
+ | |
+new_bumps(#vars{lines = New}, #vars{lines = Old}) -> | |
+ subtract(New, Old). | |
+ | |
+subtract(L1, L2) -> | |
+ [E || E <- L1, not lists:member(E, L2)]. | |
+ | |
+common_elems(L1, L2) -> | |
+ [E || E <- L1, lists:member(E, L2)]. | |
+ | |
+%%%--Analysis------------------------------------------------------------ | |
+ | |
+%% Collect data for all modules | |
+collect(Nodes) -> | |
+ %% local node | |
+ AllClauses = ets:tab2list(?COVER_CLAUSE_TABLE), | |
+ Mon1 = spawn_monitor(fun() -> pmap(fun move_modules/1,AllClauses) end), | |
+ | |
+ %% remote nodes | |
+ Mon2 = spawn_monitor(fun() -> remote_collect('_',Nodes,false) end), | |
+ get_downs([Mon1,Mon2]). | |
+ | |
+%% Collect data for a list of modules | |
+collect(Modules,Nodes) -> | |
+ MS = [{{'$1','_'},[{'==','$1',M}],['$_']} || M <- Modules], | |
+ Clauses = ets:select(?COVER_CLAUSE_TABLE,MS), | |
+ Mon1 = spawn_monitor(fun() -> pmap(fun move_modules/1,Clauses) end), | |
+ | |
+ %% remote nodes | |
+ Mon2 = spawn_monitor(fun() -> remote_collect('_',Nodes,false) end), | |
+ get_downs([Mon1,Mon2]). | |
+ | |
+%% Collect data for one module | |
+collect(Module,Clauses,Nodes) -> | |
+ %% local node | |
+ move_modules({Module,Clauses}), | |
+ | |
+ %% remote nodes | |
+ remote_collect([Module],Nodes,false). | |
+ | |
+ | |
+%% When analysing, the data from the local ?COVER_TABLE is moved to the | |
+%% ?COLLECTION_TABLE. Resetting data in ?COVER_TABLE | |
+move_modules({Module,Clauses}) -> | |
+ ets:insert(?COLLECTION_CLAUSE_TABLE,{Module,Clauses}), | |
+ Pattern = {#bump{module=Module, _='_'}, '_'}, | |
+ MatchSpec = [{Pattern,[],['$_']}], | |
+ Match = ets:select(?COVER_TABLE,MatchSpec,?CHUNK_SIZE), | |
+ do_move_module(Match). | |
+ | |
+do_move_module({Bumps,Continuation}) -> | |
+ lists:foreach(fun({Key,Val}) -> | |
+ ets:insert(?COVER_TABLE, {Key,0}), | |
+ insert_in_collection_table(Key,Val) | |
+ end, | |
+ Bumps), | |
+ do_move_module(ets:select(Continuation)); | |
+do_move_module('$end_of_table') -> | |
+ ok. | |
+ | |
+%% Given a .beam file, find the .erl file. Look first in same directory as | |
+%% the .beam file, then in ../src, then in compile info. | |
+find_source(Module, File0) -> | |
+ try | |
+ Root = filename:rootname(File0, ".beam"), | |
+ Root == File0 andalso throw(File0), %% not .beam | |
+ %% Look for .erl in pwd. | |
+ File = Root ++ ".erl", | |
+ throw_file(File), | |
+ %% Not in pwd: look in ../src. | |
+ BeamDir = filename:dirname(File), | |
+ Base = filename:basename(File), | |
+ throw_file(filename:join([BeamDir, "..", "src", Base])), | |
+ %% Not in ../src: look for source path in compile info, but | |
+ %% first look relative the beam directory. | |
+ Info = | |
+ try lists:keyfind(source, 1, Module:module_info(compile)) | |
+ catch error:undef -> | |
+ %% The module might have been imported | |
+ %% and the beam not available | |
+ throw({beam, File0}) | |
+ end, | |
+ false == Info andalso throw({beam, File0}), %% stripped | |
+ {source, SrcFile} = Info, | |
+ throw_file(splice(BeamDir, SrcFile)), %% below ../src | |
+ throw_file(SrcFile), %% or absolute | |
+ %% No success means that source is either not under ../src or | |
+ %% its relative path differs from that of compile info. (For | |
+ %% example, compiled under src/x but installed under src/y.) | |
+ %% An option to specify an arbitrary source path explicitly is | |
+ %% probably a better solution than either more heuristics or a | |
+ %% potentially slow filesystem search. | |
+ {beam, File0} | |
+ catch | |
+ Path -> Path | |
+ end. | |
+ | |
+throw_file(Path) -> | |
+ false /= Path andalso filelib:is_file(Path) andalso throw(Path). | |
+ | |
+%% Splice the tail of a source path, starting from the last "src" | |
+%% component, onto the parent of a beam directory, or return false if | |
+%% no "src" component is found. | |
+%% | |
+%% Eg. splice("/path/to/app-1.0/ebin", "/compiled/path/to/app/src/x/y.erl") | |
+%% --> "/path/to/app-1.0/ebin/../src/x/y.erl" | |
+%% | |
+%% This handles the case of source in subdirectories of ../src with | |
+%% beams that have moved since compilation. | |
+%% | |
+splice(BeamDir, SrcFile) -> | |
+ case lists:splitwith(fun(C) -> C /= "src" end, revsplit(SrcFile)) of | |
+ {T, [_|_]} -> %% found src component | |
+ filename:join([BeamDir, "..", "src" | lists:reverse(T)]); | |
+ {_, []} -> %% or not | |
+ false | |
+ end. | |
+ | |
+revsplit(Path) -> | |
+ lists:reverse(filename:split(Path)). | |
+ | |
+analyse_list(Modules, Analysis, Level, State) -> | |
+ {LoadedMF, ImportedMF, Error} = are_loaded(Modules, State, [], [], []), | |
+ Loaded = [M || {M,_} <- LoadedMF], | |
+ Imported = [M || {M,_} <- ImportedMF], | |
+ collect(Loaded, State#main_state.nodes), | |
+ MS = [{{'$1','_'},[{'==','$1',M}],['$_']} || M <- Loaded ++ Imported], | |
+ AllClauses = ets:select(?COLLECTION_CLAUSE_TABLE,MS), | |
+ Fun = fun({Module,Clauses}) -> | |
+ do_analyse(Module, Analysis, Level, Clauses) | |
+ end, | |
+ {result, lists:flatten(pmap(Fun, AllClauses)), Error}. | |
+ | |
+analyse_all(Analysis, Level, State) -> | |
+ collect(State#main_state.nodes), | |
+ AllClauses = ets:tab2list(?COLLECTION_CLAUSE_TABLE), | |
+ Fun = fun({Module,Clauses}) -> | |
+ do_analyse(Module, Analysis, Level, Clauses) | |
+ end, | |
+ {result, lists:flatten(pmap(Fun, AllClauses)), []}. | |
+ | |
+do_parallel_analysis(Module, Analysis, Level, Loaded, From, State) -> | |
+ analyse_info(Module,State#main_state.imported), | |
+ C = case Loaded of | |
+ {loaded, _File} -> | |
+ [{Module,Clauses}] = | |
+ ets:lookup(?COVER_CLAUSE_TABLE,Module), | |
+ collect(Module,Clauses,State#main_state.nodes), | |
+ Clauses; | |
+ _ -> | |
+ [{Module,Clauses}] = | |
+ ets:lookup(?COLLECTION_CLAUSE_TABLE,Module), | |
+ Clauses | |
+ end, | |
+ R = do_analyse(Module, Analysis, Level, C), | |
+ reply(From, {ok,R}). | |
+ | |
+%% do_analyse(Module, Analysis, Level, Clauses)-> {ok,Answer} | {error,Error} | |
+%% Clauses = [{Module,Function,Arity,Clause,Lines}] | |
+do_analyse(Module, Analysis, line, _Clauses) -> | |
+ Pattern = {#bump{module=Module},'_'}, | |
+ Bumps = ets:match_object(?COLLECTION_TABLE, Pattern), | |
+ Fun = case Analysis of | |
+ coverage -> | |
+ fun({#bump{line=L}, 0}) -> | |
+ {{Module,L}, {0,1}}; | |
+ ({#bump{line=L}, _N}) -> | |
+ {{Module,L}, {1,0}} | |
+ end; | |
+ calls -> | |
+ fun({#bump{line=L}, N}) -> | |
+ {{Module,L}, N} | |
+ end | |
+ end, | |
+ lists:keysort(1, lists:map(Fun, Bumps)); | |
+do_analyse(Module, Analysis, clause, _Clauses) -> | |
+ Pattern = {#bump{module=Module},'_'}, | |
+ Bumps = lists:keysort(1,ets:match_object(?COLLECTION_TABLE, Pattern)), | |
+ analyse_clause(Analysis,Bumps); | |
+do_analyse(Module, Analysis, function, Clauses) -> | |
+ ClauseResult = do_analyse(Module, Analysis, clause, Clauses), | |
+ merge_clauses(ClauseResult, merge_fun(Analysis)); | |
+do_analyse(Module, Analysis, module, Clauses) -> | |
+ FunctionResult = do_analyse(Module, Analysis, function, Clauses), | |
+ Result = merge_functions(FunctionResult, merge_fun(Analysis)), | |
+ {Module,Result}. | |
+ | |
+analyse_clause(_,[]) -> | |
+ []; | |
+analyse_clause(coverage, | |
+ [{#bump{module=M,function=F,arity=A,clause=C},_}|_]=Bumps) -> | |
+ analyse_clause_cov(Bumps,{M,F,A,C},0,0,[]); | |
+analyse_clause(calls,Bumps) -> | |
+ analyse_clause_calls(Bumps,{x,x,x,x},[]). | |
+ | |
+analyse_clause_cov([{#bump{module=M,function=F,arity=A,clause=C},N}|Bumps], | |
+ {M,F,A,C}=Clause,Ls,NotCov,Acc) -> | |
+ analyse_clause_cov(Bumps,Clause,Ls+1,if N==0->NotCov+1; true->NotCov end,Acc); | |
+analyse_clause_cov([{#bump{module=M1,function=F1,arity=A1,clause=C1},_}|_]=Bumps, | |
+ Clause,Ls,NotCov,Acc) -> | |
+ analyse_clause_cov(Bumps,{M1,F1,A1,C1},0,0,[{Clause,{Ls-NotCov,NotCov}}|Acc]); | |
+analyse_clause_cov([],Clause,Ls,NotCov,Acc) -> | |
+ lists:reverse(Acc,[{Clause,{Ls-NotCov,NotCov}}]). | |
+ | |
+analyse_clause_calls([{#bump{module=M,function=F,arity=A,clause=C},_}|Bumps], | |
+ {M,F,A,C}=Clause,Acc) -> | |
+ analyse_clause_calls(Bumps,Clause,Acc); | |
+analyse_clause_calls([{#bump{module=M1,function=F1,arity=A1,clause=C1},N}|Bumps], | |
+ _Clause,Acc) -> | |
+ analyse_clause_calls(Bumps,{M1,F1,A1,C1},[{{M1,F1,A1,C1},N}|Acc]); | |
+analyse_clause_calls([],_Clause,Acc) -> | |
+ lists:reverse(Acc). | |
+ | |
+merge_fun(coverage) -> | |
+ fun({Cov1,NotCov1}, {Cov2,NotCov2}) -> | |
+ {Cov1+Cov2, NotCov1+NotCov2} | |
+ end; | |
+merge_fun(calls) -> | |
+ fun(Calls1, Calls2) -> | |
+ Calls1+Calls2 | |
+ end. | |
+ | |
+merge_clauses(Clauses, MFun) -> merge_clauses(Clauses, MFun, []). | |
+merge_clauses([{{M,F,A,_C1},R1},{{M,F,A,C2},R2}|Clauses], MFun, Result) -> | |
+ merge_clauses([{{M,F,A,C2},MFun(R1,R2)}|Clauses], MFun, Result); | |
+merge_clauses([{{M,F,A,_C},R}|Clauses], MFun, Result) -> | |
+ merge_clauses(Clauses, MFun, [{{M,F,A},R}|Result]); | |
+merge_clauses([], _Fun, Result) -> | |
+ lists:reverse(Result). | |
+ | |
+merge_functions([{_MFA,R}|Functions], MFun) -> | |
+ merge_functions(Functions, MFun, R); | |
+merge_functions([],_MFun) -> % There are no clauses. | |
+ {0,0}. % No function can be covered or notcov. | |
+ | |
+merge_functions([{_MFA,R}|Functions], MFun, Result) -> | |
+ merge_functions(Functions, MFun, MFun(Result, R)); | |
+merge_functions([], _MFun, Result) -> | |
+ Result. | |
+ | |
+analyse_list_to_file(Modules, Opts, State) -> | |
+ {LoadedMF, ImportedMF, Error} = are_loaded(Modules, State, [], [], []), | |
+ collect([M || {M,_} <- LoadedMF], State#main_state.nodes), | |
+ OutDir = proplists:get_value(outdir,Opts), | |
+ HTML = lists:member(html,Opts), | |
+ Fun = fun({Module,File}) -> | |
+ OutFile = outfilename(OutDir,Module,HTML), | |
+ do_analyse_to_file(Module,File,OutFile,HTML,State) | |
+ end, | |
+ {Ok,Error1} = split_ok_error(pmap(Fun, LoadedMF++ImportedMF),[],[]), | |
+ {result,Ok,Error ++ Error1}. | |
+ | |
+analyse_all_to_file(Opts, State) -> | |
+ collect(State#main_state.nodes), | |
+ AllModules = get_all_modules(State), | |
+ OutDir = proplists:get_value(outdir,Opts), | |
+ HTML = lists:member(html,Opts), | |
+ Fun = fun({Module,File}) -> | |
+ OutFile = outfilename(OutDir,Module,HTML), | |
+ do_analyse_to_file(Module,File,OutFile,HTML,State) | |
+ end, | |
+ {Ok,Error} = split_ok_error(pmap(Fun, AllModules),[],[]), | |
+ {result,Ok,Error}. | |
+ | |
+get_all_modules(State) -> | |
+ get_all_modules(State#main_state.compiled ++ State#main_state.imported,[]). | |
+get_all_modules([{Module,File}|Rest],Acc) -> | |
+ get_all_modules(Rest,[{Module,File}|Acc]); | |
+get_all_modules([{Module,File,_}|Rest],Acc) -> | |
+ case lists:keymember(Module,1,Acc) of | |
+ true -> get_all_modules(Rest,Acc); | |
+ false -> get_all_modules(Rest,[{Module,File}|Acc]) | |
+ end; | |
+get_all_modules([],Acc) -> | |
+ Acc. | |
+ | |
+split_ok_error([{ok,R}|Result],Ok,Error) -> | |
+ split_ok_error(Result,[R|Ok],Error); | |
+split_ok_error([{error,R}|Result],Ok,Error) -> | |
+ split_ok_error(Result,Ok,[R|Error]); | |
+split_ok_error([],Ok,Error) -> | |
+ {Ok,Error}. | |
+ | |
+do_parallel_analysis_to_file(Module, Opts, Loaded, From, State) -> | |
+ File = case Loaded of | |
+ {loaded, File0} -> | |
+ [{Module,Clauses}] = | |
+ ets:lookup(?COVER_CLAUSE_TABLE,Module), | |
+ collect(Module, Clauses, | |
+ State#main_state.nodes), | |
+ File0; | |
+ {imported, File0, _} -> | |
+ File0 | |
+ end, | |
+ HTML = lists:member(html,Opts), | |
+ OutFile = | |
+ case proplists:get_value(outfile,Opts) of | |
+ undefined -> | |
+ outfilename(proplists:get_value(outdir,Opts),Module,HTML); | |
+ F -> | |
+ F | |
+ end, | |
+ reply(From, do_analyse_to_file(Module,File,OutFile,HTML,State)). | |
+ | |
+do_analyse_to_file(Module,File,OutFile,HTML,State) -> | |
+ case find_source(Module, File) of | |
+ {beam,_BeamFile} -> | |
+ {error,{no_source_code_found,Module}}; | |
+ ErlFile -> | |
+ analyse_info(Module,State#main_state.imported), | |
+ do_analyse_to_file1(Module,OutFile,ErlFile,HTML) | |
+ end. | |
+ | |
+%% do_analyse_to_file1(Module,OutFile,ErlFile) -> {ok,OutFile} | {error,Error} | |
+%% Module = atom() | |
+%% OutFile = ErlFile = string() | |
+do_analyse_to_file1(Module, OutFile, ErlFile, HTML) -> | |
+ case file:open(ErlFile, [read,raw,read_ahead]) of | |
+ {ok, InFd} -> | |
+ case file:open(OutFile, [write,raw,delayed_write]) of | |
+ {ok, OutFd} -> | |
+ if HTML -> | |
+ Encoding = encoding(ErlFile), | |
+ Header = | |
+ ["<!DOCTYPE HTML PUBLIC " | |
+ "\"-//W3C//DTD HTML 3.2 Final//EN\">\n" | |
+ "<html>\n" | |
+ "<head>\n" | |
+ "<meta http-equiv=\"Content-Type\"" | |
+ " content=\"text/html; charset=", | |
+ Encoding,"\"/>\n" | |
+ "<title>",OutFile,"</title>\n" | |
+ "</head>" | |
+ "<body style='background-color: white;" | |
+ " color: black'>\n" | |
+ "<pre>\n"], | |
+ file:write(OutFd,Header); | |
+ true -> ok | |
+ end, | |
+ | |
+ %% Write some initial information to the output file | |
+ {{Y,Mo,D},{H,Mi,S}} = calendar:local_time(), | |
+ Timestamp = | |
+ io_lib:format("~p-~s-~s at ~s:~s:~s", | |
+ [Y, | |
+ string:right(integer_to_list(Mo), 2, $0), | |
+ string:right(integer_to_list(D), 2, $0), | |
+ string:right(integer_to_list(H), 2, $0), | |
+ string:right(integer_to_list(Mi), 2, $0), | |
+ string:right(integer_to_list(S), 2, $0)]), | |
+ file:write(OutFd, | |
+ ["File generated from ",ErlFile," by COVER ", | |
+ Timestamp,"\n\n" | |
+ "**************************************" | |
+ "**************************************" | |
+ "\n\n"]), | |
+ | |
+ Pattern = {#bump{module=Module,line='$1',_='_'},'$2'}, | |
+ MS = [{Pattern,[{is_integer,'$1'},{'>','$1',0}],[{{'$1','$2'}}]}], | |
+ CovLines = lists:keysort(1,ets:select(?COLLECTION_TABLE, MS)), | |
+ print_lines(Module, CovLines, InFd, OutFd, 1, HTML), | |
+ | |
+ if | |
+ HTML -> | |
+ file:write(OutFd, "</pre>\n</body>\n</html>\n"); | |
+ true -> ok | |
+ end, | |
+ | |
+ file:close(OutFd), | |
+ file:close(InFd), | |
+ | |
+ {ok, OutFile}; | |
+ | |
+ {error, Reason} -> | |
+ {error, {file, OutFile, Reason}} | |
+ end; | |
+ | |
+ {error, Reason} -> | |
+ {error, {file, ErlFile, Reason}} | |
+ end. | |
+ | |
+ | |
+print_lines(Module, CovLines, InFd, OutFd, L, HTML) -> | |
+ case file:read_line(InFd) of | |
+ eof -> | |
+ ignore; | |
+ {ok,"%"++_=Line} -> %Comment line - not executed. | |
+ file:write(OutFd, [tab(),escape_lt_and_gt(Line, HTML)]), | |
+ print_lines(Module, CovLines, InFd, OutFd, L+1, HTML); | |
+ {ok,RawLine} -> | |
+ Line = escape_lt_and_gt(RawLine,HTML), | |
+ case CovLines of | |
+ [{L,N}|CovLines1] -> | |
+ %% N = lists:foldl(fun([Ni], Nacc) -> Nacc+Ni end, 0, Ns), | |
+ if | |
+ N=:=0, HTML=:=true -> | |
+ LineNoNL = Line -- "\n", | |
+ Str = " 0", | |
+ %%Str = string:right("0", 6, 32), | |
+ RedLine = ["<font color=red>",Str,fill1(), | |
+ LineNoNL,"</font>\n"], | |
+ file:write(OutFd, RedLine); | |
+ N<1000000 -> | |
+ Str = string:right(integer_to_list(N), 6, 32), | |
+ file:write(OutFd, [Str,fill1(),Line]); | |
+ N<10000000 -> | |
+ Str = integer_to_list(N), | |
+ file:write(OutFd, [Str,fill2(),Line]); | |
+ true -> | |
+ Str = integer_to_list(N), | |
+ file:write(OutFd, [Str,fill3(),Line]) | |
+ end, | |
+ print_lines(Module, CovLines1, InFd, OutFd, L+1, HTML); | |
+ _ -> | |
+ file:write(OutFd, [tab(),Line]), | |
+ print_lines(Module, CovLines, InFd, OutFd, L+1, HTML) | |
+ end | |
+ end. | |
+ | |
+tab() -> " | ". | |
+fill1() -> "..| ". | |
+fill2() -> ".| ". | |
+fill3() -> "| ". | |
+ | |
+%%%--Export-------------------------------------------------------------- | |
+do_export(Module, OutFile, From, State) -> | |
+ case file:open(OutFile,[write,binary,raw,delayed_write]) of | |
+ {ok,Fd} -> | |
+ Reply = | |
+ case Module of | |
+ '_' -> | |
+ export_info(State#main_state.imported), | |
+ collect(State#main_state.nodes), | |
+ do_export_table(State#main_state.compiled, | |
+ State#main_state.imported, | |
+ Fd); | |
+ _ -> | |
+ export_info(Module,State#main_state.imported), | |
+ try is_loaded(Module, State) of | |
+ {loaded, File} -> | |
+ [{Module,Clauses}] = | |
+ ets:lookup(?COVER_CLAUSE_TABLE,Module), | |
+ collect(Module, Clauses, | |
+ State#main_state.nodes), | |
+ do_export_table([{Module,File}],[],Fd); | |
+ {imported, File, ImportFiles} -> | |
+ %% don't know if I should allow this - | |
+ %% export a module which is only imported | |
+ Imported = [{Module,File,ImportFiles}], | |
+ do_export_table([],Imported,Fd) | |
+ catch throw:_ -> | |
+ {error,{not_cover_compiled,Module}} | |
+ end | |
+ end, | |
+ file:close(Fd), | |
+ reply(From, Reply); | |
+ {error,Reason} -> | |
+ reply(From, {error, {cant_open_file,OutFile,Reason}}) | |
+ | |
+ end. | |
+ | |
+do_export_table(Compiled, Imported, Fd) -> | |
+ ModList = merge(Imported,Compiled), | |
+ write_module_data(ModList,Fd). | |
+ | |
+merge([{Module,File,_ImportFiles}|Imported],ModuleList) -> | |
+ case lists:keymember(Module,1,ModuleList) of | |
+ true -> | |
+ merge(Imported,ModuleList); | |
+ false -> | |
+ merge(Imported,[{Module,File}|ModuleList]) | |
+ end; | |
+merge([],ModuleList) -> | |
+ ModuleList. | |
+ | |
+write_module_data([{Module,File}|ModList],Fd) -> | |
+ write({file,Module,File},Fd), | |
+ [Clauses] = ets:lookup(?COLLECTION_CLAUSE_TABLE,Module), | |
+ write(Clauses,Fd), | |
+ ModuleData = ets:match_object(?COLLECTION_TABLE,{#bump{module=Module},'_'}), | |
+ do_write_module_data(ModuleData,Fd), | |
+ write_module_data(ModList,Fd); | |
+write_module_data([],_Fd) -> | |
+ ok. | |
+ | |
+do_write_module_data([H|T],Fd) -> | |
+ write(H,Fd), | |
+ do_write_module_data(T,Fd); | |
+do_write_module_data([],_Fd) -> | |
+ ok. | |
+ | |
+write(Element,Fd) -> | |
+ Bin = term_to_binary(Element,[compressed]), | |
+ case byte_size(Bin) of | |
+ Size when Size > 255 -> | |
+ SizeBin = term_to_binary({'$size',Size}), | |
+ file:write(Fd, | |
+ <<(byte_size(SizeBin)):8,SizeBin/binary,Bin/binary>>); | |
+ Size -> | |
+ file:write(Fd,<<Size:8,Bin/binary>>) | |
+ end, | |
+ ok. | |
+ | |
+%%%--Import-------------------------------------------------------------- | |
+do_import_to_table(Fd,ImportFile,Imported) -> | |
+ do_import_to_table(Fd,ImportFile,Imported,[]). | |
+do_import_to_table(Fd,ImportFile,Imported,DontImport) -> | |
+ case get_term(Fd) of | |
+ {file,Module,File} -> | |
+ case add_imported(Module, File, ImportFile, Imported) of | |
+ {ok,NewImported} -> | |
+ do_import_to_table(Fd,ImportFile,NewImported,DontImport); | |
+ dont_import -> | |
+ do_import_to_table(Fd,ImportFile,Imported, | |
+ [Module|DontImport]) | |
+ end; | |
+ {Key=#bump{module=Module},Val} -> | |
+ case lists:member(Module,DontImport) of | |
+ false -> | |
+ insert_in_collection_table(Key,Val); | |
+ true -> | |
+ ok | |
+ end, | |
+ do_import_to_table(Fd,ImportFile,Imported,DontImport); | |
+ {Module,Clauses} -> | |
+ case lists:member(Module,DontImport) of | |
+ false -> | |
+ ets:insert(?COLLECTION_CLAUSE_TABLE,{Module,Clauses}); | |
+ true -> | |
+ ok | |
+ end, | |
+ do_import_to_table(Fd,ImportFile,Imported,DontImport); | |
+ eof -> | |
+ Imported | |
+ end. | |
+ | |
+ | |
+get_term(Fd) -> | |
+ case file:read(Fd,1) of | |
+ {ok,<<Size1:8>>} -> | |
+ {ok,Bin1} = file:read(Fd,Size1), | |
+ case binary_to_term(Bin1) of | |
+ {'$size',Size2} -> | |
+ {ok,Bin2} = file:read(Fd,Size2), | |
+ binary_to_term(Bin2); | |
+ Term -> | |
+ Term | |
+ end; | |
+ eof -> | |
+ eof | |
+ end. | |
+ | |
+%%%--Reset--------------------------------------------------------------- | |
+ | |
+%% Reset main node and all remote nodes | |
+do_reset_main_node(Module,Nodes) -> | |
+ do_reset(Module), | |
+ do_reset_collection_table(Module), | |
+ remote_reset(Module,Nodes). | |
+ | |
+do_reset_collection_table(Module) -> | |
+ ets:delete(?COLLECTION_CLAUSE_TABLE,Module), | |
+ ets:match_delete(?COLLECTION_TABLE, {#bump{module=Module},'_'}). | |
+ | |
+%% do_reset(Module) -> ok | |
+%% The reset is done on ?CHUNK_SIZE number of bumps to avoid building | |
+%% long lists in the case of very large modules | |
+do_reset(Module) -> | |
+ Pattern = {#bump{module=Module, _='_'}, '$1'}, | |
+ MatchSpec = [{Pattern,[{'=/=','$1',0}],['$_']}], | |
+ Match = ets:select(?COVER_TABLE,MatchSpec,?CHUNK_SIZE), | |
+ do_reset2(Match). | |
+ | |
+do_reset2({Bumps,Continuation}) -> | |
+ lists:foreach(fun({Bump,_N}) -> | |
+ ets:insert(?COVER_TABLE, {Bump,0}) | |
+ end, | |
+ Bumps), | |
+ do_reset2(ets:select(Continuation)); | |
+do_reset2('$end_of_table') -> | |
+ ok. | |
+ | |
+do_clear(Module) -> | |
+ ets:match_delete(?COVER_CLAUSE_TABLE, {Module,'_'}), | |
+ ets:match_delete(?COVER_TABLE, {#bump{module=Module},'_'}), | |
+ case lists:member(?COLLECTION_TABLE, ets:all()) of | |
+ true -> | |
+ %% We're on the main node | |
+ ets:match_delete(?COLLECTION_TABLE, {#bump{module=Module},'_'}); | |
+ false -> | |
+ ok | |
+ end. | |
+ | |
+not_loaded(Module, unloaded, State) -> | |
+ do_clear(Module), | |
+ remote_unload(State#main_state.nodes,[Module]), | |
+ Compiled = update_compiled([Module], | |
+ State#main_state.compiled), | |
+ State#main_state{ compiled = Compiled }; | |
+not_loaded(_Module,_Else, State) -> | |
+ State. | |
+ | |
+ | |
+ | |
+%%%--Div----------------------------------------------------------------- | |
+ | |
+escape_lt_and_gt(Rawline,HTML) when HTML =/= true -> | |
+ Rawline; | |
+escape_lt_and_gt(Rawline,_HTML) -> | |
+ escape_lt_and_gt1(Rawline,[]). | |
+ | |
+escape_lt_and_gt1([$<|T],Acc) -> | |
+ escape_lt_and_gt1(T,[$;,$t,$l,$&|Acc]); | |
+escape_lt_and_gt1([$>|T],Acc) -> | |
+ escape_lt_and_gt1(T,[$;,$t,$g,$&|Acc]); | |
+escape_lt_and_gt1([$&|T],Acc) -> | |
+ escape_lt_and_gt1(T,[$;,$p,$m,$a,$&|Acc]); | |
+escape_lt_and_gt1([],Acc) -> | |
+ lists:reverse(Acc); | |
+escape_lt_and_gt1([H|T],Acc) -> | |
+ escape_lt_and_gt1(T,[H|Acc]). | |
+ | |
+%%%--Internal functions for parallelization------------------------------ | |
+pmap(Fun,List) -> | |
+ NTot = length(List), | |
+ NProcs = erlang:system_info(schedulers) * 2, | |
+ NPerProc = (NTot div NProcs) + 1, | |
+ Mons = pmap_spawn(Fun,NPerProc,List,[]), | |
+ pmap_collect(Mons,[]). | |
+ | |
+pmap_spawn(_,_,[],Mons) -> | |
+ Mons; | |
+pmap_spawn(Fun,NPerProc,List,Mons) -> | |
+ {L1,L2} = if length(List)>=NPerProc -> lists:split(NPerProc,List); | |
+ true -> {List,[]} % last chunk | |
+ end, | |
+ Mon = | |
+ spawn_monitor( | |
+ fun() -> | |
+ exit({pmap_done,lists:map(Fun,L1)}) | |
+ end), | |
+ pmap_spawn(Fun,NPerProc,L2,[Mon|Mons]). | |
+ | |
+pmap_collect([],Acc) -> | |
+ lists:append(Acc); | |
+pmap_collect(Mons,Acc) -> | |
+ receive | |
+ {'DOWN', Ref, process, Pid, {pmap_done,Result}} -> | |
+ pmap_collect(lists:delete({Pid,Ref},Mons),[Result|Acc]); | |
+ {'DOWN', Ref, process, Pid, Reason} = Down -> | |
+ case lists:member({Pid,Ref},Mons) of | |
+ true -> | |
+ %% Something went really wrong - don't hang! | |
+ exit(Reason); | |
+ false -> | |
+ %% This should be handled somewhere else | |
+ self() ! Down, | |
+ pmap_collect(Mons,Acc) | |
+ end | |
+ end. | |
+ | |
+%%%----------------------------------------------------------------- | |
+%%% Read encoding from source file | |
+encoding(File) -> | |
+ Encoding = | |
+ case epp:read_encoding(File) of | |
+ none -> | |
+ epp:default_encoding(); | |
+ E -> | |
+ E | |
+ end, | |
+ html_encoding(Encoding). | |
+ | |
+html_encoding(latin1) -> | |
+ "iso-8859-1"; | |
+html_encoding(utf8) -> | |
+ "utf-8". |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment