%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2006-2009. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
%%% @doc Common Test Framework code coverage support module.
%%%
%%% <p>This module exports help functions for performing code
%%% coverage analysis.</p>
-module(ct_cover).
-export([get_spec/1, add_nodes/1, remove_nodes/1]).
-include("ct_util.hrl").
-include_lib("kernel/include/file.hrl").
%%%-----------------------------------------------------------------
%%% @spec add_nodes(Nodes) -> {ok,StartedNodes} | {error,Reason}
%%% Nodes = [atom()]
%%% StartedNodes = [atom()]
%%% Reason = cover_not_running | not_main_node
%%%
%%% @doc Add nodes to current cover test (only works if cover support
%%% is active!). To have effect, this function should be called
%%% from init_per_suite/1 before any actual tests are performed.
%%%
add_nodes([]) ->
{ok,[]};
add_nodes(Nodes) ->
case whereis(cover_server) of
undefined ->
{error,cover_not_running};
_ ->
{File,Nodes0,Import,Export,AppInfo} = ct_util:get_testdata(cover),
Nodes1 = [Node || Node <- Nodes,
lists:member(Node,Nodes0) == false],
ct_logs:log("COVER INFO",
"Adding nodes to cover test: ~w", [Nodes1]),
case cover:start(Nodes1) of
Result = {ok,_} ->
ct_util:set_testdata({cover,{File,Nodes1++Nodes0,
Import,Export,AppInfo}}),
Result;
Error ->
Error
end
end.
%%%-----------------------------------------------------------------
%%% @spec remove_nodes(Nodes) -> ok | {error,Reason}
%%% Nodes = [atom()]
%%% Reason = cover_not_running | not_main_node
%%%
%%% @doc Remove nodes from current cover test. Call this function
%%% to stop cover test on nodes previously added with add_nodes/1.
%%% Results on the remote node are transferred to the Common Test
%%% node.
%%%
remove_nodes([]) ->
ok;
remove_nodes(Nodes) ->
case whereis(cover_server) of
undefined ->
{error,cover_not_running};
_ ->
{File,Nodes0,Import,Export,AppInfo} = ct_util:get_testdata(cover),
ToRemove = [Node || Node <- Nodes, lists:member(Node,Nodes0)],
ct_logs:log("COVER INFO",
"Removing nodes from cover test: ~w", [ToRemove]),
case cover:stop(ToRemove) of
ok ->
Nodes1 = lists:foldl(fun(N,Deleted) ->
lists:delete(N,Deleted)
end, Nodes0, ToRemove),
ct_util:set_testdata({cover,{File,Nodes1,
Import,Export,AppInfo}}),
ok;
Error ->
Error
end
end.
%%%-----------------------------------------------------------------
%%% @hidden
%% Read cover specification file and return the parsed info.
%% -> CoverSpec: {CoverFile,Nodes,Import,Export,AppCoverInfo}
get_spec(File) ->
catch get_spec_test(File).
get_spec_test(File) ->
FullName = filename:absname(File),
case filelib:is_file(FullName) of
true ->
case file:consult(FullName) of
{ok,Terms} ->
Import =
case lists:keysearch(import, 1, Terms) of
{value,{_,Imps=[S|_]}} when is_list(S) ->
ImpsFN = lists:map(fun(F) ->
filename:absname(F)
end, Imps),
test_files(ImpsFN, ImpsFN);
{value,{_,Imp=[IC|_]}} when is_integer(IC) ->
ImpFN = filename:absname(Imp),
test_files([ImpFN], [ImpFN]);
_ ->
[]
end,
Export =
case lists:keysearch(export, 1, Terms) of
{value,{_,Exp=[EC|_]}} when is_integer(EC) ->
filename:absname(Exp);
{value,{_,[Exp]}} ->
filename:absname(Exp);
_ ->
[]
end,
Nodes =
case lists:keysearch(nodes, 1, Terms) of
{value,{_,Ns}} ->
Ns;
_ ->
[]
end,
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% NOTE! We can read specifications with multiple %%
%% apps, but since we don't have support for %%
%% running cover on more than one app at a time, %%
%% we just allow 1 app per spec for now. %%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
case collect_apps(Terms, []) of
Res when Res == [] ; length(Res) == 1 -> % 1 app = ok
Apps = case Res of
[] -> [#cover{app=none, level=details}];
_ -> Res
end,
case get_cover_opts(Apps, Terms, []) of
E = {error,_} ->
E;
[CoverSpec] ->
CoverSpec1 = remove_excludes_and_dups(CoverSpec),
{FullName,Nodes,Import,Export,CoverSpec1};
_ ->
{error,multiple_apps_in_cover_spec}
end;
Apps when is_list(Apps) ->
{error,multiple_apps_in_cover_spec}
end;
Error -> % file:consult/1 fails
{error,{invalid_cover_spec,Error}}
end;
false ->
{error,{cant_read_cover_spec_file,FullName}}
end.
collect_apps([{level,Level}|Ts], Apps) ->
collect_apps(Ts, [#cover{app=none, level=Level}|Apps]);
collect_apps([{incl_app,App,Level}|Ts], Apps) ->
collect_apps(Ts, [#cover{app=App, level=Level}|Apps]);
collect_apps([_|Ts], Apps) ->
collect_apps(Ts, Apps);
collect_apps([], Apps) ->
Apps.
%% get_cover_opts(Terms) -> AppCoverInfo
%% AppCoverInfo: [#cover{app=App,...}]
get_cover_opts([App | Apps], Terms, CoverInfo) ->
case get_app_info(App, Terms) of
E = {error,_} -> E;
AppInfo ->
AppInfo1 = files2mods(AppInfo),
get_cover_opts(Apps, Terms, [AppInfo1|CoverInfo])
end;
get_cover_opts([], _, CoverInfo) ->
lists:reverse(CoverInfo).
%% get_app_info(App, Terms) -> App1
get_app_info(App=#cover{app=none}, [{incl_dirs,Dirs}|Terms]) ->
get_app_info(App, [{incl_dirs,none,Dirs}|Terms]);
get_app_info(App=#cover{app=Name}, [{incl_dirs,Name,Dirs}|Terms]) ->
case get_files(Dirs, ".beam", false, []) of
E = {error,_} -> E;
Mods1 ->
Mods = App#cover.incl_mods,
get_app_info(App#cover{incl_mods=Mods++Mods1},Terms)
end;
get_app_info(App=#cover{app=none}, [{incl_dirs_r,Dirs}|Terms]) ->
get_app_info(App, [{incl_dirs_r,none,Dirs}|Terms]);
get_app_info(App=#cover{app=Name}, [{incl_dirs_r,Name,Dirs}|Terms]) ->
case get_files(Dirs, ".beam", true, []) of
E = {error,_} -> E;
Mods1 ->
Mods = App#cover.incl_mods,
get_app_info(App#cover{incl_mods=Mods++Mods1},Terms)
end;
get_app_info(App=#cover{app=none}, [{incl_mods,Mods1}|Terms]) ->
get_app_info(App, [{incl_mods,none,Mods1}|Terms]);
get_app_info(App=#cover{app=Name}, [{incl_mods,Name,Mods1}|Terms]) ->
Mods = App#cover.incl_mods,
get_app_info(App#cover{incl_mods=Mods++Mods1},Terms);
get_app_info(App=#cover{app=none}, [{excl_dirs,Dirs}|Terms]) ->
get_app_info(App, [{excl_dirs,none,Dirs}|Terms]);
get_app_info(App=#cover{app=Name}, [{excl_dirs,Name,Dirs}|Terms]) ->
case get_files(Dirs, ".beam", false, []) of
E = {error,_} -> E;
Mods1 ->
Mods = App#cover.excl_mods,
get_app_info(App#cover{excl_mods=Mods++Mods1},Terms)
end;
get_app_info(App=#cover{app=none}, [{excl_dirs_r,Dirs}|Terms]) ->
get_app_info(App, [{excl_dirs_r,none,Dirs}|Terms]);
get_app_info(App=#cover{app=Name}, [{excl_dirs_r,Name,Dirs}|Terms]) ->
case get_files(Dirs, ".beam", true, []) of
E = {error,_} -> E;
Mods1 ->
Mods = App#cover.excl_mods,
get_app_info(App#cover{excl_mods=Mods++Mods1},Terms)
end;
get_app_info(App=#cover{app=none}, [{excl_mods,Mods1}|Terms]) ->
get_app_info(App, [{excl_mods,none,Mods1}|Terms]);
get_app_info(App=#cover{app=Name}, [{excl_mods,Name,Mods1}|Terms]) ->
Mods = App#cover.excl_mods,
get_app_info(App#cover{excl_mods=Mods++Mods1},Terms);
get_app_info(App=#cover{app=Name}, [{cross_apps,Name,AppMods1}|Terms]) ->
AppMods = App#cover.cross,
get_app_info(App#cover{cross=AppMods++AppMods1},Terms);
get_app_info(App=#cover{app=none}, [{src_dirs,Dirs}|Terms]) ->
get_app_info(App, [{src_dirs,none,Dirs}|Terms]);
get_app_info(App=#cover{app=Name}, [{src_dirs,Name,Dirs}|Terms]) ->
case get_files(Dirs, ".erl", false, []) of
E = {error,_} -> E;
Src1 ->
Src = App#cover.src,
get_app_info(App#cover{src=Src++Src1},Terms)
end;
get_app_info(App=#cover{app=none}, [{src_dirs_r,Dirs}|Terms]) ->
get_app_info(App, [{src_dirs_r,none,Dirs}|Terms]);
get_app_info(App=#cover{app=Name}, [{src_dirs_r,Name,Dirs}|Terms]) ->
case get_files(Dirs, ".erl", true, []) of
E = {error,_} -> E;
Src1 ->
Src = App#cover.src,
get_app_info(App#cover{src=Src++Src1},Terms)
end;
get_app_info(App=#cover{app=none}, [{src_files,Src1}|Terms]) ->
get_app_info(App, [{src_files,none,Src1}|Terms]);
get_app_info(App=#cover{app=Name}, [{src_files,Name,Src1}|Terms]) ->
Src = App#cover.src,
get_app_info(App#cover{src=Src++Src1},Terms);
get_app_info(App, [_|Terms]) ->
get_app_info(App, Terms);
get_app_info(App, []) ->
App.
%% get_files(...)
get_files([Dir|Dirs], Ext, Recurse, Files) ->
case file:list_dir(Dir) of
{ok,Entries} ->
{SubDirs,Matches} = analyse_files(Entries, Dir, Ext, [], []),
if Recurse == false ->
get_files(Dirs, Ext, Recurse, Files++Matches);
true ->
Files1 = get_files(SubDirs, Ext, Recurse, Files++Matches),
get_files(Dirs, Ext, Recurse, Files1)
end;
{error,Reason} ->
{error,{Reason,Dir}}
end;
get_files([], _Ext, _R, Files) ->
Files.
%% analyse_files(...)
analyse_files([F|Fs], Dir, Ext, Dirs, Matches) ->
Fullname = filename:absname(F, Dir),
{ok,Info} = file:read_file_info(Fullname),
case Info#file_info.type of
directory ->
analyse_files(Fs, Dir, Ext,
[Fullname|Dirs], Matches);
_ ->
case filename:extension(Fullname) of
".beam" when Ext == ".beam" ->
%% File = {file,Dir,filename:rootname(F)},
Mod = list_to_atom(filename:rootname(F)),
analyse_files(Fs, Dir, Ext, Dirs,
[Mod|Matches]);
".erl" when Ext == ".erl" ->
analyse_files(Fs, Dir, Ext, Dirs,
[Fullname|Matches]);
_ ->
analyse_files(Fs, Dir, Ext, Dirs, Matches)
end
end;
analyse_files([], _Dir, _Ext, Dirs, Matches) ->
{Dirs,Matches}.
test_files([F|Fs], Ret) ->
case filelib:is_file(F) of
true ->
test_files(Fs, Ret);
false ->
throw({error,{invalid_cover_file,F}})
end;
test_files([], Ret) ->
Ret.
remove_excludes_and_dups(CoverData=#cover{excl_mods=Excl,incl_mods=Incl}) ->
Incl1 = [Mod || Mod <- Incl, lists:member(Mod, Excl) == false],
%% delete duplicates and sort
Incl2 = lists:sort(lists:foldl(fun(M,L) ->
case lists:member(M,L) of
true -> L;
false -> [M|L]
end
end, [], Incl1)),
CoverData#cover{incl_mods=Incl2}.
files2mods(Info=#cover{excl_mods=ExclFs,
incl_mods=InclFs,
cross=CrossFs}) ->
Info#cover{excl_mods=files2mods1(ExclFs),
incl_mods=files2mods1(InclFs),
cross=files2mods1(CrossFs)}.
files2mods1([M|Fs]) when is_atom(M) ->
[M|files2mods1(Fs)];
files2mods1([F|Fs]) when is_list(F) ->
M = filename:rootname(filename:basename(F)),
[list_to_atom(M)|files2mods1(Fs)];
files2mods1([]) ->
[].