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