1%% Copyright 2014 Sean Cribbs 2%% 3%% Licensed under the Apache License, Version 2.0 (the "License"); 4%% you may not use this file except in compliance with the License. 5%% You may obtain a copy of the License at 6%% 7%% http://www.apache.org/licenses/LICENSE-2.0 8%% 9%% Unless required by applicable law or agreed to in writing, software 10%% distributed under the License is distributed on an "AS IS" BASIS, 11%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12%% See the License for the specific language governing permissions and 13%% limitations under the License. 14 15 16%% @doc A listener/reporter for eunit that prints '.' for each 17%% success, 'F' for each failure, and 'E' for each error. It can also 18%% optionally summarize the failures at the end. 19-module(eunit_progress). 20-behaviour(eunit_listener). 21-define(NOTEST, true). 22-include_lib("eunit/include/eunit.hrl"). 23 24-define(RED, "\e[0;31m"). 25-define(GREEN, "\e[0;32m"). 26-define(YELLOW, "\e[0;33m"). 27-define(WHITE, "\e[0;37m"). 28-define(CYAN, "\e[0;36m"). 29-define(RESET, "\e[0m"). 30 31%% eunit_listener callbacks 32-export([ 33 init/1, 34 handle_begin/3, 35 handle_end/3, 36 handle_cancel/3, 37 terminate/2 38 ]). 39 40-export([ 41 start/0, 42 start/1 43 ]). 44 45-ifdef(namespaced_dicts). 46-type euf_dict() :: dict:dict(). 47-else. 48-type euf_dict() :: dict(). 49-endif. 50 51-record(state, { 52 status = dict:new() :: euf_dict(), 53 failures = [] :: [[pos_integer()]], 54 skips = [] :: [[pos_integer()]], 55 timings = binomial_heap:new() :: binomial_heap:binomial_heap(), 56 colored = true :: boolean(), 57 profile = false :: boolean() 58 }). 59 60%% Startup 61start() -> 62 start([]). 63 64start(Options) -> 65 eunit_listener:start(?MODULE, Options). 66 67%%------------------------------------------ 68%% eunit_listener callbacks 69%%------------------------------------------ 70init(Options) -> 71 #state{colored=proplists:get_bool(colored, Options), 72 profile=proplists:get_bool(profile, Options)}. 73 74handle_begin(group, Data, St) -> 75 GID = proplists:get_value(id, Data), 76 Dict = St#state.status, 77 St#state{status=dict:store(GID, orddict:from_list([{type, group}|Data]), Dict)}; 78handle_begin(test, Data, St) -> 79 TID = proplists:get_value(id, Data), 80 Dict = St#state.status, 81 St#state{status=dict:store(TID, orddict:from_list([{type, test}|Data]), Dict)}. 82 83handle_end(group, Data, St) -> 84 St#state{status=merge_on_end(Data, St#state.status)}; 85handle_end(test, Data, St) -> 86 NewStatus = merge_on_end(Data, St#state.status), 87 St1 = print_progress(Data, St), 88 St2 = record_timing(Data, St1), 89 St2#state{status=NewStatus}. 90 91handle_cancel(_, Data, #state{status=Status, skips=Skips}=St) -> 92 Status1 = merge_on_end(Data, Status), 93 ID = proplists:get_value(id, Data), 94 St#state{status=Status1, skips=[ID|Skips]}. 95 96terminate({ok, Data}, St) -> 97 print_failures(St), 98 print_pending(St), 99 print_profile(St), 100 print_timing(St), 101 print_results(Data, St); 102terminate({error, Reason}, St) -> 103 io:nl(), io:nl(), 104 print_colored(io_lib:format("Eunit failed: ~25p~n", [Reason]), ?RED, St), 105 sync_end(error). 106 107sync_end(Result) -> 108 receive 109 {stop, Reference, ReplyTo} -> 110 ReplyTo ! {result, Reference, Result}, 111 ok 112 end. 113 114%%------------------------------------------ 115%% Print and collect information during run 116%%------------------------------------------ 117print_progress(Data, St) -> 118 TID = proplists:get_value(id, Data), 119 case proplists:get_value(status, Data) of 120 ok -> 121 print_progress_success(St), 122 St; 123 {skipped, _Reason} -> 124 print_progress_skipped(St), 125 St#state{skips=[TID|St#state.skips]}; 126 {error, Exception} -> 127 print_progress_failed(Exception, St), 128 St#state{failures=[TID|St#state.failures]} 129 end. 130 131record_timing(Data, State=#state{timings=T, profile=true}) -> 132 TID = proplists:get_value(id, Data), 133 case lists:keyfind(time, 1, Data) of 134 {time, Int} -> 135 %% It's a min-heap, so we insert negative numbers instead 136 %% of the actuals and normalize when we report on them. 137 T1 = binomial_heap:insert(-Int, TID, T), 138 State#state{timings=T1}; 139 false -> 140 State 141 end; 142record_timing(_Data, State) -> 143 State. 144 145print_progress_success(St) -> 146 print_colored(".", ?GREEN, St). 147 148print_progress_skipped(St) -> 149 print_colored("*", ?YELLOW, St). 150 151print_progress_failed(_Exc, St) -> 152 print_colored("F", ?RED, St). 153 154merge_on_end(Data, Dict) -> 155 ID = proplists:get_value(id, Data), 156 dict:update(ID, 157 fun(Old) -> 158 orddict:merge(fun merge_data/3, Old, orddict:from_list(Data)) 159 end, Dict). 160 161merge_data(_K, undefined, X) -> X; 162merge_data(_K, X, undefined) -> X; 163merge_data(_K, _, X) -> X. 164 165%%------------------------------------------ 166%% Print information at end of run 167%%------------------------------------------ 168print_failures(#state{failures=[]}) -> 169 ok; 170print_failures(#state{failures=Fails}=State) -> 171 io:nl(), 172 io:fwrite("Failures:~n~n",[]), 173 lists:foldr(print_failure_fun(State), 1, Fails), 174 ok. 175 176print_failure_fun(#state{status=Status}=State) -> 177 fun(Key, Count) -> 178 TestData = dict:fetch(Key, Status), 179 TestId = format_test_identifier(TestData), 180 io:fwrite(" ~p) ~ts~n", [Count, TestId]), 181 print_failure_reason(proplists:get_value(status, TestData), 182 proplists:get_value(output, TestData), 183 State), 184 io:nl(), 185 Count + 1 186 end. 187 188print_failure_reason({skipped, Reason}, _Output, State) -> 189 print_colored(io_lib:format(" ~ts~n", [format_pending_reason(Reason)]), 190 ?RED, State); 191print_failure_reason({error, {_Class, Term, Stack}}, Output, State) when 192 is_tuple(Term), tuple_size(Term) == 2, is_list(element(2, Term)) -> 193 print_assertion_failure(Term, Stack, Output, State), 194 print_failure_output(5, Output, State); 195print_failure_reason({error, Reason}, Output, State) -> 196 print_colored(indent(5, "Failure/Error: ~p~n", [Reason]), ?RED, State), 197 print_failure_output(5, Output, State). 198 199print_failure_output(_, <<>>, _) -> ok; 200print_failure_output(_, undefined, _) -> ok; 201print_failure_output(Indent, Output, State) -> 202 print_colored(indent(Indent, "Output: ~ts", [Output]), ?CYAN, State). 203 204print_assertion_failure({Type, Props}, Stack, Output, State) -> 205 FailureDesc = format_assertion_failure(Type, Props, 5), 206 {M,F,A,Loc} = lists:last(Stack), 207 LocationText = io_lib:format(" %% ~ts:~p:in `~ts`", [proplists:get_value(file, Loc), 208 proplists:get_value(line, Loc), 209 format_function_name(M,F,A)]), 210 print_colored(FailureDesc, ?RED, State), 211 io:nl(), 212 print_colored(LocationText, ?CYAN, State), 213 io:nl(), 214 print_failure_output(5, Output, State), 215 io:nl(). 216 217print_pending(#state{skips=[]}) -> 218 ok; 219print_pending(#state{status=Status, skips=Skips}=State) -> 220 io:nl(), 221 io:fwrite("Pending:~n", []), 222 lists:foreach(fun(ID) -> 223 Info = dict:fetch(ID, Status), 224 case proplists:get_value(reason, Info) of 225 undefined -> 226 ok; 227 Reason -> 228 print_pending_reason(Reason, Info, State) 229 end 230 end, lists:reverse(Skips)), 231 io:nl(). 232 233print_pending_reason(Reason0, Data, State) -> 234 Text = case proplists:get_value(type, Data) of 235 group -> 236 io_lib:format(" ~ts~n", [proplists:get_value(desc, Data)]); 237 test -> 238 io_lib:format(" ~ts~n", [format_test_identifier(Data)]) 239 end, 240 Reason = io_lib:format(" %% ~ts~n", [format_pending_reason(Reason0)]), 241 print_colored(Text, ?YELLOW, State), 242 print_colored(Reason, ?CYAN, State). 243 244print_profile(#state{timings=T, status=Status, profile=true}=State) -> 245 TopN = binomial_heap:take(10, T), 246 TopNTime = abs(lists:sum([ Time || {Time, _} <- TopN ])), 247 TLG = dict:fetch([], Status), 248 TotalTime = proplists:get_value(time, TLG), 249 if TotalTime =/= undefined andalso TotalTime > 0 andalso TopN =/= [] -> 250 TopNPct = (TopNTime / TotalTime) * 100, 251 io:nl(), io:nl(), 252 io:fwrite("Top ~p slowest tests (~ts, ~.1f% of total time):", [length(TopN), format_time(TopNTime), TopNPct]), 253 lists:foreach(print_timing_fun(State), TopN), 254 io:nl(); 255 true -> ok 256 end; 257print_profile(#state{profile=false}) -> 258 ok. 259 260print_timing(#state{status=Status}) -> 261 TLG = dict:fetch([], Status), 262 Time = proplists:get_value(time, TLG), 263 io:nl(), 264 io:fwrite("Finished in ~ts~n", [format_time(Time)]), 265 ok. 266 267print_results(Data, State) -> 268 Pass = proplists:get_value(pass, Data, 0), 269 Fail = proplists:get_value(fail, Data, 0), 270 Skip = proplists:get_value(skip, Data, 0), 271 Cancel = proplists:get_value(cancel, Data, 0), 272 Total = Pass + Fail + Skip + Cancel, 273 {Color, Result} = if Fail > 0 -> {?RED, error}; 274 Skip > 0; Cancel > 0 -> {?YELLOW, error}; 275 Pass =:= 0 -> {?YELLOW, ok}; 276 true -> {?GREEN, ok} 277 end, 278 print_results(Color, Total, Fail, Skip, Cancel, State), 279 sync_end(Result). 280 281print_results(Color, 0, _, _, _, State) -> 282 print_colored(Color, "0 tests\n", State); 283print_results(Color, Total, Fail, Skip, Cancel, State) -> 284 SkipText = format_optional_result(Skip, "skipped"), 285 CancelText = format_optional_result(Cancel, "cancelled"), 286 Text = io_lib:format("~p tests, ~p failures~ts~ts~n", [Total, Fail, SkipText, CancelText]), 287 print_colored(Text, Color, State). 288 289print_timing_fun(#state{status=Status}=State) -> 290 fun({Time, Key}) -> 291 TestData = dict:fetch(Key, Status), 292 TestId = format_test_identifier(TestData), 293 io:nl(), 294 io:fwrite(" ~ts~n", [TestId]), 295 print_colored([" "|format_time(abs(Time))], ?CYAN, State) 296 end. 297 298%%------------------------------------------ 299%% Print to the console with the given color 300%% if enabled. 301%%------------------------------------------ 302print_colored(Text, Color, #state{colored=true}) -> 303 io:fwrite("~s~ts~s", [Color, Text, ?RESET]); 304print_colored(Text, _Color, #state{colored=false}) -> 305 io:fwrite("~ts", [Text]). 306 307%%------------------------------------------ 308%% Generic data formatters 309%%------------------------------------------ 310format_function_name(M, F, A) -> 311 io_lib:format("~ts:~ts/~p", [M, F, A]). 312 313format_optional_result(0, _) -> 314 []; 315format_optional_result(Count, Text) -> 316 io_lib:format(", ~p ~ts", [Count, Text]). 317 318format_test_identifier(Data) -> 319 {Mod, Fun, Arity} = proplists:get_value(source, Data), 320 Line = case proplists:get_value(line, Data) of 321 0 -> ""; 322 L -> io_lib:format(":~p", [L]) 323 end, 324 Desc = case proplists:get_value(desc, Data) of 325 undefined -> ""; 326 DescText -> io_lib:format(": ~ts", [DescText]) 327 end, 328 io_lib:format("~ts~ts~ts", [format_function_name(Mod, Fun, Arity), Line, Desc]). 329 330format_time(undefined) -> 331 "? seconds"; 332format_time(Time) -> 333 io_lib:format("~.3f seconds", [Time / 1000]). 334 335format_pending_reason({module_not_found, M}) -> 336 io_lib:format("Module '~ts' missing", [M]); 337format_pending_reason({no_such_function, {M,F,A}}) -> 338 io_lib:format("Function ~ts undefined", [format_function_name(M,F,A)]); 339format_pending_reason({exit, Reason}) -> 340 io_lib:format("Related process exited with reason: ~p", [Reason]); 341format_pending_reason(Reason) -> 342 io_lib:format("Unknown error: ~p", [Reason]). 343 344%% @doc Formats all the known eunit assertions, you're on your own if 345%% you make an assertion yourself. 346format_assertion_failure(Type, Props, I) when Type =:= assertion_failed 347 ; Type =:= assert -> 348 Keys = proplists:get_keys(Props), 349 HasEUnitProps = ([expression, value] -- Keys) =:= [], 350 HasHamcrestProps = ([expected, actual, matcher] -- Keys) =:= [], 351 if 352 HasEUnitProps -> 353 [indent(I, "Failure/Error: ?assert(~ts)~n", [proplists:get_value(expression, Props)]), 354 indent(I, " expected: true~n", []), 355 case proplists:get_value(value, Props) of 356 false -> 357 indent(I, " got: false", []); 358 {not_a_boolean, V} -> 359 indent(I, " got: ~p", [V]) 360 end]; 361 HasHamcrestProps -> 362 [indent(I, "Failure/Error: ?assertThat(~p)~n", [proplists:get_value(matcher, Props)]), 363 indent(I, " expected: ~p~n", [proplists:get_value(expected, Props)]), 364 indent(I, " got: ~p", [proplists:get_value(actual, Props)])]; 365 true -> 366 [indent(I, "Failure/Error: unknown assert: ~p", [Props])] 367 end; 368 369format_assertion_failure(Type, Props, I) when Type =:= assertMatch_failed 370 ; Type =:= assertMatch -> 371 Expr = proplists:get_value(expression, Props), 372 Pattern = proplists:get_value(pattern, Props), 373 Value = proplists:get_value(value, Props), 374 [indent(I, "Failure/Error: ?assertMatch(~ts, ~ts)~n", [Pattern, Expr]), 375 indent(I, " expected: = ~ts~n", [Pattern]), 376 indent(I, " got: ~p", [Value])]; 377 378format_assertion_failure(Type, Props, I) when Type =:= assertNotMatch_failed 379 ; Type =:= assertNotMatch -> 380 Expr = proplists:get_value(expression, Props), 381 Pattern = proplists:get_value(pattern, Props), 382 Value = proplists:get_value(value, Props), 383 [indent(I, "Failure/Error: ?assertNotMatch(~ts, ~ts)~n", [Pattern, Expr]), 384 indent(I, " expected not: = ~ts~n", [Pattern]), 385 indent(I, " got: ~p", [Value])]; 386 387format_assertion_failure(Type, Props, I) when Type =:= assertEqual_failed 388 ; Type =:= assertEqual -> 389 Expr = proplists:get_value(expression, Props), 390 Expected = proplists:get_value(expected, Props), 391 Value = proplists:get_value(value, Props), 392 [indent(I, "Failure/Error: ?assertEqual(~w, ~ts)~n", [Expected, 393 Expr]), 394 indent(I, " expected: ~p~n", [Expected]), 395 indent(I, " got: ~p", [Value])]; 396 397format_assertion_failure(Type, Props, I) when Type =:= assertNotEqual_failed 398 ; Type =:= assertNotEqual -> 399 Expr = proplists:get_value(expression, Props), 400 Value = proplists:get_value(value, Props), 401 [indent(I, "Failure/Error: ?assertNotEqual(~p, ~ts)~n", 402 [Value, Expr]), 403 indent(I, " expected not: == ~p~n", [Value]), 404 indent(I, " got: ~p", [Value])]; 405 406format_assertion_failure(Type, Props, I) when Type =:= assertException_failed 407 ; Type =:= assertException -> 408 Expr = proplists:get_value(expression, Props), 409 Pattern = proplists:get_value(pattern, Props), 410 {Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DATA 411 [indent(I, "Failure/Error: ?assertException(~ts, ~ts, ~ts)~n", [Class, Term, Expr]), 412 case proplists:is_defined(unexpected_success, Props) of 413 true -> 414 [indent(I, " expected: exception ~ts but nothing was raised~n", [Pattern]), 415 indent(I, " got: value ~p", [proplists:get_value(unexpected_success, Props)])]; 416 false -> 417 Ex = proplists:get_value(unexpected_exception, Props), 418 [indent(I, " expected: exception ~ts~n", [Pattern]), 419 indent(I, " got: exception ~p", [Ex])] 420 end]; 421 422format_assertion_failure(Type, Props, I) when Type =:= assertNotException_failed 423 ; Type =:= assertNotException -> 424 Expr = proplists:get_value(expression, Props), 425 Pattern = proplists:get_value(pattern, Props), 426 {Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DAT 427 Ex = proplists:get_value(unexpected_exception, Props), 428 [indent(I, "Failure/Error: ?assertNotException(~ts, ~ts, ~ts)~n", [Class, Term, Expr]), 429 indent(I, " expected not: exception ~ts~n", [Pattern]), 430 indent(I, " got: exception ~p", [Ex])]; 431 432format_assertion_failure(Type, Props, I) when Type =:= command_failed 433 ; Type =:= command -> 434 Cmd = proplists:get_value(command, Props), 435 Expected = proplists:get_value(expected_status, Props), 436 Status = proplists:get_value(status, Props), 437 [indent(I, "Failure/Error: ?cmdStatus(~p, ~p)~n", [Expected, Cmd]), 438 indent(I, " expected: status ~p~n", [Expected]), 439 indent(I, " got: status ~p", [Status])]; 440 441format_assertion_failure(Type, Props, I) when Type =:= assertCmd_failed 442 ; Type =:= assertCmd -> 443 Cmd = proplists:get_value(command, Props), 444 Expected = proplists:get_value(expected_status, Props), 445 Status = proplists:get_value(status, Props), 446 [indent(I, "Failure/Error: ?assertCmdStatus(~p, ~p)~n", [Expected, Cmd]), 447 indent(I, " expected: status ~p~n", [Expected]), 448 indent(I, " got: status ~p", [Status])]; 449 450format_assertion_failure(Type, Props, I) when Type =:= assertCmdOutput_failed 451 ; Type =:= assertCmdOutput -> 452 Cmd = proplists:get_value(command, Props), 453 Expected = proplists:get_value(expected_output, Props), 454 Output = proplists:get_value(output, Props), 455 [indent(I, "Failure/Error: ?assertCmdOutput(~p, ~p)~n", [Expected, Cmd]), 456 indent(I, " expected: ~p~n", [Expected]), 457 indent(I, " got: ~p", [Output])]; 458 459format_assertion_failure(Type, Props, I) -> 460 indent(I, "~p", [{Type, Props}]). 461 462indent(I, Fmt, Args) -> 463 io_lib:format("~" ++ integer_to_list(I) ++ "s" ++ Fmt, [" "|Args]). 464 465extract_exception_pattern(Str) -> 466 ["{", Class, Term|_] = re:split(Str, "[, ]{1,2}", [unicode,{return,list}]), 467 {Class, Term}. 468