%% Copyright (c) 2008-2009 Nick Gerakines %% %% Permission is hereby granted, free of charge, to any person %% obtaining a copy of this software and associated documentation %% files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, %% copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following %% conditions: %% %% The above copyright notice and this permission notice shall be %% included in all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR %% OTHER DEALINGS IN THE SOFTWARE. %% %% @doc A module for creating nice looking code coverage reports. -module(etap_report). -export([create/0]). %% @spec create() -> ok %% @doc Create html code coverage reports for each module that code coverage %% data exists for. create() -> [cover:import(File) || File <- filelib:wildcard("cover/*.coverdata")], Modules = lists:foldl( fun(Module, Acc) -> [{Module, file_report(Module)} | Acc] end, [], cover:imported_modules() ), index(Modules). %% @private index(Modules) -> {ok, IndexFD} = file:open("cover/index.html", [write]), io:format(IndexFD, "", []), io:format(IndexFD, "", []), lists:foldl( fun({Module, {Good, Bad, Source}}, LastRow) -> case {Good + Bad, Source} of {0, _} -> LastRow; {_, none} -> LastRow; _ -> CovPer = round((Good / (Good + Bad)) * 100), UnCovPer = round((Bad / (Good + Bad)) * 100), RowClass = case LastRow of 1 -> "odd"; _ -> "even" end, io:format(IndexFD, "
", [RowClass]), io:format(IndexFD, "~s", [atom_to_list(Module) ++ "_report.html", atom_to_list(Module)]), io:format(IndexFD, "
~p% 
", [CovPer, CovPer, UnCovPer]), io:format(IndexFD, "
", []), case LastRow of 1 -> 0; 0 -> 1 end end end, 0, lists:sort(Modules) ), {TotalGood, TotalBad} = lists:foldl( fun({_, {Good, Bad, Source}}, {TGood, TBad}) -> case Source of none -> {TGood, TBad}; _ -> {TGood + Good, TBad + Bad} end end, {0, 0}, Modules ), io:format(IndexFD, "

Generated on ~s.

~n", [etap:datetime({date(), time()})]), case TotalGood + TotalBad of 0 -> ok; _ -> TotalCovPer = round((TotalGood / (TotalGood + TotalBad)) * 100), TotalUnCovPer = round((TotalBad / (TotalGood + TotalBad)) * 100), io:format(IndexFD, "
", []), io:format(IndexFD, "Total
~p% 
", [TotalCovPer, TotalCovPer, TotalUnCovPer]), io:format(IndexFD, "
", []) end, io:format(IndexFD, "", []), file:close(IndexFD), ok. %% @private file_report(Module) -> {ok, Data} = cover:analyse(Module, calls, line), Source = find_source(Module), {Good, Bad} = collect_coverage(Data, {0, 0}), case {Source, Good + Bad} of {none, _} -> ok; {_, 0} -> ok; _ -> {ok, SourceFD} = file:open(Source, [read]), {ok, WriteFD} = file:open("cover/" ++ atom_to_list(Module) ++ "_report.html", [write]), io:format(WriteFD, "~s", [header(Module, Good, Bad)]), output_lines(Data, WriteFD, SourceFD, 1), io:format(WriteFD, "~s", [footer()]), file:close(WriteFD), file:close(SourceFD), ok end, {Good, Bad, Source}. %% @private collect_coverage([], Acc) -> Acc; collect_coverage([{{_, _}, 0} | Data], {Good, Bad}) -> collect_coverage(Data, {Good, Bad + 1}); collect_coverage([_ | Data], {Good, Bad}) -> collect_coverage(Data, {Good + 1, Bad}). %% @private output_lines(Data, WriteFD, SourceFD, LineNumber) -> {Match, NextData} = datas_match(Data, LineNumber), case io:get_line(SourceFD, '') of eof -> ok; Line = "%% @todo" ++ _ -> io:format(WriteFD, "~s", [out_line(LineNumber, highlight, Line)]), output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); Line = "% " ++ _ -> io:format(WriteFD, "~s", [out_line(LineNumber, none, Line)]), output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); Line -> case Match of {true, CC} -> io:format(WriteFD, "~s", [out_line(LineNumber, CC, Line)]), output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); false -> io:format(WriteFD, "~s", [out_line(LineNumber, none, Line)]), output_lines(NextData, WriteFD, SourceFD, LineNumber + 1) end end. %% @private out_line(Number, none, Line) -> PadNu = string:right(integer_to_list(Number), 5, $.), io_lib:format("~s ~s", [Number, PadNu, Line]); out_line(Number, highlight, Line) -> PadNu = string:right(integer_to_list(Number), 5, $.), io_lib:format("~s ~s", [Number, PadNu, Line]); out_line(Number, 0, Line) -> PadNu = string:right(integer_to_list(Number), 5, $.), io_lib:format("~s ~s", [Number, PadNu, Line]); out_line(Number, _, Line) -> PadNu = string:right(integer_to_list(Number), 5, $.), io_lib:format("~s ~s", [Number, PadNu, Line]). %% @private datas_match([], _) -> {false, []}; datas_match([{{_, Line}, CC} | Datas], LineNumber) when Line == LineNumber -> {{true, CC}, Datas}; datas_match(Data, _) -> {false, Data}. %% @private find_source(Module) when is_atom(Module) -> Root = filename:rootname(Module), Dir = filename:dirname(Root), XDir = case os:getenv("SRC") of false -> "src"; X -> X end, find_source([ filename:join([Dir, Root ++ ".erl"]), filename:join([Dir, "..", "src", Root ++ ".erl"]), filename:join([Dir, "src", Root ++ ".erl"]), filename:join([Dir, "elibs", Root ++ ".erl"]), filename:join([Dir, "..", "elibs", Root ++ ".erl"]), filename:join([Dir, XDir, Root ++ ".erl"]) ]); find_source([]) -> none; find_source([Test | Tests]) -> case filelib:is_file(Test) of true -> Test; false -> find_source(Tests) end. %% @private header(Module, Good, Bad) -> io:format("Good ~p~n", [Good]), io:format("Bad ~p~n", [Bad]), CovPer = round((Good / (Good + Bad)) * 100), UnCovPer = round((Bad / (Good + Bad)) * 100), io:format("CovPer ~p~n", [CovPer]), io_lib:format(" ~s - C0 code coverage information

C0 code coverage information

Generated on ~s with etap 0.3.4.

Name Total lines Lines of code Total coverage Code coverage
~s ?? ?? ??
~p% 
", [Module, etap:datetime({date(), time()}), atom_to_list(Module) ++ "_report.html", Module, CovPer, CovPer, UnCovPer]).

%% @private
footer() ->
    "

Generated using etap 0.3.4.

".