1%% 2%% %CopyrightBegin% 3%% 4%% Copyright Ericsson AB 2006-2020. All Rights Reserved. 5%% 6%% Licensed under the Apache License, Version 2.0 (the "License"); 7%% you may not use this file except in compliance with the License. 8%% You may obtain a copy of the License at 9%% 10%% http://www.apache.org/licenses/LICENSE-2.0 11%% 12%% Unless required by applicable law or agreed to in writing, software 13%% distributed under the License is distributed on an "AS IS" BASIS, 14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15%% See the License for the specific language governing permissions and 16%% limitations under the License. 17%% 18%% %CopyrightEnd% 19%% 20 21-module(ct_cover). 22 23-export([get_spec/1, add_nodes/1, remove_nodes/1, cross_cover_analyse/2]). 24 25-include("ct_util.hrl"). 26 27-include_lib("kernel/include/file.hrl"). 28 29add_nodes([]) -> 30 {ok,[]}; 31add_nodes(Nodes) -> 32 case whereis(cover_server) of 33 undefined -> 34 {error,cover_not_running}; 35 _ -> 36 Nodes0 = cover:which_nodes(), 37 Nodes1 = [Node || Node <- Nodes, 38 lists:member(Node,Nodes0) == false], 39 ct_logs:log("COVER INFO", 40 "Adding nodes to cover test: ~w", [Nodes1]), 41 case cover:start(Nodes1) of 42 Result = {ok,StartedNodes} -> 43 ct_logs:log("COVER INFO", 44 "Successfully added nodes to cover test: ~w", 45 [StartedNodes]), 46 Result; 47 Error -> 48 ct_logs:log("COVER INFO", 49 "Failed to add nodes to cover test: ~tp", 50 [Error]), 51 Error 52 end 53 end. 54 55remove_nodes([]) -> 56 ok; 57remove_nodes(Nodes) -> 58 case whereis(cover_server) of 59 undefined -> 60 {error,cover_not_running}; 61 _ -> 62 Nodes0 = cover:which_nodes(), 63 ToRemove = [Node || Node <- Nodes, lists:member(Node,Nodes0)], 64 ct_logs:log("COVER INFO", 65 "Removing nodes from cover test: ~w", [ToRemove]), 66 case cover:stop(ToRemove) of 67 ok -> 68 ct_logs:log("COVER INFO", 69 "Successfully removed nodes from cover test.", 70 []), 71 ok; 72 Error -> 73 ct_logs:log("COVER INFO", 74 "Failed to remove nodes from cover test: ~tp", 75 [Error]), 76 Error 77 end 78 end. 79 80cross_cover_analyse(Level,Tests) -> 81 test_server_ctrl:cross_cover_analyse(Level,Tests). 82 83 84%%%----------------------------------------------------------------- 85 86%% Read cover specification file and return the parsed info. 87%% -> CoverSpec: {CoverFile,Nodes,Import,Export,AppCoverInfo} 88get_spec(File) -> 89 catch get_spec_test(File). 90 91get_spec_test(File) -> 92 Dir = filename:dirname(File), % always abs path in here, set in ct_run 93 case filelib:is_file(File) of 94 true -> 95 case file:consult(File) of 96 {ok,Terms} -> 97 Import = 98 case lists:keysearch(import, 1, Terms) of 99 {value,{_,Imps=[S|_]}} when is_list(S) -> 100 ImpsFN = lists:map(fun(F) -> 101 filename:absname(F,Dir) 102 end, Imps), 103 test_files(ImpsFN, ImpsFN); 104 {value,{_,Imp=[IC|_]}} when is_integer(IC) -> 105 ImpFN = filename:absname(Imp,Dir), 106 test_files([ImpFN], [ImpFN]); 107 _ -> 108 [] 109 end, 110 Export = 111 case lists:keysearch(export, 1, Terms) of 112 {value,{_,Exp=[EC|_]}} when is_integer(EC) -> 113 filename:absname(Exp,Dir); 114 {value,{_,[Exp]}} -> 115 filename:absname(Exp,Dir); 116 _ -> 117 undefined 118 end, 119 Nodes = 120 case lists:keysearch(nodes, 1, Terms) of 121 {value,{_,Ns}} -> 122 Ns; 123 _ -> 124 [] 125 end, 126 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 127 %% NOTE! We can read specifications with multiple %% 128 %% apps, but since we don't have support for %% 129 %% running cover on more than one app at a time, %% 130 %% we just allow 1 app per spec for now. %% 131 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 132 case collect_apps(Terms, []) of 133 Res when Res == [] ; length(Res) == 1 -> % 1 app = ok 134 Apps = case Res of 135 [] -> [#cover{app=none, level=details}]; 136 _ -> Res 137 end, 138 case get_cover_opts(Apps, Terms, Dir, []) of 139 E = {error,_} -> 140 E; 141 [CoverSpec] -> 142 CoverSpec1 = remove_excludes_and_dups(CoverSpec), 143 {File,Nodes,Import,Export,CoverSpec1}; 144 _ -> 145 {error,multiple_apps_in_cover_spec} 146 end; 147 Apps when is_list(Apps) -> 148 {error,multiple_apps_in_cover_spec} 149 end; 150 Error -> % file:consult/1 fails 151 {error,{invalid_cover_spec,Error}} 152 end; 153 false -> 154 {error,{cant_read_cover_spec_file,File}} 155 end. 156 157collect_apps([{level,Level}|Ts], Apps) -> 158 collect_apps(Ts, [#cover{app=none, level=Level}|Apps]); 159collect_apps([{incl_app,App,Level}|Ts], Apps) -> 160 collect_apps(Ts, [#cover{app=App, level=Level}|Apps]); 161collect_apps([_|Ts], Apps) -> 162 collect_apps(Ts, Apps); 163collect_apps([], Apps) -> 164 Apps. 165 166%% get_cover_opts(Terms) -> AppCoverInfo 167%% AppCoverInfo: [#cover{app=App,...}] 168 169get_cover_opts([App | Apps], Terms, Dir, CoverInfo) -> 170 case get_app_info(App, Terms, Dir) of 171 E = {error,_} -> E; 172 AppInfo -> 173 AppInfo1 = files2mods(AppInfo), 174 get_cover_opts(Apps, Terms, Dir, [AppInfo1|CoverInfo]) 175 end; 176get_cover_opts([], _, _, CoverInfo) -> 177 lists:reverse(CoverInfo). 178 179%% get_app_info(App, Terms, Dir) -> App1 180 181get_app_info(App=#cover{app=none}, [{incl_dirs,Dirs}|Terms], Dir) -> 182 get_app_info(App, [{incl_dirs,none,Dirs}|Terms], Dir); 183get_app_info(App=#cover{app=Name}, [{incl_dirs,Name,Dirs}|Terms], Dir) -> 184 case get_files(Dirs, Dir, ".beam", false, []) of 185 E = {error,_} -> E; 186 Mods1 -> 187 Mods = App#cover.incl_mods, 188 get_app_info(App#cover{incl_mods=Mods++Mods1},Terms,Dir) 189 end; 190 191get_app_info(App=#cover{app=none}, [{incl_dirs_r,Dirs}|Terms], Dir) -> 192 get_app_info(App, [{incl_dirs_r,none,Dirs}|Terms], Dir); 193get_app_info(App=#cover{app=Name}, [{incl_dirs_r,Name,Dirs}|Terms], Dir) -> 194 case get_files(Dirs, Dir, ".beam", true, []) of 195 E = {error,_} -> E; 196 Mods1 -> 197 Mods = App#cover.incl_mods, 198 get_app_info(App#cover{incl_mods=Mods++Mods1},Terms,Dir) 199 end; 200 201get_app_info(App=#cover{app=none}, [{incl_mods,Mods1}|Terms], Dir) -> 202 get_app_info(App, [{incl_mods,none,Mods1}|Terms], Dir); 203get_app_info(App=#cover{app=Name}, [{incl_mods,Name,Mods1}|Terms], Dir) -> 204 Mods = App#cover.incl_mods, 205 get_app_info(App#cover{incl_mods=Mods++Mods1},Terms,Dir); 206 207get_app_info(App=#cover{app=none}, [{excl_dirs,Dirs}|Terms], Dir) -> 208 get_app_info(App, [{excl_dirs,none,Dirs}|Terms], Dir); 209get_app_info(App=#cover{app=Name}, [{excl_dirs,Name,Dirs}|Terms], Dir) -> 210 case get_files(Dirs, Dir, ".beam", false, []) of 211 E = {error,_} -> E; 212 Mods1 -> 213 Mods = App#cover.excl_mods, 214 get_app_info(App#cover{excl_mods=Mods++Mods1},Terms,Dir) 215 end; 216 217get_app_info(App=#cover{app=none}, [{excl_dirs_r,Dirs}|Terms],Dir) -> 218 get_app_info(App, [{excl_dirs_r,none,Dirs}|Terms],Dir); 219get_app_info(App=#cover{app=Name}, [{excl_dirs_r,Name,Dirs}|Terms],Dir) -> 220 case get_files(Dirs, Dir, ".beam", true, []) of 221 E = {error,_} -> E; 222 Mods1 -> 223 Mods = App#cover.excl_mods, 224 get_app_info(App#cover{excl_mods=Mods++Mods1},Terms,Dir) 225 end; 226 227get_app_info(App=#cover{app=none}, [{excl_mods,Mods1}|Terms], Dir) -> 228 get_app_info(App, [{excl_mods,none,Mods1}|Terms], Dir); 229get_app_info(App=#cover{app=Name}, [{excl_mods,Name,Mods1}|Terms], Dir) -> 230 Mods = App#cover.excl_mods, 231 get_app_info(App#cover{excl_mods=Mods++Mods1},Terms,Dir); 232 233get_app_info(App=#cover{app=none}, [{cross,Cross}|Terms], Dir) -> 234 get_app_info(App, [{cross,none,Cross}|Terms], Dir); 235get_app_info(App=#cover{app=Name}, [{cross,Name,Cross1}|Terms], Dir) -> 236 Cross = App#cover.cross, 237 get_app_info(App#cover{cross=Cross++Cross1},Terms,Dir); 238 239get_app_info(App=#cover{app=none}, [{src_dirs,Dirs}|Terms], Dir) -> 240 get_app_info(App, [{src_dirs,none,Dirs}|Terms], Dir); 241get_app_info(App=#cover{app=Name}, [{src_dirs,Name,Dirs}|Terms], Dir) -> 242 case get_files(Dirs, Dir, ".erl", false, []) of 243 E = {error,_} -> E; 244 Src1 -> 245 Src = App#cover.src, 246 get_app_info(App#cover{src=Src++Src1},Terms,Dir) 247 end; 248 249get_app_info(App=#cover{app=none}, [{src_dirs_r,Dirs}|Terms], Dir) -> 250 get_app_info(App, [{src_dirs_r,none,Dirs}|Terms], Dir); 251get_app_info(App=#cover{app=Name}, [{src_dirs_r,Name,Dirs}|Terms], Dir) -> 252 case get_files(Dirs, Dir, ".erl", true, []) of 253 E = {error,_} -> E; 254 Src1 -> 255 Src = App#cover.src, 256 get_app_info(App#cover{src=Src++Src1},Terms,Dir) 257 end; 258 259get_app_info(App=#cover{app=none}, [{src_files,Src1}|Terms], Dir) -> 260 get_app_info(App, [{src_files,none,Src1}|Terms], Dir); 261get_app_info(App=#cover{app=Name}, [{src_files,Name,Src1}|Terms], Dir) -> 262 Src = App#cover.src, 263 get_app_info(App#cover{src=Src++Src1},Terms,Dir); 264 265get_app_info(App=#cover{app=none}, [{local_only,Bool}|Terms], Dir) -> 266 get_app_info(App, [{local_only,none,Bool}|Terms], Dir); 267get_app_info(App=#cover{app=Name}, [{local_only,Name,Bool}|Terms], Dir) -> 268 get_app_info(App#cover{local_only=Bool},Terms,Dir); 269 270get_app_info(App, [_|Terms], Dir) -> 271 get_app_info(App, Terms, Dir); 272 273get_app_info(App, [], _) -> 274 App. 275 276%% get_files(...) 277 278get_files([Dir|Dirs], RootDir, Ext, Recurse, Files) -> 279 DirAbs = filename:absname(Dir, RootDir), 280 case file:list_dir(DirAbs) of 281 {ok,Entries} -> 282 {SubDirs,Matches} = analyse_files(Entries, DirAbs, Ext, [], []), 283 if Recurse == false -> 284 get_files(Dirs, RootDir, Ext, Recurse, Files++Matches); 285 true -> 286 Files1 = get_files(SubDirs, RootDir, Ext, Recurse, Files++Matches), 287 get_files(Dirs, RootDir, Ext, Recurse, Files1) 288 end; 289 {error,Reason} -> 290 {error,{Reason,DirAbs}} 291 end; 292get_files([], _RootDir, _Ext, _R, Files) -> 293 Files. 294 295%% analyse_files(...) 296 297analyse_files([F|Fs], Dir, Ext, Dirs, Matches) -> 298 Fullname = filename:absname(F, Dir), 299 {ok,Info} = file:read_file_info(Fullname), 300 case Info#file_info.type of 301 directory -> 302 analyse_files(Fs, Dir, Ext, 303 [Fullname|Dirs], Matches); 304 _ -> 305 case filename:extension(Fullname) of 306 ".beam" when Ext == ".beam" -> 307 %% File = {file,Dir,filename:rootname(F)}, 308 Mod = list_to_atom(filename:rootname(F)), 309 analyse_files(Fs, Dir, Ext, Dirs, 310 [Mod|Matches]); 311 ".erl" when Ext == ".erl" -> 312 analyse_files(Fs, Dir, Ext, Dirs, 313 [Fullname|Matches]); 314 _ -> 315 analyse_files(Fs, Dir, Ext, Dirs, Matches) 316 end 317 end; 318analyse_files([], _Dir, _Ext, Dirs, Matches) -> 319 {Dirs,Matches}. 320 321 322test_files([F|Fs], Ret) -> 323 case filelib:is_file(F) of 324 true -> 325 test_files(Fs, Ret); 326 false -> 327 throw({error,{invalid_cover_file,F}}) 328 end; 329test_files([], Ret) -> 330 Ret. 331 332remove_excludes_and_dups(CoverData=#cover{excl_mods=Excl,incl_mods=Incl}) -> 333 Incl1 = [Mod || Mod <- Incl, lists:member(Mod, Excl) == false], 334 %% delete duplicates and sort 335 Incl2 = lists:sort(lists:foldl(fun(M,L) -> 336 case lists:member(M,L) of 337 true -> L; 338 false -> [M|L] 339 end 340 end, [], Incl1)), 341 CoverData#cover{incl_mods=Incl2}. 342 343 344files2mods(Info=#cover{excl_mods=ExclFs, 345 incl_mods=InclFs, 346 cross=Cross}) -> 347 Info#cover{excl_mods=files2mods1(ExclFs), 348 incl_mods=files2mods1(InclFs), 349 cross=[{Tag,files2mods1(Fs)} || {Tag,Fs} <- Cross]}. 350 351files2mods1([M|Fs]) when is_atom(M) -> 352 [M|files2mods1(Fs)]; 353files2mods1([F|Fs]) when is_list(F) -> 354 M = filename:rootname(filename:basename(F)), 355 [list_to_atom(M)|files2mods1(Fs)]; 356files2mods1([]) -> 357 []. 358