%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 1997-2010. 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% %% %%% Purpose : Produces reports in HTML from the outcome of test suite runs. -module(ts_reports). -export([make_index/0, make_master_index/2, make_progress_index/2]). -export([count_cases/1, year/0, current_time/0]). -include_lib("kernel/include/file.hrl"). -include("ts.hrl"). -compile({no_auto_import,[error/1]}). -import(filename, [basename/1, rootname/1]). -import(ts_lib, [error/1]). %% Make master index page which points out index pages for all platforms. make_master_index(Dir, Vars) -> IndexName = filename:join(Dir, "index.html"), {ok, Index0} = make_master_index1(directories(Dir), master_header(Vars)), Index = [Index0|master_footer()], io:put_chars("Updating " ++ IndexName ++ "... "), ok = ts_lib:force_write_file(IndexName, Index), io:put_chars("done\n"). make_master_index1([Dir|Rest], Result) -> NewResult = case catch read_variables(Dir) of {'EXIT',{{bad_installation,Reason},_}} -> io:put_chars("Failed to read " ++ filename:join(Dir,?variables)++ ": " ++ Reason ++ " - Ignoring this directory\n"), Result; Vars -> Platform = ts_lib:var(platform_label, Vars), case make_index(Dir, Vars, false) of {ok, Summary} -> make_master_index(Platform, Dir, Summary, Result); {error, _} -> Result end end, make_master_index1(Rest, NewResult); make_master_index1([], Result) -> {ok, Result}. make_progress_index(Dir, Vars) -> IndexName = filename:join(Dir, "index.html"), io:put_chars("Updating " ++ IndexName ++ "... "), Index0=progress_header(Vars), ts_lib:force_delete(IndexName), Dirs=find_progress_runs(Dir), Index1=[Index0|make_progress_links(Dirs, [])], IndexF=[Index1|progress_footer()], ok = ts_lib:force_write_file(IndexName, IndexF), io:put_chars("done\n"). find_progress_runs(Dir) -> case file:list_dir(Dir) of {ok, Dirs0} -> Dirs1= [filename:join(Dir,X) || X <- Dirs0, filelib:is_dir(filename:join(Dir,X))], lists:sort(Dirs1); _ -> [] end. name_from_vars(Dir, Platform) -> VarFile=filename:join([Dir, Platform, "variables"]), case file:consult(VarFile) of {ok, Vars} -> ts_lib:var(platform_id, Vars); _Other -> Platform end. make_progress_links([], Acc) -> Acc; make_progress_links([RDir|Rest], Acc) -> Dir=filename:basename(RDir), Platforms=[filename:basename(X) || X <- find_progress_runs(RDir)], PlatformLinks=[""++name_from_vars(RDir, X)++"
" || X <- Platforms], LinkName=Dir++"/index.html", Link = [ "\n", "", Dir, "", "\n", "", PlatformLinks, "", "\n" ], make_progress_links(Rest, [Link|Acc]). read_variables(Dir) -> case file:consult(filename:join(Dir, ?variables)) of {ok, Vars} -> Vars; {error, Reason} -> erlang:error({bad_installation,file:format_error(Reason)}, [Dir]) end. make_master_index(Platform, Dirname, {Succ, Fail, UserSkip,AutoSkip}, Result) -> Link = filename:join(filename:basename(Dirname), "index.html"), FailStr = if Fail > 0 -> ["", integer_to_list(Fail),""]; true -> integer_to_list(Fail) end, AutoSkipStr = if AutoSkip > 0 -> ["", integer_to_list(AutoSkip),""]; true -> integer_to_list(AutoSkip) end, [Result, "\n", "", Platform, "", "\n", make_row(integer_to_list(Succ), false), make_row(FailStr, false), make_row(integer_to_list(UserSkip), false), make_row(AutoSkipStr, false), "\n"]. %% Make index page which points out individual test suites for a single platform. make_index() -> {ok, Pwd} = file:get_cwd(), Vars = read_variables(Pwd), make_index(Pwd, Vars, true). make_index(Dir, Vars, IncludeLast) -> IndexName = filename:absname("index.html", Dir), io:put_chars("Updating " ++ IndexName ++ "... "), case catch make_index1(Dir, IndexName, Vars, IncludeLast) of {'EXIT', Reason} -> io:put_chars("CRASHED!\n"), io:format("~p~n", [Reason]), {error, Reason}; {error, Reason} -> io:put_chars("FAILED\n"), io:format("~p~n", [Reason]), {error, Reason}; {ok, Summary} -> io:put_chars("done\n"), {ok, Summary}; Err -> io:format("Unknown internal error. Please report.\n(Err: ~p, ID: 1)", [Err]), {error, Err} end. make_index1(Dir, IndexName, Vars, IncludeLast) -> Logs0 = ts_lib:interesting_logs(Dir), Logs = case IncludeLast of true -> add_last_name(Logs0); false -> Logs0 end, {ok, {Index0, Summary}} = make_index(Logs, header(Vars), 0, 0, 0, 0, 0), Index = [Index0|footer()], case ts_lib:force_write_file(IndexName, Index) of ok -> {ok, Summary}; {error, Reason} -> error({index_write_error, Reason}) end. make_index([Name|Rest], Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt) -> case ts_lib:last_test(Name) of false -> %% Silently skip. make_index(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt); Last -> case count_cases(Last) of {Succ, Fail, USkip, ASkip} -> Cov = case file:read_file(filename:join(Last,?cover_total)) of {ok,Bin} -> TotCoverage = binary_to_term(Bin), io_lib:format("~w %",[TotCoverage]); _error -> "" end, Link = filename:join(basename(Name), basename(Last)), JustTheName = rootname(basename(Name)), NotBuilt = not_built(JustTheName), NewResult = [Result, make_index1(JustTheName, Link, Succ, Fail, USkip, ASkip, NotBuilt, Cov, false)], make_index(Rest, NewResult, TotSucc+Succ, TotFail+Fail, UserSkip+USkip, AutoSkip+ASkip, TotNotBuilt+NotBuilt); error -> make_index(Rest, Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt) end end; make_index([], Result, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt) -> {ok, {[Result|make_index1("Total", no_link, TotSucc, TotFail, UserSkip, AutoSkip, TotNotBuilt, "", true)], {TotSucc, TotFail, UserSkip, AutoSkip}}}. make_index1(SuiteName, Link, Success, Fail, UserSkip, AutoSkip, NotBuilt, Coverage, Bold) -> Name = test_suite_name(SuiteName), FailStr = if Fail > 0 -> ["", integer_to_list(Fail),""]; true -> integer_to_list(Fail) end, AutoSkipStr = if AutoSkip > 0 -> ["", integer_to_list(AutoSkip),""]; true -> integer_to_list(AutoSkip) end, ["\n", "", case Link of no_link -> ["", Name|""]; _Other -> CrashDumpName = SuiteName ++ "_erl_crash.dump", CrashDumpLink = case filelib:is_file(CrashDumpName) of true -> [" (CrashDump)"]; false -> "" end, LogFile = filename:join(Link, ?suitelog_name ++ ".html"), ["", Name, "\n", CrashDumpLink, "\n"] end, make_row(integer_to_list(Success), Bold), make_row(FailStr, Bold), make_row(integer_to_list(UserSkip), Bold), make_row(AutoSkipStr, Bold), make_row(integer_to_list(NotBuilt), Bold), make_row(Coverage, Bold), "\n"]. make_row(Row, true) -> ["", Row|""]; make_row(Row, false) -> ["", Row|""]. not_built(BaseName) -> Dir = filename:join("..", BaseName++"_test"), Erl = length(filelib:wildcard(filename:join(Dir,"*_SUITE.erl"))), Beam = length(filelib:wildcard(filename:join(Dir,"*_SUITE.beam"))), Erl-Beam. %% Add the log file directory for the very last test run (according to %% last_name). add_last_name(Logs) -> case file:read_file("last_name") of {ok, Bin} -> Name = filename:dirname(lib:nonl(binary_to_list(Bin))), case lists:member(Name, Logs) of true -> Logs; false -> [Name|Logs] end; _ -> Logs end. term_to_text(Term) -> lists:flatten(io_lib:format("~p.\n", [Term])). test_suite_name(Name) -> ts_lib:initial_capital(Name) ++ " suite". directories(Dir) -> {ok, Files} = file:list_dir(Dir), [filename:join(Dir, X) || X <- Files, filelib:is_dir(filename:join(Dir, X))]. %%% Headers and footers. header(Vars) -> Platform = ts_lib:var(platform_id, Vars), ["\n" "\n" "\n", "\n", "Test Results for ", Platform, "\n", "\n", body_tag(), "\n", "
\n", "

Test Results for ", Platform, "

\n", "
\n", "\n", "
\n", "\n", "\n", "\n", "\n", "\n" "\n" "\n" "\n" "\n"]. footer() -> ["
FamilySuccessfulFailedUser SkippedAuto SkippedMissing SuitesCoverage
\n" "
\n" "

\n" "
\n" "

\n" "Copyright © ", year(), " Open Telecom Platform
\n" "Updated: ", current_time(), "
\n" "
\n" "

\n" "\n" "\n"]. progress_header(Vars) -> Release = ts_lib:var(erl_release, Vars), ["\n" "\n" "\n", "\n", "", Release, " Progress Test Results\n", "\n", body_tag(), "\n", "
\n", "

", Release, " Progress Test Results

\n", "\n", "\n"]. progress_footer() -> ["
Test RunPlatforms
\n", "
\n", "

\n", "
\n", "

\n", "Copyright © ", year(), " Open Telecom Platform
\n", "Updated: ", current_time(), "
\n", "
\n", "

\n", "\n", "\n"]. master_header(Vars) -> Release = ts_lib:var(erl_release, Vars), Vsn = erlang:system_info(version), ["\n" "\n" "\n", "\n", "", Release, " Test Results (", Vsn, ")\n", "\n", body_tag(), "\n", "
\n", "

", Release, " Test Results (", Vsn, ")

\n", "
\n", "\n", "
\n", "\n", "\n", "\n", "\n", "\n" "\n" "\n"]. master_footer() -> ["
PlatformSuccessfulFailedUser SkippedAuto Skipped
\n", "
\n", "

\n", "
\n", "

\n", "Copyright © ", year(), " Open Telecom Platform
\n", "Updated: ", current_time(), "
\n", "
\n", "

\n", "\n", "\n"]. body_tag() -> "". year() -> {Y, _, _} = date(), integer_to_list(Y). current_time() -> {{Y, Mon, D}, {H, Min, S}} = calendar:local_time(), Weekday = weekday(calendar:day_of_the_week(Y, Mon, D)), lists:flatten(io_lib:format("~s ~s ~p ~2.2.0w:~2.2.0w:~2.2.0w ~w", [Weekday, month(Mon), D, H, Min, S, Y])). weekday(1) -> "Mon"; weekday(2) -> "Tue"; weekday(3) -> "Wed"; weekday(4) -> "Thu"; weekday(5) -> "Fri"; weekday(6) -> "Sat"; weekday(7) -> "Sun". month(1) -> "Jan"; month(2) -> "Feb"; month(3) -> "Mar"; month(4) -> "Apr"; month(5) -> "May"; month(6) -> "Jun"; month(7) -> "Jul"; month(8) -> "Aug"; month(9) -> "Sep"; month(10) -> "Oct"; month(11) -> "Nov"; month(12) -> "Dec". %% Count test cases in the given directory (a directory of the type %% run.1997-08-04_09.58.52). count_cases(Dir) -> SumFile = filename:join(Dir, ?run_summary), case read_summary(SumFile, [summary]) of {ok, [{Succ,Fail,Skip}]} -> {Succ,Fail,Skip,0}; {ok, [Summary]} -> Summary; {error, _} -> LogFile = filename:join(Dir, ?suitelog_name), case file:read_file(LogFile) of {ok, Bin} -> Summary = count_cases1(binary_to_list(Bin), {0, 0, 0, 0}), write_summary(SumFile, Summary), Summary; {error, _Reason} -> io:format("\nFailed to read ~p (skipped)\n", [LogFile]), error end end. write_summary(Name, Summary) -> File = [term_to_text({summary, Summary})], ts_lib:force_write_file(Name, File). % XXX: This function doesn't do what the writer expect. It can't handle % the case if there are several different keys and I had to add a special % case for the empty file. The caller also expect just one tuple as % a result so this function is written way to general for no reason. % But it works sort of. /kgb read_summary(Name, Keys) -> case file:consult(Name) of {ok, []} -> {error, "Empty summary file"}; {ok, Terms} -> {ok, lists:map(fun(Key) -> {value, {_, Value}} = lists:keysearch(Key, 1, Terms), Value end, Keys)}; {error, Reason} -> {error, Reason} end. count_cases1("=failed" ++ Rest, {Success, _Fail, UserSkip,AutoSkip}) -> {NextLine, Count} = get_number(Rest), count_cases1(NextLine, {Success, Count, UserSkip,AutoSkip}); count_cases1("=successful" ++ Rest, {_Success, Fail, UserSkip,AutoSkip}) -> {NextLine, Count} = get_number(Rest), count_cases1(NextLine, {Count, Fail, UserSkip,AutoSkip}); count_cases1("=skipped" ++ Rest, {Success, Fail, _UserSkip,AutoSkip}) -> {NextLine, Count} = get_number(Rest), count_cases1(NextLine, {Success, Fail, Count,AutoSkip}); count_cases1("=user_skipped" ++ Rest, {Success, Fail, _UserSkip,AutoSkip}) -> {NextLine, Count} = get_number(Rest), count_cases1(NextLine, {Success, Fail, Count,AutoSkip}); count_cases1("=auto_skipped" ++ Rest, {Success, Fail, UserSkip,_AutoSkip}) -> {NextLine, Count} = get_number(Rest), count_cases1(NextLine, {Success, Fail, UserSkip,Count}); count_cases1([], Counters) -> Counters; count_cases1(Other, Counters) -> count_cases1(skip_to_nl(Other), Counters). get_number([$\s|Rest]) -> get_number(Rest); get_number([Digit|Rest]) when $0 =< Digit, Digit =< $9 -> get_number(Rest, Digit-$0). get_number([Digit|Rest], Acc) when $0 =< Digit, Digit =< $9 -> get_number(Rest, Acc*10+Digit-$0); get_number([$\n|Rest], Acc) -> {Rest, Acc}; get_number([_|Rest], Acc) -> get_number(Rest, Acc). skip_to_nl([$\n|Rest]) -> Rest; skip_to_nl([_|Rest]) -> skip_to_nl(Rest); skip_to_nl([]) -> [].