%% %% %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. %%% %%%
This module exports help functions for performing code %%% coverage analysis.
-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([]) -> [].