├── .gitignore ├── LICENSE ├── README.md ├── rebar.config ├── rebar.config.script ├── rebar.lock ├── src ├── cth_readable.app.src ├── cth_readable_compact_shell.erl ├── cth_readable_failonly.erl ├── cth_readable_helpers.erl ├── cth_readable_lager_backend.erl ├── cth_readable_nosasl.erl ├── cth_readable_shell.erl ├── cth_readable_transform.erl └── cthr.erl └── test ├── failonly_SUITE.erl ├── log_tests.erl ├── macro_wrap.hrl ├── sample_SUITE.erl └── show_logs_SUITE.erl /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | _rel 15 | _deps 16 | _plugins 17 | _tdeps 18 | logs 19 | _build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Fred Hebert . 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * The names of its contributors may not be used to endorse or promote 16 | products derived from this software without specific prior written 17 | permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `cth_readable` 2 | 3 | An OTP library to be used for CT log outputs you want to be readable 4 | around all that noise they contain. 5 | 6 | There are currently the following hooks: 7 | 8 | 1. `cth_readable_shell`, which shows failure stacktraces in the shell and 9 | otherwise shows successes properly, in color. 10 | 2. `cth_readable_compact_shell`, which is similar to the previous ones, but 11 | only ouputs a period (`.`) for each successful test 12 | 3. `cth_readable_failonly`, which only outputs error and SASL logs to the 13 | shell in case of failures. It also provides `cthr:pal/1-4` functions, 14 | working like `ct:pal/1-4`, but being silenceable by that hook. A parse 15 | transform exists to automatically convert `ct:pal/1-3` into `cthr:pal/1-3`. 16 | Also automatically handles lager. This hook buffers the IO/logging events, 17 | and the buffer size can be limited with the `max_events` config option. The 18 | default value is `inf` which means that all events are buffered. 19 | 4. `cth_readable_nosasl`, which disables all SASL logging. It however requires 20 | to be run *before* `cth_readable_failonly` to work. 21 | 22 | ## What it looks like 23 | 24 | ![example](http://i.imgur.com/dDFNxZr.png) 25 | ![example](http://i.imgur.com/RXZBG7H.png) 26 | 27 | ## Usage with rebar3 28 | 29 | Supported and enabled by default. 30 | 31 | ## Usage with rebar2.x 32 | 33 | Add the following to your `rebar.config`: 34 | 35 | ```erlang 36 | {deps, [ 37 | {cth_readable, {git, "https://github.com/ferd/cth_readable.git", {tag, "v1.6.0"}}} 38 | ]}. 39 | 40 | {ct_compile_opts, [{parse_transform, cth_readable_transform}]}. 41 | {ct_opts, [{ct_hooks, [cth_readable_failonly, cth_readable_shell]}]}. 42 | %% Or add limitations to how many messages are buffered with: 43 | %% {ct_opts, [{ct_hooks, [{cth_readable_failonly, [{max_events, 1000}]}, cth_readable_shell]}]}. 44 | ``` 45 | 46 | ## Usage with lager 47 | 48 | If your lager handler has a custom formatter and you want that formatter 49 | to take effect, rather than using a configuration such as: 50 | 51 | ```erlang 52 | {lager, [ 53 | {handlers, [{lager_console_backend, 54 | [info, {custom_formatter, [{app, "some-val"}]}]} 55 | ]} 56 | ]}. 57 | ``` 58 | 59 | Use: 60 | 61 | ```erlang 62 | {lager, [ 63 | {handlers, [{cth_readable_lager_backend, 64 | [info, {custom_formatter, [{app, "some-val"}]}]} 65 | ]} 66 | ]}. 67 | ``` 68 | 69 | It will let you have both proper formatting and support for arbitrary 70 | configurations. 71 | 72 | ## Changelog 73 | 74 | 1.6.1: 75 | - Cleaning up some code for OTP-28, mostly around type usage 76 | 77 | 1.6.0: 78 | - Adding support for less verbose test skipping (thanks @paulo-ferraz-oliveira) 79 | 80 | 1.5.1: 81 | - Adding support for `cthr:pal/5` (thanks @ashleyjlive) 82 | 83 | 1.5.0: 84 | - Adding an optional bound buffer in `cth_readable_failonly` (thanks @TheGeorge) 85 | - (published to hex but never to github, ended up with a messy commit tree) 86 | 87 | 1.4.9: 88 | - No change, re-pushing the hex.pm package since it had an untracked dependency somehow 89 | 90 | 1.4.8: 91 | - Fixed handling of comments in EUnit macros 92 | 93 | 1.4.7: 94 | - Fixed handling of the result of an `?assertNot()` macro 95 | 96 | 1.4.6: 97 | - Reloading formatter config for logs after each test where the information needs to be printed 98 | 99 | 1.4.5: 100 | - Restoring proper logs for Lager in OTP-21+. A problem existed when `error_logger` was no longer registered by default and lager log lines would silently get lost. 101 | 102 | 1.4.4: 103 | - Better interactions with Lager; since newer releases, it removes the Logger default interface when starting, which could cause crashes when this happened before the CT hooks would start (i.e. a eunit suite) 104 | 105 | 1.4.3: 106 | - OTP-21.2 support (Logger interface); importing a function that was de-exported by OTP team 107 | 108 | 1.4.2: 109 | - OTP-21.0 support (Logger interface) 110 | 111 | 1.4.1: 112 | - OTP-21-rc2 support (Logger interface); dropping rc1 support. 113 | 114 | 1.4.0: 115 | - OTP-21-rc1 support (Logger interface) 116 | - Add compact shell output handler 117 | 118 | 1.3.4: 119 | - Restore proper eunit assertion formatting 120 | 121 | 1.3.3: 122 | - More fixes due to lager old default config formats 123 | 124 | 1.3.2: 125 | - Fix deprecation warning on newer lagers due to old default config format 126 | 127 | 1.3.1: 128 | - Unicode support and OTP-21 readiness. 129 | 130 | 1.3.0: 131 | - display groups in test output. Thanks to @egobrain for the contribution 132 | 133 | 1.2.6: 134 | - report `end_per_testcase` errors as a non-critical failure when the test case passes 135 | - add in a (voluntarily failing) test suite to demo multiple output cases required 136 | 137 | 1.2.5: 138 | - support for `on_tc_skip/4` to fully prevent misreporting of skipped suites 139 | 140 | 1.2.4: 141 | - unset suite name at the end of hooks run to prevent misreporting 142 | 143 | 1.2.3: 144 | - correct `syntax_lib` to `syntax_tools` as an app dependency 145 | 146 | 1.2.2: 147 | - fix output for assertions 148 | 149 | 1.2.1: 150 | - handle failures of parse transforms by just ignoring the culprit files. 151 | 152 | 1.2.0: 153 | - move to `cf` library for color output, adding support for 'dumb' terminals 154 | 155 | 1.1.1: 156 | - fix typo of `poplist -> proplist`, thanks to @egobrain 157 | 158 | 1.1.0: 159 | - support for better looking EUnit logs 160 | - support for lager backends logging to HTML files 161 | 162 | 1.0.1: 163 | - support for CT versions in Erlang copies older than R16 164 | 165 | 1.0.0: 166 | - initial stable release 167 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [cf]}. 2 | 3 | {ct_opts, [ 4 | {ct_hooks, [cth_readable_failonly, cth_readable_shell]} 5 | ]}. 6 | 7 | {ct_compile_opts, [ 8 | {parse_transform, cth_readable_transform} 9 | ]}. 10 | {eunit_compile_opts, [ % to avoid 'do eunit, ct' eating up the parse transform 11 | {parse_transform, cth_readable_transform} 12 | ]}. 13 | 14 | {erl_opts, [{platform_define, "^(R|1|20)", no_logger_hrl}]}. 15 | 16 | {profiles, [ 17 | {test, [ 18 | {deps, [{lager, "3.9.2"}]}, 19 | {erl_opts, [debug_info, nowarn_export_all]} 20 | ]} 21 | ]}. 22 | 23 | {dialyzer, [ 24 | {warnings, [no_unknown]} 25 | ]}. 26 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | case erlang:function_exported(rebar3, main, 1) of 2 | true -> % rebar3 3 | CONFIG; 4 | false -> % rebar 2.x or older 5 | %% Rebuild deps, possibly including those that have been moved to 6 | %% profiles 7 | [{deps, [ 8 | {cf, ".*", {git, "https://github.com/project-fifo/cf.git", "a6b3957"}} 9 | ]} | lists:keydelete(deps, 1, CONFIG)] 10 | end. 11 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.2.0", 2 | [{<<"cf">>,{pkg,<<"cf">>,<<"0.2.1">>},0}]}. 3 | [ 4 | {pkg_hash,[ 5 | {<<"cf">>, <<"69D0B1349FD4D7D4DC55B7F407D29D7A840BF9A1EF5AF529F1EBE0CE153FC2AB">>}]}, 6 | {pkg_hash_ext,[ 7 | {<<"cf">>, <<"BAEE9AA7EC2DFA3CB4486B67211177CAA293F876780F0B313B45718EDEF6A0A5">>}]} 8 | ]. 9 | -------------------------------------------------------------------------------- /src/cth_readable.app.src: -------------------------------------------------------------------------------- 1 | {application, cth_readable, 2 | [{description, "Common Test hooks for more readable logs"}, 3 | {vsn, "1.6.1"}, 4 | {registered, [cth_readable_failonly, cth_readable_logger]}, 5 | {applications, 6 | [kernel, 7 | stdlib, 8 | syntax_tools, 9 | common_test, 10 | cf 11 | ]}, 12 | {env,[]}, 13 | {modules, []}, 14 | 15 | {licenses, ["BSD"]}, 16 | {links, [{"Github", "https://github.com/ferd/cth_readable"}]} 17 | ]}. 18 | -------------------------------------------------------------------------------- /src/cth_readable_compact_shell.erl: -------------------------------------------------------------------------------- 1 | -module(cth_readable_compact_shell). 2 | -import(cth_readable_helpers, [format_path/2, colorize/2, maybe_eunit_format/1]). 3 | 4 | -define(OKC, green). 5 | -define(FAILC, red). 6 | -define(SKIPC, magenta). 7 | 8 | 9 | %% Callbacks 10 | -export([id/1]). 11 | -export([init/2]). 12 | 13 | -export([pre_init_per_suite/3]). 14 | -export([post_init_per_suite/4]). 15 | -export([pre_end_per_suite/3]). 16 | -export([post_end_per_suite/4]). 17 | 18 | -export([pre_init_per_group/3]). 19 | -export([post_init_per_group/4]). 20 | -export([pre_end_per_group/3]). 21 | -export([post_end_per_group/4]). 22 | 23 | -export([pre_init_per_testcase/3]). 24 | -export([post_end_per_testcase/4]). 25 | 26 | -export([on_tc_fail/3]). 27 | -export([on_tc_skip/3, on_tc_skip/4]). 28 | 29 | -export([terminate/1]). 30 | 31 | -record(state, {id, suite, groups, opts}). 32 | 33 | %% @doc Return a unique id for this CTH. 34 | id(_Opts) -> 35 | {?MODULE, make_ref()}. 36 | 37 | %% @doc Always called before any other callback function. Use this to initiate 38 | %% any common state. 39 | init(Id, Opts) -> 40 | {ok, #state{id=Id, opts=Opts}}. 41 | 42 | %% @doc Called before init_per_suite is called. 43 | pre_init_per_suite(Suite,Config,State) -> 44 | io:format(user, "%%% ~p: ", [Suite]), 45 | {Config, State#state{suite=Suite, groups=[]}}. 46 | 47 | %% @doc Called after init_per_suite. 48 | post_init_per_suite(_Suite,_Config,Return,State) -> 49 | {Return, State}. 50 | 51 | %% @doc Called before end_per_suite. 52 | pre_end_per_suite(_Suite,Config,State) -> 53 | {Config, State}. 54 | 55 | %% @doc Called after end_per_suite. 56 | post_end_per_suite(_Suite,_Config,Return,State) -> 57 | io:format(user, "~n", []), 58 | {Return, State#state{suite=undefined, groups=[]}}. 59 | 60 | %% @doc Called before each init_per_group. 61 | pre_init_per_group(_Group,Config,State) -> 62 | {Config, State}. 63 | 64 | %% @doc Called after each init_per_group. 65 | post_init_per_group(Group,_Config,Return, State=#state{groups=Groups}) -> 66 | {Return, State#state{groups=[Group|Groups]}}. 67 | 68 | %% @doc Called after each end_per_group. 69 | pre_end_per_group(_Group,Config,State) -> 70 | {Config, State}. 71 | 72 | %% @doc Called after each end_per_group. 73 | post_end_per_group(_Group,_Config,Return, State=#state{groups=Groups}) -> 74 | {Return, State#state{groups=tl(Groups)}}. 75 | 76 | %% @doc Called before each test case. 77 | pre_init_per_testcase(_TC,Config,State) -> 78 | {Config, State}. 79 | 80 | %% @doc Called after each test case. 81 | post_end_per_testcase(TC,_Config,ok,State=#state{suite=Suite, groups=Groups}) -> 82 | format_ok(Suite, "~s", [format_path(TC,Groups)]), 83 | {ok, State}; 84 | post_end_per_testcase(TC,Config,Error,State=#state{suite=Suite, groups=Groups}) -> 85 | case lists:keyfind(tc_status, 1, Config) of 86 | {tc_status, ok} -> 87 | %% Test case passed, but we still ended in an error 88 | format_stack(Suite, "~s", [format_path(TC,Groups)], Error, ?SKIPC, "end_per_testcase FAILED"); 89 | _ -> 90 | %% Test case failed, in which case on_tc_fail already reports it 91 | ok 92 | end, 93 | {Error, State}. 94 | 95 | %% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group, 96 | %% post_end_per_group and post_end_per_testcase if the suite, group or test case failed. 97 | on_tc_fail({TC,_Group}, Reason, State=#state{suite=Suite, groups=Groups}) -> 98 | format_fail(Suite, "~s", [format_path(TC,Groups)], Reason), 99 | State; 100 | on_tc_fail(TC, Reason, State=#state{suite=Suite, groups=Groups}) -> 101 | format_fail(Suite, "~s", [format_path(TC,Groups)], Reason), 102 | State. 103 | 104 | %% @doc Called when a test case is skipped by either user action 105 | %% or due to an init function failing. (>= 19.3) 106 | on_tc_skip(Suite, {TC,_Group}, Reason, State=#state{groups=Groups, opts=Opts}) -> 107 | skip(Suite, TC, Groups, Reason, Opts), 108 | State#state{suite=Suite}; 109 | on_tc_skip(Suite, TC, Reason, State=#state{groups=Groups, opts=Opts}) -> 110 | skip(Suite, TC, Groups, Reason, Opts), 111 | State#state{suite=Suite}. 112 | 113 | skip(Suite, TC, Groups, Reason, Opts) -> 114 | Verbose = proplists:get_value(verbose, Opts, true), 115 | format_skip(Suite, "~s", [format_path(TC,Groups)], Reason, Verbose). 116 | 117 | %% @doc Called when a test case is skipped by either user action 118 | %% or due to an init function failing. (Pre-19.3) 119 | on_tc_skip({TC,Group}, Reason, State=#state{suite=Suite}) -> 120 | format_skip(Suite, "~p (group ~p)", [TC, Group], Reason, true), 121 | State; 122 | on_tc_skip(TC, Reason, State=#state{suite=Suite}) -> 123 | format_skip(Suite, "~p", [TC], Reason, true), 124 | State. 125 | 126 | %% @doc Called when the scope of the CTH is done 127 | terminate(_State) -> 128 | ok. 129 | 130 | %%%%%%%%%%%%%%% 131 | %%% HELPERS %%% 132 | %%%%%%%%%%%%%%% 133 | 134 | format_ok(Suite, CasePat, CaseArgs) -> 135 | format_case(Suite, CasePat, ?OKC, "OK", CaseArgs). 136 | 137 | format_skip(Suite, CasePat, CaseArgs, Reason, Verbose) -> 138 | format_stack(Suite, CasePat, CaseArgs, Reason, ?SKIPC, "SKIPPED", Verbose). 139 | 140 | format_fail(Suite, CasePat, CaseArgs, Reason) -> 141 | format_stack(Suite, CasePat, CaseArgs, Reason, ?FAILC, "FAILED"). 142 | 143 | format_stack(Suite, CasePat, CaseArgs, Reason, Color, Label) -> 144 | format_stack(Suite, CasePat, CaseArgs, Reason, Color, Label, true). 145 | 146 | format_stack(Suite, CasePat, CaseArgs, Reason, Color, Label, Verbose) -> 147 | case Verbose of 148 | true -> 149 | format_case(Suite, CasePat, Color, Label, CaseArgs), 150 | io:format(user, "%%% ~p ==> ~ts~n", [Suite,colorize(Color, maybe_eunit_format(Reason))]); 151 | false -> 152 | io:format(user, colorize(Color, "*"), []) 153 | end. 154 | 155 | format_case(Suite, CasePat, Color, Res, Args) -> 156 | case Res of 157 | "OK" -> io:put_chars(user, colorize(Color, ".")); 158 | _ -> io:format(user, lists:flatten(["~n%%% ~p ==> ",CasePat,": ",colorize(Color, Res),"~n"]), [Suite | Args]) 159 | end. 160 | -------------------------------------------------------------------------------- /src/cth_readable_failonly.erl: -------------------------------------------------------------------------------- 1 | -module(cth_readable_failonly). 2 | 3 | %% We use configuration patterns for older versions of Erlang/OTP 4 | %% that do not exist anymore in the modern versions scanned by 5 | %% Dialyzer, so turn off the warnings for those. 6 | %% 7 | %% Also we create an idle fun we know never returns, so turn 8 | %% that off too. 9 | -dialyzer([no_return]). 10 | 11 | -record(state, {id, 12 | sasl_reset, 13 | lager_reset, 14 | handlers=[], 15 | named, 16 | has_logger}). 17 | -record(eh_state, {buf=queue:new(), 18 | sasl=false, 19 | max_events = inf, 20 | stored_events = 0, 21 | dropped_events = 0}). 22 | 23 | %% Callbacks 24 | -export([id/1]). 25 | -export([init/2]). 26 | 27 | -export([pre_init_per_suite/3]). 28 | -export([post_init_per_suite/4]). 29 | -export([pre_end_per_suite/3]). 30 | -export([post_end_per_suite/4]). 31 | 32 | -export([pre_init_per_group/3]). 33 | -export([post_init_per_group/4]). 34 | -export([pre_end_per_group/3]). 35 | -export([post_end_per_group/4]). 36 | 37 | -export([pre_init_per_testcase/3]). 38 | -export([post_end_per_testcase/4]). 39 | 40 | -export([on_tc_fail/3]). 41 | -export([on_tc_skip/3, on_tc_skip/4]). 42 | 43 | -export([terminate/1]). 44 | 45 | %% Error Logger Handler API 46 | -export([init/1, 47 | handle_event/2, handle_call/2, handle_info/2, 48 | terminate/2, code_change/3]). 49 | 50 | %% Logger API 51 | -export([log/2, 52 | adding_handler/1, removing_handler/1]). 53 | 54 | -define(DEFAULT_LAGER_SINK, lager_event). 55 | -define(DEFAULT_LAGER_HANDLER_CONF, 56 | [{lager_console_backend, [{level, info}]}, 57 | {lager_file_backend, 58 | [{file, "log/error.log"}, {level, error}, 59 | {size, 10485760}, {date, "$D0"}, {count, 5}] 60 | }, 61 | {lager_file_backend, 62 | [{file, "log/console.log"}, {level, info}, 63 | {size, 10485760}, {date, "$D0"}, {count, 5}] 64 | } 65 | ]). 66 | 67 | -ifndef(LOCATION). 68 | %% imported from kernel/include/logger.hrl but with replaced unsupported macros 69 | -define(LOCATION,#{mfa=>{?MODULE,log_to_binary,2}, 70 | line=>?LINE, 71 | file=>?FILE}). 72 | -endif. 73 | %% imported from logger_internal.hrl 74 | -define(DEFAULT_FORMATTER, logger_formatter). 75 | -define(DEFAULT_FORMAT_CONFIG, #{legacy_header => true, 76 | single_line => false}). 77 | -define(LOG_INTERNAL(Level,Report), 78 | case logger:allow(Level,?MODULE) of 79 | true -> 80 | %% Spawn this to avoid deadlocks 81 | _ = spawn(logger,macro_log,[?LOCATION,Level,Report, 82 | logger:add_default_metadata(#{})]), 83 | ok; 84 | false -> 85 | ok 86 | end). 87 | 88 | %% @doc Return a unique id for this CTH. 89 | id(_Opts) -> 90 | {?MODULE, make_ref()}. 91 | 92 | %% @doc Always called before any other callback function. Use this to initiate 93 | %% any common state. 94 | init(Id, Opts) -> 95 | %% ct:pal replacement needs to know if this hook is enabled -- we use a named proc for that. 96 | %% Use a `receive' -- if people mock `timer' or reload it, it can kill the 97 | %% hook and then CT as a whole. 98 | Named = spawn_link(fun() -> receive after infinity -> ok end end), 99 | register(?MODULE, Named), 100 | MaxEvents = proplists:get_value(max_events, Opts, inf), 101 | HasLogger = has_logger(), % Pre OTP-21 or not 102 | Cfg = maybe_steal_logger_config(), 103 | case HasLogger of 104 | false -> 105 | error_logger:tty(false), % TODO check if on to begin with 106 | application:load(sasl); % TODO do this optionally? 107 | true -> 108 | %% Assume default logger is running // TODO: check if on to begin with 109 | logger:add_handler_filter(default, ?MODULE, {fun(_,_) -> stop end, nostate}), 110 | ok 111 | end, 112 | LagerReset = setup_lager(), 113 | case application:get_env(sasl, sasl_error_logger) of 114 | {ok, tty} when not HasLogger -> 115 | ok = gen_event:add_handler(error_logger, ?MODULE, [sasl, {max_events, MaxEvents}]), 116 | application:set_env(sasl, sasl_error_logger, false), 117 | {ok, #state{id=Id, sasl_reset={reset, tty}, lager_reset=LagerReset, 118 | handlers=[?MODULE], named=Named, has_logger=HasLogger}}; 119 | {ok, tty} when HasLogger -> 120 | SubCfg = maps:get(config, Cfg, #{}), 121 | logger:add_handler(?MODULE, ?MODULE, Cfg#{config => SubCfg#{sasl => true, max_events => MaxEvents}}), 122 | {ok, #state{id=Id, lager_reset=LagerReset, handlers=[?MODULE], 123 | named=Named, has_logger=HasLogger}}; 124 | _ when HasLogger -> 125 | SubCfg = maps:get(config, Cfg, #{}), 126 | logger:add_handler(?MODULE, ?MODULE, Cfg#{config => SubCfg#{sasl => false, max_events => MaxEvents}}), 127 | {ok, #state{id=Id, lager_reset=LagerReset, handlers=[?MODULE], 128 | named=Named, has_logger=HasLogger}}; 129 | _ -> 130 | ok = gen_event:add_handler(error_logger, ?MODULE, [{max_events, MaxEvents}]), 131 | {ok, #state{id=Id, lager_reset=LagerReset, handlers=[?MODULE], 132 | named=Named, has_logger=HasLogger}} 133 | end. 134 | 135 | %% @doc Called before init_per_suite is called. 136 | pre_init_per_suite(_Suite,Config,State) -> 137 | {Config, State}. 138 | 139 | %% @doc Called after init_per_suite. 140 | post_init_per_suite(_Suite,_Config,Return,State) -> 141 | {Return, State}. 142 | 143 | %% @doc Called before end_per_suite. 144 | pre_end_per_suite(_Suite,Config,State) -> 145 | call_handlers(ignore, State), 146 | {Config, State}. 147 | 148 | %% @doc Called after end_per_suite. 149 | post_end_per_suite(_Suite,_Config,Return,State) -> 150 | {Return, State}. 151 | 152 | %% @doc Called before each init_per_group. 153 | pre_init_per_group(_Group,Config,State) -> 154 | call_handlers(ignore, State), 155 | {Config, State}. 156 | 157 | %% @doc Called after each init_per_group. 158 | post_init_per_group(_Group,_Config,Return,State) -> 159 | {Return, State}. 160 | 161 | %% @doc Called after each end_per_group. 162 | pre_end_per_group(_Group,Config,State) -> 163 | {Config, State}. 164 | 165 | %% @doc Called after each end_per_group. 166 | post_end_per_group(_Group,_Config,Return,State) -> 167 | {Return, State}. 168 | 169 | %% @doc Called before each test case. 170 | pre_init_per_testcase(_TC,Config,State) -> 171 | call_handlers(ignore, State), 172 | {Config, State}. 173 | 174 | %% @doc Called after each test case. 175 | post_end_per_testcase(_TC,_Config,ok,State=#state{}) -> 176 | {ok, State}; 177 | post_end_per_testcase(_TC,_Config,Error,State) -> 178 | {Error, State}. 179 | 180 | %% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group, 181 | %% post_end_per_group and post_end_per_testcase if the suite, group or test case failed. 182 | on_tc_fail({_TC,_Group}, _Reason, State=#state{}) -> 183 | call_handlers(flush, State), 184 | State; 185 | on_tc_fail(_TC, _Reason, State=#state{}) -> 186 | call_handlers(flush, State), 187 | State. 188 | 189 | %% @doc Called when a test case is skipped by either user action 190 | %% or due to an init function failing (>= 19.3) 191 | on_tc_skip(_Suite, {_TC,_Group}, _Reason, State=#state{}) -> 192 | call_handlers(flush, State), 193 | State; 194 | on_tc_skip(_Suite, _TC, _Reason, State=#state{}) -> 195 | call_handlers(flush, State), 196 | State. 197 | 198 | %% @doc Called when a test case is skipped by either user action 199 | %% or due to an init function failing (pre 19.3) 200 | on_tc_skip({_TC,_Group}, _Reason, State=#state{}) -> 201 | call_handlers(flush, State), 202 | State; 203 | on_tc_skip(_TC, _Reason, State=#state{}) -> 204 | call_handlers(flush, State), 205 | State. 206 | 207 | %% @doc Called when the scope of the CTH is done 208 | terminate(_State=#state{handlers=Handlers, sasl_reset=SReset, 209 | lager_reset=LReset, named=Pid, has_logger=HasLogger}) -> 210 | 211 | if HasLogger -> 212 | logger:remove_handler(?MODULE), 213 | logger:remove_handler_filter(default, ?MODULE); 214 | not HasLogger -> 215 | _ = [gen_event:delete_handler(error_logger, Handler, shutdown) 216 | || Handler <- Handlers] 217 | end, 218 | case SReset of 219 | {reset, Val} -> application:set_env(sasl, sasl_error_logger, Val); 220 | undefined -> ok 221 | end, 222 | not HasLogger andalso error_logger:tty(true), 223 | application:unload(sasl), % silently fails if running 224 | lager_reset(LReset), 225 | %% Kill the named process signifying this is running 226 | unlink(Pid), 227 | Ref = erlang:monitor(process, Pid), 228 | exit(Pid, shutdown), 229 | receive 230 | {'DOWN', Ref, process, Pid, shutdown} -> ok 231 | end. 232 | 233 | %%%%%%%%%%%%% ERROR_LOGGER HANDLER %%%%%%%%%%%% 234 | 235 | init(Opts) -> 236 | {ok, 237 | #eh_state{ 238 | sasl = proplists:get_bool(sasl, Opts), 239 | max_events = proplists:get_value(max_events, Opts) 240 | }}. 241 | 242 | handle_event(Event, State) -> 243 | NewState = case parse_event(Event) of 244 | ignore -> State; 245 | logger -> buffer_event({logger, Event}, State); 246 | sasl -> buffer_event({sasl, {calendar:local_time(), Event}}, State); 247 | error_logger -> buffer_event({error_logger, {erlang:universaltime(), Event}}, State) 248 | end, 249 | {ok, NewState}. 250 | 251 | handle_info(_, State) -> 252 | {ok, State}. 253 | 254 | handle_call({lager, _} = Event, State) -> 255 | %% lager events come in from our fake handler, pre-filtered. 256 | {ok, ok, buffer_event(Event, State)}; 257 | handle_call({ct_pal, ignore}, S) -> 258 | {ok, ok, S}; 259 | handle_call({ct_pal, _}=Event, State) -> 260 | {ok, ok, buffer_event(Event, State)}; 261 | handle_call(ignore, State) -> 262 | {ok, ok, State#eh_state{buf=queue:new(), stored_events=0}}; 263 | handle_call(flush, S=#eh_state{buf=Buf, dropped_events=Dropped}) -> 264 | Cfg = maybe_steal_logger_config(), 265 | ShowSASL = sasl_running() orelse sasl_ran(Buf) andalso S#eh_state.sasl, 266 | SASLType = get_sasl_error_logger_type(), 267 | not queue:is_empty(Buf) andalso io:put_chars(user, "\n"), 268 | flush(Buf, Cfg, ShowSASL, SASLType, Dropped), 269 | {ok, ok, S#eh_state{buf=queue:new(), stored_events=0}}; 270 | handle_call(_Event, State) -> 271 | {ok, ok, State}. 272 | 273 | code_change(_, _, State) -> 274 | {ok, State}. 275 | 276 | terminate(_, _) -> 277 | ok. 278 | 279 | buffer_event(Event, S=#eh_state{buf=Buf, max_events=inf}) -> 280 | %% unbound buffer 281 | S#eh_state{buf=queue:in(Event, Buf)}; 282 | buffer_event(Event, S=#eh_state{buf=Buf, max_events=MaxEvents, stored_events=StoredEvents}) when MaxEvents > StoredEvents -> 283 | %% bound buffer; buffer not filled yet 284 | S#eh_state{buf=queue:in(Event, Buf), stored_events=StoredEvents + 1}; 285 | buffer_event(Event, S=#eh_state{buf=Buf0, dropped_events=DroppedEvents}) -> 286 | %% bound buffer; buffer filled 287 | {_, Buf1} = queue:out(Buf0), 288 | S#eh_state{buf=queue:in(Event, Buf1), dropped_events=DroppedEvents + 1}. 289 | 290 | flush(Buf, Cfg, ShowSASL, SASLType, Dropped) when Dropped > 0 -> 291 | io:format(user, "(logs are truncated, dropped ~b events)~n", [Dropped]), 292 | flush(Buf, Cfg, ShowSASL, SASLType); 293 | flush(Buf, Cfg, ShowSASL, SASLType, _) -> 294 | flush(Buf, Cfg, ShowSASL, SASLType). 295 | 296 | flush(Buf, Cfg, ShowSASL, SASLType) -> 297 | case queue:out(Buf) of 298 | {empty, _} -> ok; 299 | {{value, {T, Event}}, NextBuf} -> 300 | case T of 301 | error_logger -> 302 | error_logger_tty_h:write_event(Event, io); 303 | sasl when ShowSASL -> 304 | sasl_report:write_report(standard_io, SASLType, Event); 305 | ct_pal -> 306 | io:format(user, Event, []); 307 | lager -> 308 | io:put_chars(user, Event); 309 | logger -> 310 | Bin = log_to_binary(Event,Cfg), 311 | io:put_chars(user, Bin); 312 | _ -> 313 | ignore 314 | end, 315 | flush(NextBuf, Cfg, ShowSASL, SASLType) 316 | end. 317 | 318 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 319 | 320 | 321 | %%%%%%%%%%%%%%%% LOGGER %%%%%%%%%%%%%%%%%%% 322 | adding_handler(Config = #{config := #{sasl := SASL, max_events := MaxEvents}}) -> 323 | {ok, Pid} = gen_event:start({local, cth_readable_logger}, []), 324 | gen_event:add_handler(cth_readable_logger, ?MODULE, [{sasl, SASL}, {max_events, MaxEvents}]), 325 | {ok, Config#{cth_readable_logger => Pid}}. 326 | 327 | removing_handler(#{cth_readable_logger := Pid}) -> 328 | try gen_event:stop(Pid, shutdown, 1000) of 329 | ok -> ok 330 | catch 331 | error:noproc -> ok; 332 | error:timeout -> at_least_we_tried 333 | end. 334 | 335 | log(Msg, #{cth_readable_logger := Pid}) -> 336 | gen_event:notify(Pid, Msg). 337 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 338 | 339 | has_logger() -> 340 | %% Module is present 341 | erlang:function_exported(logger, module_info, 0). 342 | 343 | has_usable_logger() -> 344 | %% The config is set (lager didn't remove it) 345 | erlang:function_exported(logger, get_handler_config, 1) andalso 346 | logger:get_handler_config(default) =/= {error, {not_found, default}}. 347 | 348 | maybe_steal_logger_config() -> 349 | case has_logger() andalso has_usable_logger() of 350 | false -> 351 | #{}; 352 | true -> 353 | {ok, Cfg} = logger:get_handler_config(default), 354 | maps:with([formatter], Cfg) % only keep the essential 355 | end. 356 | 357 | sasl_running() -> 358 | length([1 || {sasl, _, _} <- application:which_applications()]) > 0. 359 | 360 | get_sasl_error_logger_type() -> 361 | case application:get_env(sasl, errlog_type) of 362 | {ok, error} -> error; 363 | {ok, progress} -> progress; 364 | {ok, all} -> all; 365 | {ok, Bad} -> exit({bad_config, {sasl, {errlog_type, Bad}}}); 366 | _ -> all 367 | end. 368 | 369 | sasl_ran(Buf) -> 370 | case queue:out(Buf) of 371 | {empty, _} -> false; 372 | {{value, {sasl, {_DateTime, {info_report,_, 373 | {_,progress, [{application,sasl},{started_at,_}|_]}}}}}, _} -> true; 374 | {_, Rest} -> sasl_ran(Rest) 375 | end. 376 | 377 | call_handlers(Msg, #state{handlers=Handlers, has_logger=HasLogger}) -> 378 | Name = if HasLogger -> cth_readable_logger; 379 | not HasLogger -> error_logger 380 | end, 381 | _ = [gen_event:call(Name, Handler, Msg, 300000) 382 | || Handler <- Handlers], 383 | ok. 384 | 385 | parse_event({_, GL, _}) when node(GL) =/= node() -> ignore; 386 | parse_event({info_report, _GL, {_Pid, progress, _Args}}) -> sasl; 387 | parse_event({error_report, _GL, {_Pid, supervisor_report, _Args}}) -> sasl; 388 | parse_event({error_report, _GL, {_Pid, crash_report, _Args}}) -> sasl; 389 | parse_event({error, _GL, {_Pid, _Format, _Args}}) -> error_logger; 390 | parse_event({info_msg, _GL, {_Pid, _Format, _Args}}) -> error_logger; 391 | parse_event({warning_msg, _GL, {_Pid, _Format, _Args}}) -> error_logger; 392 | parse_event({error_report, _GL, {_Pid, _Format, _Args}}) -> error_logger; 393 | parse_event({info_report, _GL, {_Pid, _Format, _Args}}) -> error_logger; 394 | parse_event({warning_report, _GL, {_Pid, _Format, _Args}}) -> error_logger; 395 | parse_event(Map) when is_map(Map) -> logger; 396 | parse_event(_) -> sasl. % sasl does its own filtering 397 | 398 | setup_lager() -> 399 | case application:load(lager) of 400 | {error, {"no such file or directory", _}} -> 401 | %% app not available 402 | undefined; 403 | _ -> % it's show time 404 | %% Keep lager from throwing us out 405 | WhiteList = application:get_env(lager, error_logger_whitelist, []), 406 | application:set_env(lager, error_logger_whitelist, [?MODULE|WhiteList]), 407 | InitConf = application:get_env(lager, handlers, ?DEFAULT_LAGER_HANDLER_CONF), 408 | %% Add ourselves to the config 409 | NewConf = case proplists:get_value(lager_console_backend, InitConf) of 410 | undefined -> % no console backend running 411 | InitConf; 412 | Opts -> 413 | [{cth_readable_lager_backend, Opts} 414 | | InitConf -- [{lager_console_backend, Opts}]] 415 | end, 416 | application:set_env(lager, handlers, NewConf), 417 | %% check if lager is running and override! 418 | case {whereis(lager_sup), 419 | proplists:get_value(cth_readable_lager_backend, NewConf)} of 420 | {undefined, _} -> 421 | InitConf; 422 | {_, undefined} -> 423 | InitConf; 424 | {_, LOpts} -> 425 | swap_lager_handlers(lager_console_backend, 426 | cth_readable_lager_backend, LOpts), 427 | InitConf 428 | end 429 | end. 430 | 431 | lager_reset(undefined) -> 432 | ok; 433 | lager_reset(InitConf) -> 434 | %% Reset the whitelist 435 | WhiteList = application:get_env(lager, error_logger_whitelist, []), 436 | application:set_env(lager, error_logger_whitelist, WhiteList--[?MODULE]), 437 | %% Swap them handlers again 438 | Opts = proplists:get_value(lager_console_backend, InitConf), 439 | application:set_env(lager, handlers, InitConf), 440 | case {whereis(lager_sup), Opts} of 441 | {undefined, _} -> % not running 442 | ok; 443 | {_, undefined} -> % not scheduled 444 | ok; 445 | {_, _} -> 446 | swap_lager_handlers(cth_readable_lager_backend, 447 | lager_console_backend, Opts) 448 | end. 449 | 450 | swap_lager_handlers(Old, New, Opts) -> 451 | gen_event:delete_handler(?DEFAULT_LAGER_SINK, Old, shutdown), 452 | lager_app:start_handler(?DEFAULT_LAGER_SINK, 453 | New, Opts). 454 | 455 | %% Imported from Erlang/OTP -- this function used to be public in OTP-20, 456 | %% but was then taken public by OTP-21, which broke functionality. 457 | %% Original at https://raw.githubusercontent.com/erlang/otp/OTP-21.2.5/lib/kernel/src/logger_h_common.erl 458 | log_to_binary(#{msg:={report,_},meta:=#{report_cb:=_}}=Log,Config) -> 459 | do_log_to_binary(Log,Config); 460 | log_to_binary(#{msg:={report,_},meta:=Meta}=Log,Config) -> 461 | DefaultReportCb = fun logger:format_otp_report/1, 462 | do_log_to_binary(Log#{meta=>Meta#{report_cb=>DefaultReportCb}},Config); 463 | log_to_binary(Log,Config) -> 464 | do_log_to_binary(Log,Config). 465 | 466 | do_log_to_binary(Log,Config) -> 467 | {Formatter,FormatterConfig} = 468 | maps:get(formatter,Config,{?DEFAULT_FORMATTER,?DEFAULT_FORMAT_CONFIG}), 469 | String = try_format(Log,Formatter,FormatterConfig), 470 | try string_to_binary(String) 471 | catch C2:R2 -> 472 | ?LOG_INTERNAL(debug,[{formatter_error,Formatter}, 473 | {config,FormatterConfig}, 474 | {log_event,Log}, 475 | {bad_return_value,String}, 476 | {catched,{C2,R2,[]}}]), 477 | <<"FORMATTER ERROR: bad return value">> 478 | end. 479 | 480 | try_format(Log,Formatter,FormatterConfig) -> 481 | try Formatter:format(Log,FormatterConfig) 482 | catch 483 | C:R -> 484 | ?LOG_INTERNAL(debug,[{formatter_crashed,Formatter}, 485 | {config,FormatterConfig}, 486 | {log_event,Log}, 487 | {reason, 488 | {C,R,[]}}]), 489 | case {?DEFAULT_FORMATTER,#{}} of 490 | {Formatter,FormatterConfig} -> 491 | "DEFAULT FORMATTER CRASHED"; 492 | {DefaultFormatter,DefaultConfig} -> 493 | try_format(Log#{msg=>{"FORMATTER CRASH: ~tp", 494 | [maps:get(msg,Log)]}}, 495 | DefaultFormatter,DefaultConfig) 496 | end 497 | end. 498 | 499 | string_to_binary(String) -> 500 | case unicode:characters_to_binary(String) of 501 | Binary when is_binary(Binary) -> 502 | Binary; 503 | Error -> 504 | throw(Error) 505 | end. 506 | -------------------------------------------------------------------------------- /src/cth_readable_helpers.erl: -------------------------------------------------------------------------------- 1 | -module(cth_readable_helpers). 2 | -export([format_path/2, colorize/2, maybe_eunit_format/1]). 3 | 4 | format_path(TC, Groups) -> 5 | join([atom_to_list(P) || P <- lists:reverse([TC|Groups])], "."). 6 | 7 | %% string:join/2 copy; string:join/2 is getting obsoleted 8 | %% and replaced by lists:join/2, but lists:join/2 is too new 9 | %% for version support (only appeared in 19.0) so it cannot be 10 | %% used. Instead we just adopt join/2 locally and hope it works 11 | %% for most unicode use cases anyway. 12 | join([], Sep) when is_list(Sep) -> 13 | []; 14 | join([H|T], Sep) -> 15 | H ++ lists:append([Sep ++ X || X <- T]). 16 | 17 | colorize(red, Txt) -> cf:format("~!r~s~!!", [Txt]); 18 | colorize(green, Txt) -> cf:format("~!g~s~!!", [Txt]); 19 | colorize(magenta, Txt) -> cf:format("~!m~s~!!",[Txt]). 20 | 21 | maybe_eunit_format({failed, Reason}) -> 22 | maybe_eunit_format(Reason); 23 | 24 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assert_failed 25 | ; Type =:= assert -> 26 | Keys = proplists:get_keys(Props), 27 | HasEUnitProps = ([expression, value, line] -- Keys) =:= [], 28 | HasHamcrestProps = ([expected, actual, matcher, line] -- Keys) =:= [], 29 | if 30 | HasEUnitProps -> 31 | [io_lib:format("~nFailure/Error: ?assert(~s)~n", [proplists:get_value(expression, Props)]), 32 | io_lib:format(" expected: ~p~n", [proplists:get_value(expected, Props)]), 33 | case proplists:get_value(value, Props) of 34 | {not_a_boolean, V} -> 35 | io_lib:format(" got: ~p~n", [V]); 36 | V -> 37 | io_lib:format(" got: ~p~n", [V]) 38 | end, io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 39 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 40 | HasHamcrestProps -> 41 | [io_lib:format("~nFailure/Error: ?assertThat(~p)~n", [proplists:get_value(matcher, Props)]), 42 | io_lib:format(" expected: ~p~n", [proplists:get_value(expected, Props)]), 43 | io_lib:format(" got: ~p~n", [proplists:get_value(actual, Props)]), 44 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])]; 45 | true -> 46 | [io_lib:format("~nFailure/Error: unknown assert: ~p", [Props])] 47 | end; 48 | 49 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assertMatch_failed 50 | ; Type =:= assertMatch -> 51 | Expr = proplists:get_value(expression, Props), 52 | Pattern = proplists:get_value(pattern, Props), 53 | Value = proplists:get_value(value, Props), 54 | [io_lib:format("~nFailure/Error: ?assertMatch(~s, ~s)~n", [Pattern, Expr]), 55 | io_lib:format(" expected: = ~s~n", [Pattern]), 56 | io_lib:format(" got: ~p~n", [Value]), 57 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 58 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 59 | 60 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assertNotMatch_failed 61 | ; Type =:= assertNotMatch -> 62 | Expr = proplists:get_value(expression, Props), 63 | Pattern = proplists:get_value(pattern, Props), 64 | Value = proplists:get_value(value, Props), 65 | [io_lib:format("~nFailure/Error: ?assertNotMatch(~s, ~s)~n", [Pattern, Expr]), 66 | io_lib:format(" expected not: = ~s~n", [Pattern]), 67 | io_lib:format(" got: ~p~n", [Value]), 68 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 69 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 70 | 71 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assertEqual_failed 72 | ; Type =:= assertEqual -> 73 | Expr = proplists:get_value(expression, Props), 74 | Expected = proplists:get_value(expected, Props), 75 | Value = proplists:get_value(value, Props), 76 | [io_lib:format("~nFailure/Error: ?assertEqual(~w, ~s)~n", [Expected, 77 | Expr]), 78 | io_lib:format(" expected: ~p~n", [Expected]), 79 | io_lib:format(" got: ~p~n", [Value]), 80 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 81 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 82 | 83 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assertNotEqual_failed 84 | ; Type =:= assertNotEqual -> 85 | Expr = proplists:get_value(expression, Props), 86 | Value = proplists:get_value(value, Props), 87 | [io_lib:format("~nFailure/Error: ?assertNotEqual(~p, ~s)~n", 88 | [Value, Expr]), 89 | io_lib:format(" expected not: == ~p~n", [Value]), 90 | io_lib:format(" got: ~p~n", [Value]), 91 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 92 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 93 | 94 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assertException_failed 95 | ; Type =:= assertException -> 96 | Expr = proplists:get_value(expression, Props), 97 | Pattern = proplists:get_value(pattern, Props), 98 | {Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DATA 99 | [io_lib:format("~nFailure/Error: ?assertException(~s, ~s, ~s)~n", [Class, Term, Expr]), 100 | case proplists:is_defined(unexpected_success, Props) of 101 | true -> 102 | [io_lib:format(" expected: exception ~s but nothing was raised~n", [Pattern]), 103 | io_lib:format(" got: value ~p~n", [proplists:get_value(unexpected_success, Props)]), 104 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])]; 105 | false -> 106 | Ex = proplists:get_value(unexpected_exception, Props), 107 | [io_lib:format(" expected: exception ~s~n", [Pattern]), 108 | io_lib:format(" got: exception ~p~n", [Ex]), 109 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] 110 | end] ++ 111 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 112 | 113 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assertNotException_failed 114 | ; Type =:= assertNotException -> 115 | Expr = proplists:get_value(expression, Props), 116 | Pattern = proplists:get_value(pattern, Props), 117 | {Class, Term} = extract_exception_pattern(Pattern), % I hate that we have to do this, why not just give DAT 118 | Ex = proplists:get_value(unexpected_exception, Props), 119 | [io_lib:format("~nFailure/Error: ?assertNotException(~s, ~s, ~s)~n", [Class, Term, Expr]), 120 | io_lib:format(" expected not: exception ~s~n", [Pattern]), 121 | io_lib:format(" got: exception ~p~n", [Ex]), 122 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 123 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 124 | 125 | maybe_eunit_format({{Type, Props}, _}) when Type =:= command_failed 126 | ; Type =:= command -> 127 | Cmd = proplists:get_value(command, Props), 128 | Expected = proplists:get_value(expected_status, Props), 129 | Status = proplists:get_value(status, Props), 130 | [io_lib:format("~nFailure/Error: ?cmdStatus(~p, ~p)~n", [Expected, Cmd]), 131 | io_lib:format(" expected: status ~p~n", [Expected]), 132 | io_lib:format(" got: status ~p~n", [Status]), 133 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 134 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 135 | 136 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assertCmd_failed 137 | ; Type =:= assertCmd -> 138 | Cmd = proplists:get_value(command, Props), 139 | Expected = proplists:get_value(expected_status, Props), 140 | Status = proplists:get_value(status, Props), 141 | [io_lib:format("~nFailure/Error: ?assertCmdStatus(~p, ~p)~n", [Expected, Cmd]), 142 | io_lib:format(" expected: status ~p~n", [Expected]), 143 | io_lib:format(" got: status ~p~n", [Status]), 144 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 145 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 146 | 147 | maybe_eunit_format({{Type, Props}, _}) when Type =:= assertCmdOutput_failed 148 | ; Type =:= assertCmdOutput -> 149 | Cmd = proplists:get_value(command, Props), 150 | Expected = proplists:get_value(expected_output, Props), 151 | Output = proplists:get_value(output, Props), 152 | [io_lib:format("~nFailure/Error: ?assertCmdOutput(~p, ~p)~n", [Expected, Cmd]), 153 | io_lib:format(" expected: ~p~n", [Expected]), 154 | io_lib:format(" got: ~p~n", [Output]), 155 | io_lib:format(" line: ~p", [proplists:get_value(line, Props)])] ++ 156 | [io_lib:format("~n comment: ~p", [Comment]) || {comment, Comment} <- [proplists:lookup(comment, Props)]]; 157 | 158 | maybe_eunit_format(Reason) -> 159 | io_lib:format("~p", [Reason]). 160 | 161 | extract_exception_pattern(Str) -> 162 | ["{", Class, Term|_] = re:split(Str, "[, ]{1,2}", [unicode,{return,list}]), 163 | {Class, Term}. 164 | -------------------------------------------------------------------------------- /src/cth_readable_lager_backend.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2011-2012, 2014 Basho Technologies, Inc. All Rights Reserved. 2 | %% 3 | %% This file is provided to you under the Apache License, 4 | %% Version 2.0 (the "License"); you may not use this file 5 | %% except in compliance with the License. You may obtain 6 | %% a copy of the License at 7 | %% 8 | %% http://www.apache.org/licenses/LICENSE-2.0 9 | %% 10 | %% Unless required by applicable law or agreed to in writing, 11 | %% software distributed under the License is distributed on an 12 | %% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 13 | %% KIND, either express or implied. See the License for the 14 | %% specific language governing permissions and limitations 15 | %% under the License. 16 | 17 | %% @doc Console backend for lager that mutes logs to the shell when 18 | %% CT runs succeed. Configured with a single option, the loglevel 19 | %% desired. 20 | 21 | -module(cth_readable_lager_backend). 22 | 23 | -behaviour(gen_event). 24 | 25 | -export([init/1, handle_call/2, handle_event/2, handle_info/2, terminate/2, 26 | code_change/3]). 27 | 28 | -record(state, {level :: {'mask', integer()}, 29 | out = user :: user | standard_error, 30 | formatter :: atom(), 31 | format_config :: any(), 32 | colors=[] :: list()}). 33 | 34 | %-include("lager.hrl"). 35 | -define(TERSE_FORMAT,[time, " ", color, "[", severity,"] ", message]). 36 | -define(DEFAULT_FORMAT_CONFIG, ?TERSE_FORMAT ++ [eol()]). 37 | -define(FORMAT_CONFIG_OFF, [{eol, eol()}]). 38 | 39 | %% @private 40 | init([Level]) when is_atom(Level) -> 41 | init([{level, Level}]); 42 | init([Level, true]) when is_atom(Level) -> % for backwards compatibility 43 | init([{level, Level}, {formatter_config, ?FORMAT_CONFIG_OFF}]); 44 | init([Level, false]) when is_atom(Level) -> % for backwards compatibility 45 | init([{level, Level}]); 46 | init(Options) when is_list(Options) -> 47 | Colors = case application:get_env(lager, colored) of 48 | {ok, true} -> 49 | {ok, LagerColors} = application:get_env(lager, colors), 50 | LagerColors; 51 | _ -> [] 52 | end, 53 | 54 | Level = get_option(level, Options, undefined), 55 | %% edited out a bunch of console detection stuff, hopefully not breaking 56 | try lager_util:config_to_mask(Level) of 57 | L -> 58 | [UseErr, Formatter, Config] = 59 | [get_option(K, Options, Default) || {K, Default} <- [{use_stderr, false}, 60 | {formatter, lager_default_formatter}, 61 | {formatter_config, ?DEFAULT_FORMAT_CONFIG}] 62 | ], 63 | Out = case UseErr of 64 | false -> user; 65 | true -> standard_error 66 | end, 67 | {ok, #state{level=L, 68 | out=Out, 69 | formatter=Formatter, 70 | format_config=Config, 71 | colors=Colors}} 72 | catch 73 | _:_ -> 74 | {error, {fatal, bad_log_level}} 75 | end; 76 | init(Level) -> 77 | init([Level,{lager_default_formatter,?TERSE_FORMAT ++ [eol()]}]). 78 | 79 | get_option(K, Options, Default) -> 80 | case lists:keyfind(K, 1, Options) of 81 | {K, V} -> V; 82 | false -> Default 83 | end. 84 | 85 | %% @private 86 | handle_call(get_loglevel, #state{level=Level} = State) -> 87 | {ok, Level, State}; 88 | handle_call({set_loglevel, Level}, State) -> 89 | try lager_util:config_to_mask(Level) of 90 | Levels -> 91 | {ok, ok, State#state{level=Levels}} 92 | catch 93 | _:_ -> 94 | {ok, {error, bad_log_level}, State} 95 | end; 96 | handle_call(_Request, State) -> 97 | {ok, ok, State}. 98 | 99 | %% @private 100 | handle_event({log, Message}, 101 | #state{level=L,formatter=Formatter,format_config=FormatConfig,colors=Colors} = State) -> 102 | case lager_util:is_loggable(Message, L, lager_console_backend) of 103 | true -> 104 | %% Handle multiple possible functions -- older lagers didn't 105 | %% support colors, and we depend on the currently running lib. 106 | Formatted = case erlang:function_exported(Formatter, format, 3) of 107 | true -> 108 | Formatter:format(Message,FormatConfig,Colors); 109 | false -> 110 | Formatter:format(Message,FormatConfig) 111 | end, 112 | %% lagger forwards in sync mode, and a call to error_logger makes 113 | %% everything deadlock, so we gotta go async on the logging call. 114 | %% We also need to do a call so that lager doesn't reforward the 115 | %% event in an infinite loop. 116 | Name = case erlang:function_exported(logger, module_info, 0) of 117 | true -> cth_readable_logger; 118 | false -> error_logger 119 | end, 120 | spawn(fun() -> gen_event:call(Name, cth_readable_failonly, {lager, Formatted}) end), 121 | ct_logs:tc_log(default, Formatted, []), 122 | {ok, State}; 123 | false -> 124 | {ok, State} 125 | end; 126 | handle_event(_Event, State) -> 127 | {ok, State}. 128 | 129 | %% @private 130 | handle_info(_Info, State) -> 131 | {ok, State}. 132 | 133 | %% @private 134 | terminate(_Reason, _State) -> 135 | ok. 136 | 137 | %% @private 138 | code_change(_OldVsn, State, _Extra) -> 139 | {ok, State}. 140 | 141 | eol() -> 142 | case application:get_env(lager, colored) of 143 | {ok, true} -> 144 | "\e[0m\r\n"; 145 | _ -> 146 | "\r\n" 147 | end. 148 | -------------------------------------------------------------------------------- /src/cth_readable_nosasl.erl: -------------------------------------------------------------------------------- 1 | -module(cth_readable_nosasl). 2 | 3 | %% Callbacks 4 | -export([id/1]). 5 | -export([init/2]). 6 | 7 | -export([pre_init_per_suite/3]). 8 | -export([post_init_per_suite/4]). 9 | -export([pre_end_per_suite/3]). 10 | -export([post_end_per_suite/4]). 11 | 12 | -export([pre_init_per_group/3]). 13 | -export([post_init_per_group/4]). 14 | -export([pre_end_per_group/3]). 15 | -export([post_end_per_group/4]). 16 | 17 | -export([pre_init_per_testcase/3]). 18 | -export([post_end_per_testcase/4]). 19 | 20 | -export([on_tc_fail/3]). 21 | -export([on_tc_skip/3, on_tc_skip/4]). 22 | 23 | -export([terminate/1]). 24 | 25 | %% @doc Return a unique id for this CTH. 26 | id(_Opts) -> 27 | {?MODULE, make_ref()}. 28 | 29 | %% @doc Always called before any other callback function. Use this to initiate 30 | %% any common state. 31 | init(_Id, _Opts) -> 32 | application:load(sasl), % TODO do this optionally? 33 | Res = application:get_env(sasl, sasl_error_logger), 34 | application:set_env(sasl, sasl_error_logger, false), 35 | {ok, Res}. 36 | 37 | %% @doc Called before init_per_suite is called. 38 | pre_init_per_suite(_Suite,Config,State) -> 39 | {Config, State}. 40 | 41 | %% @doc Called after init_per_suite. 42 | post_init_per_suite(_Suite,_Config,Return,State) -> 43 | {Return, State}. 44 | 45 | %% @doc Called before end_per_suite. 46 | pre_end_per_suite(_Suite,Config,State) -> 47 | {Config, State}. 48 | 49 | %% @doc Called after end_per_suite. 50 | post_end_per_suite(_Suite,_Config,Return,State) -> 51 | {Return, State}. 52 | 53 | %% @doc Called before each init_per_group. 54 | pre_init_per_group(_Group,Config,State) -> 55 | {Config, State}. 56 | 57 | %% @doc Called after each init_per_group. 58 | post_init_per_group(_Group,_Config,Return,State) -> 59 | {Return, State}. 60 | 61 | %% @doc Called after each end_per_group. 62 | pre_end_per_group(_Group,Config,State) -> 63 | {Config, State}. 64 | 65 | %% @doc Called after each end_per_group. 66 | post_end_per_group(_Group,_Config,Return,State) -> 67 | {Return, State}. 68 | 69 | %% @doc Called before each test case. 70 | pre_init_per_testcase(_TC,Config,State) -> 71 | {Config, State}. 72 | 73 | %% @doc Called after each test case. 74 | post_end_per_testcase(_TC,_Config,Error,State) -> 75 | {Error, State}. 76 | 77 | %% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group, 78 | %% post_end_per_group and post_end_per_testcase if the suite, group or test case failed. 79 | on_tc_fail(_TC, _Reason, State) -> 80 | State. 81 | 82 | %% @doc Called when a test case is skipped by either user action 83 | %% or due to an init function failing. (>= 19.3) 84 | on_tc_skip(_Suite, _TC, _Reason, State) -> 85 | State. 86 | %% @doc Called when a test case is skipped by either user action 87 | %% or due to an init function failing. (Pre-19.3) 88 | on_tc_skip(_TC, _Reason, State) -> 89 | State. 90 | 91 | %% @doc Called when the scope of the CTH is done 92 | terminate(Env) -> 93 | case Env of 94 | {ok, Val} -> 95 | application:set_env(sasl, sasl_error_logger, Val); 96 | undefined -> 97 | application:unset_env(sasl, sasl_error_logger) 98 | end, 99 | application:unload(sasl), % silently fails if running 100 | ok. 101 | -------------------------------------------------------------------------------- /src/cth_readable_shell.erl: -------------------------------------------------------------------------------- 1 | -module(cth_readable_shell). 2 | -import(cth_readable_helpers, [format_path/2, colorize/2, maybe_eunit_format/1]). 3 | 4 | -define(OKC, green). 5 | -define(FAILC, red). 6 | -define(SKIPC, magenta). 7 | 8 | %% Callbacks 9 | -export([id/1]). 10 | -export([init/2]). 11 | 12 | -export([pre_init_per_suite/3]). 13 | -export([post_init_per_suite/4]). 14 | -export([pre_end_per_suite/3]). 15 | -export([post_end_per_suite/4]). 16 | 17 | -export([pre_init_per_group/3]). 18 | -export([post_init_per_group/4]). 19 | -export([pre_end_per_group/3]). 20 | -export([post_end_per_group/4]). 21 | 22 | -export([pre_init_per_testcase/3]). 23 | -export([post_end_per_testcase/4]). 24 | 25 | -export([on_tc_fail/3]). 26 | -export([on_tc_skip/3, on_tc_skip/4]). 27 | 28 | -export([terminate/1]). 29 | 30 | -record(state, {id, suite, groups, opts}). 31 | 32 | %% @doc Return a unique id for this CTH. 33 | id(_Opts) -> 34 | {?MODULE, make_ref()}. 35 | 36 | %% @doc Always called before any other callback function. Use this to initiate 37 | %% any common state. 38 | init(Id, Opts) -> 39 | {ok, #state{id=Id, opts=Opts}}. 40 | 41 | %% @doc Called before init_per_suite is called. 42 | pre_init_per_suite(Suite,Config,State) -> 43 | {Config, State#state{suite=Suite, groups=[]}}. 44 | 45 | %% @doc Called after init_per_suite. 46 | post_init_per_suite(_Suite,_Config,Return,State) -> 47 | {Return, State}. 48 | 49 | %% @doc Called before end_per_suite. 50 | pre_end_per_suite(_Suite,Config,State) -> 51 | {Config, State}. 52 | 53 | %% @doc Called after end_per_suite. 54 | post_end_per_suite(_Suite,_Config,Return,State) -> 55 | {Return, State#state{suite=undefined, groups=[]}}. 56 | 57 | %% @doc Called before each init_per_group. 58 | pre_init_per_group(_Group,Config,State) -> 59 | {Config, State}. 60 | 61 | %% @doc Called after each init_per_group. 62 | post_init_per_group(Group,_Config,Return, State=#state{groups=Groups}) -> 63 | {Return, State#state{groups=[Group|Groups]}}. 64 | 65 | %% @doc Called after each end_per_group. 66 | pre_end_per_group(_Group,Config,State) -> 67 | {Config, State}. 68 | 69 | %% @doc Called after each end_per_group. 70 | post_end_per_group(_Group,_Config,Return, State=#state{groups=Groups}) -> 71 | {Return, State#state{groups=tl(Groups)}}. 72 | 73 | %% @doc Called before each test case. 74 | pre_init_per_testcase(_TC,Config,State) -> 75 | {Config, State}. 76 | 77 | %% @doc Called after each test case. 78 | post_end_per_testcase(TC,_Config,ok,State=#state{suite=Suite, groups=Groups}) -> 79 | format_ok(Suite, "~s", [format_path(TC,Groups)]), 80 | {ok, State}; 81 | post_end_per_testcase(TC,Config,Error,State=#state{suite=Suite, groups=Groups}) -> 82 | case lists:keyfind(tc_status, 1, Config) of 83 | {tc_status, ok} -> 84 | %% Test case passed, but we still ended in an error 85 | format_stack(Suite, "~s", [format_path(TC,Groups)], Error, ?SKIPC, "end_per_testcase FAILED"); 86 | _ -> 87 | %% Test case failed, in which case on_tc_fail already reports it 88 | ok 89 | end, 90 | {Error, State}. 91 | 92 | %% @doc Called after post_init_per_suite, post_end_per_suite, post_init_per_group, 93 | %% post_end_per_group and post_end_per_testcase if the suite, group or test case failed. 94 | on_tc_fail({TC,_Group}, Reason, State=#state{suite=Suite, groups=Groups}) -> 95 | format_fail(Suite, "~s", [format_path(TC,Groups)], Reason), 96 | State; 97 | on_tc_fail(TC, Reason, State=#state{suite=Suite, groups=Groups}) -> 98 | format_fail(Suite, "~s", [format_path(TC,Groups)], Reason), 99 | State. 100 | 101 | %% @doc Called when a test case is skipped by either user action 102 | %% or due to an init function failing. (>= 19.3) 103 | on_tc_skip(Suite, {TC,_Group}, Reason, State=#state{groups=Groups, opts=Opts}) -> 104 | skip(Suite, TC, Groups, Reason, Opts), 105 | State#state{suite=Suite}; 106 | on_tc_skip(Suite, TC, Reason, State=#state{groups=Groups, opts=Opts}) -> 107 | skip(Suite, TC, Groups, Reason, Opts), 108 | State#state{suite=Suite}. 109 | 110 | skip(Suite, TC, Groups, Reason, Opts) -> 111 | Verbose = proplists:get_value(verbose, Opts, true), 112 | format_skip(Suite, "~s", [format_path(TC,Groups)], Reason, Verbose). 113 | 114 | %% @doc Called when a test case is skipped by either user action 115 | %% or due to an init function failing. (Pre-19.3) 116 | on_tc_skip({TC,Group}, Reason, State=#state{suite=Suite}) -> 117 | format_skip(Suite, "~p (group ~p)", [TC, Group], Reason, true), 118 | State; 119 | on_tc_skip(TC, Reason, State=#state{suite=Suite}) -> 120 | format_skip(Suite, "~p", [TC], Reason, true), 121 | State. 122 | 123 | %% @doc Called when the scope of the CTH is done 124 | terminate(_State) -> 125 | ok. 126 | 127 | %%%%%%%%%%%%%%%% 128 | %%% Helpers %%% 129 | %%%%%%%%%%%%%%%% 130 | format_ok(Suite, CasePat, CaseArgs) -> 131 | format_case(Suite, CasePat, ?OKC, "OK", CaseArgs). 132 | 133 | format_skip(Suite, CasePat, CaseArgs, Reason, Verbose) -> 134 | format_stack(Suite, CasePat, CaseArgs, Reason, ?SKIPC, "SKIPPED", Verbose). 135 | 136 | format_fail(Suite, CasePat, CaseArgs, Reason) -> 137 | format_stack(Suite, CasePat, CaseArgs, Reason, ?FAILC, "FAILED"). 138 | 139 | format_case(Suite, CasePat, Color, Res, Args) -> 140 | case Res of 141 | "OK" -> io:put_chars(user, colorize(Color, ".")); 142 | _ -> io:format(user, lists:flatten(["~n%%% ~p ==> ",CasePat,": ",colorize(Color, Res),"~n"]), [Suite | Args]) 143 | end. 144 | 145 | format_stack(Suite, CasePat, CaseArgs, Reason, Color, Label) -> 146 | format_stack(Suite, CasePat, CaseArgs, Reason, Color, Label, true). 147 | 148 | format_stack(Suite, CasePat, CaseArgs, Reason, Color, Label, Verbose) -> 149 | case Verbose of 150 | true -> 151 | format_case(Suite, CasePat, Color, Label, CaseArgs), 152 | io:format(user, "%%% ~p ==> ~ts~n", [Suite,colorize(Color, maybe_eunit_format(Reason))]); 153 | false -> 154 | io:format(user, colorize(Color, "*"), []) 155 | end. 156 | -------------------------------------------------------------------------------- /src/cth_readable_transform.erl: -------------------------------------------------------------------------------- 1 | -module(cth_readable_transform). 2 | -export([parse_transform/2]). 3 | 4 | parse_transform(ASTs, _Options) -> 5 | try 6 | [erl_syntax_lib:map(fun(T) -> 7 | transform(erl_syntax:revert(T)) 8 | end, AST) || AST <- ASTs] 9 | catch 10 | _:_ -> 11 | ASTs 12 | end. 13 | 14 | transform({call, Line, {remote, _, {atom, _, ct}, {atom, _, pal}}, Args}) -> 15 | {call, Line, {remote, Line, {atom, Line, cthr}, {atom, Line, pal}}, Args}; 16 | transform(Term) -> 17 | Term. 18 | -------------------------------------------------------------------------------- /src/cthr.erl: -------------------------------------------------------------------------------- 1 | %% @doc Experimental 2 | -module(cthr). 3 | -include_lib("common_test/include/ct.hrl"). 4 | 5 | -ifndef(MAX_VERBOSITY). 6 | -define(R15_FALLBACK, true). 7 | %% the log level is used as argument to any CT logging function 8 | -define(MIN_IMPORTANCE, 0 ). 9 | -define(LOW_IMPORTANCE, 25). 10 | -define(STD_IMPORTANCE, 50). 11 | -define(HI_IMPORTANCE, 75). 12 | -define(MAX_IMPORTANCE, 99). 13 | 14 | %% verbosity thresholds to filter out logging printouts 15 | -define(MIN_VERBOSITY, 0 ). %% turn logging off 16 | -define(LOW_VERBOSITY, 25 ). 17 | -define(STD_VERBOSITY, 50 ). 18 | -define(HI_VERBOSITY, 75 ). 19 | -define(MAX_VERBOSITY, 100). 20 | -endif. 21 | 22 | -export([pal/1, pal/2, pal/3, pal/4, pal/5]). 23 | 24 | pal(Format) -> 25 | pal(default, ?STD_IMPORTANCE, Format, []). 26 | 27 | pal(X1,X2) -> 28 | {Category,Importance,Format,Args} = 29 | if is_atom(X1) -> {X1,?STD_IMPORTANCE,X2,[]}; 30 | is_integer(X1) -> {default,X1,X2,[]}; 31 | is_list(X1) -> {default,?STD_IMPORTANCE,X1,X2} 32 | end, 33 | pal(Category,Importance,Format,Args). 34 | 35 | pal(X1,X2,X3) -> 36 | {Category,Importance,Format,Args} = 37 | if is_atom(X1), is_integer(X2) -> {X1,X2,X3,[]}; 38 | is_atom(X1), is_list(X2) -> {X1,?STD_IMPORTANCE,X2,X3}; 39 | is_integer(X1) -> {default,X1,X2,X3} 40 | end, 41 | pal(Category,Importance,Format,Args). 42 | 43 | -ifdef(R15_FALLBACK). 44 | %% R15 and earlier didn't support log verbosity. 45 | 46 | pal(Category,_Importance,Format,Args) -> 47 | case whereis(cth_readable_failonly) of 48 | undefined -> % hook not running, passthrough 49 | ct_logs:tc_pal(Category,Format,Args); 50 | _ -> % hook running, take over 51 | Name = case erlang:function_exported(logger, module_info, 0) of 52 | true -> cth_readable_logger; 53 | false -> error_logger 54 | end, 55 | gen_event:call(Name, cth_readable_failonly, 56 | {ct_pal, format(Category,Format,Args)}), 57 | %% Send to ct group leader 58 | ct_logs:tc_log(Category, Format, Args), 59 | ok 60 | end. 61 | 62 | -else. 63 | 64 | pal(Category,Importance,Format,Args) -> 65 | case whereis(cth_readable_failonly) of 66 | undefined -> % hook not running, passthrough 67 | ct_logs:tc_pal(Category,Importance,Format,Args); 68 | _ -> % hook running, take over 69 | %% Send to error_logger, but only our own handler 70 | Name = case erlang:function_exported(logger, module_info, 0) of 71 | true -> cth_readable_logger; 72 | false -> error_logger 73 | end, 74 | gen_event:call(Name, cth_readable_failonly, 75 | {ct_pal, format(Category,Importance,Format,Args)}), 76 | %% Send to ct group leader 77 | ct_logs:tc_log(Category, Importance, Format, Args), 78 | ok 79 | end. 80 | 81 | pal(Category,Importance,Format,Args,Opts) -> 82 | case whereis(cth_readable_failonly) of 83 | undefined -> % hook not running, passthrough 84 | ct_logs:tc_pal(Category,Importance,Format,Args,Opts); 85 | _ -> % hook running, take over 86 | %% Send to error_logger, but only our own handler 87 | Name = case erlang:function_exported(logger, module_info, 0) of 88 | true -> cth_readable_logger; 89 | false -> error_logger 90 | end, 91 | gen_event:call(Name, cth_readable_failonly, 92 | {ct_pal, format(Category,Importance,Format,Args)}), 93 | %% Send to ct group leader 94 | ct_logs:tc_log(Category, Importance, Format, Args, Opts), 95 | ok 96 | end. 97 | 98 | -endif. 99 | %%% Replicate CT stuff but don't output it 100 | format(Category, Importance, Format, Args) -> 101 | VLvl = try ct_util:get_verbosity(Category) of 102 | undefined -> 103 | ct_util:get_verbosity('$unspecified'); 104 | {error,bad_invocation} -> 105 | ?MAX_VERBOSITY; 106 | {error,_Failure} -> 107 | ?MAX_VERBOSITY; 108 | Val -> 109 | Val 110 | catch error:undef -> 111 | ?MAX_VERBOSITY 112 | end, 113 | if Importance >= (100-VLvl) -> 114 | format(Category, Format, Args); 115 | true -> 116 | ignore 117 | end. 118 | 119 | format(Category, Format, Args) -> 120 | Head = get_heading(Category), 121 | io_lib:format(lists:concat([Head,Format,"\n\n"]), Args). 122 | 123 | get_heading(default) -> 124 | io_lib:format("\n-----------------------------" 125 | "-----------------------\n~s\n", 126 | [log_timestamp(os:timestamp())]); 127 | get_heading(Category) -> 128 | io_lib:format("\n-----------------------------" 129 | "-----------------------\n~s ~w\n", 130 | [log_timestamp(os:timestamp()),Category]). 131 | 132 | log_timestamp({MS,S,US}) -> 133 | put(log_timestamp, {MS,S,US}), 134 | {{Year,Month,Day}, {Hour,Min,Sec}} = 135 | calendar:now_to_local_time({MS,S,US}), 136 | MilliSec = trunc(US/1000), 137 | lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B " 138 | "~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0B", 139 | [Year,Month,Day,Hour,Min,Sec,MilliSec])). 140 | 141 | -------------------------------------------------------------------------------- /test/failonly_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(failonly_SUITE). 2 | 3 | -compile(export_all). 4 | 5 | -include_lib("common_test/include/ct.hrl"). 6 | 7 | 8 | all() -> 9 | [pal]. 10 | 11 | %% run with rebar3 ct --readable false 12 | %% and configure rebar.config with 13 | %% {ct_opts, [ 14 | % {ct_hooks, [{cth_readable_failonly, [{max_events, 2}]}, cth_readable_shell]} 15 | % ]}. 16 | pal() -> 17 | [ct:pal("Event ~p", [X]) || X <- lists:seq(0, 10)], 18 | error(crash). 19 | -------------------------------------------------------------------------------- /test/log_tests.erl: -------------------------------------------------------------------------------- 1 | -module(log_tests). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | 4 | %% run this to check interactions between lager and logger 5 | %% by calling `rebar3 do eunit, ct' 6 | start_test() -> 7 | {ok, Apps} = application:ensure_all_started(lager), 8 | io:format(user, "~p~n", [logger:get_handler_config()]), 9 | [application:stop(App) || App <- Apps]. 10 | -------------------------------------------------------------------------------- /test/macro_wrap.hrl: -------------------------------------------------------------------------------- 1 | -ifdef(no_logger_hrl). 2 | 3 | -define(LOG_EMERGENCY(A),?DO_LOG(emergency,[A])). 4 | -define(LOG_EMERGENCY(A,B),?DO_LOG(emergency,[A,B])). 5 | -define(LOG_EMERGENCY(A,B,C),?DO_LOG(emergency,[A,B,C])). 6 | 7 | -define(LOG_ALERT(A),?DO_LOG(alert,[A])). 8 | -define(LOG_ALERT(A,B),?DO_LOG(alert,[A,B])). 9 | -define(LOG_ALERT(A,B,C),?DO_LOG(alert,[A,B,C])). 10 | 11 | -define(LOG_CRITICAL(A),?DO_LOG(critical,[A])). 12 | -define(LOG_CRITICAL(A,B),?DO_LOG(critical,[A,B])). 13 | -define(LOG_CRITICAL(A,B,C),?DO_LOG(critical,[A,B,C])). 14 | 15 | -define(LOG_ERROR(A),?DO_LOG(error,[A])). 16 | -define(LOG_ERROR(A,B),?DO_LOG(error,[A,B])). 17 | -define(LOG_ERROR(A,B,C),?DO_LOG(error,[A,B,C])). 18 | 19 | -define(LOG_WARNING(A),?DO_LOG(warning,[A])). 20 | -define(LOG_WARNING(A,B),?DO_LOG(warning,[A,B])). 21 | -define(LOG_WARNING(A,B,C),?DO_LOG(warning,[A,B,C])). 22 | 23 | -define(LOG_NOTICE(A),?DO_LOG(notice,[A])). 24 | -define(LOG_NOTICE(A,B),?DO_LOG(notice,[A,B])). 25 | -define(LOG_NOTICE(A,B,C),?DO_LOG(notice,[A,B,C])). 26 | 27 | -define(LOG_INFO(A),?DO_LOG(info,[A])). 28 | -define(LOG_INFO(A,B),?DO_LOG(info,[A,B])). 29 | -define(LOG_INFO(A,B,C),?DO_LOG(info,[A,B,C])). 30 | 31 | -define(LOG_DEBUG(A),?DO_LOG(debug,[A])). 32 | -define(LOG_DEBUG(A,B),?DO_LOG(debug,[A,B])). 33 | -define(LOG_DEBUG(A,B,C),?DO_LOG(debug,[A,B,C])). 34 | 35 | -define(DO_LOG(_X,_Y), ok). 36 | 37 | -else. 38 | 39 | -include_lib("kernel/include/logger.hrl"). 40 | 41 | -endif. 42 | -------------------------------------------------------------------------------- /test/sample_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(sample_SUITE). 2 | 3 | -compile(export_all). 4 | 5 | -include_lib("common_test/include/ct.hrl"). 6 | 7 | init_per_suite(Config) -> 8 | Config. 9 | 10 | end_per_suite(_Config) -> 11 | ok. 12 | 13 | init_per_group(_GroupName, Config) -> 14 | Config. 15 | 16 | end_per_group(_GroupName, _Config) -> 17 | ok. 18 | 19 | init_per_testcase(_TestCase, Config) -> 20 | Config. 21 | 22 | end_per_testcase(_TestCase, _Config) -> 23 | ok = application:stop(foo), 24 | ok. 25 | 26 | groups() -> 27 | []. 28 | 29 | all() -> 30 | [my_test_case]. 31 | 32 | my_test_case() -> 33 | []. 34 | 35 | my_test_case(_Config) -> 36 | ok. 37 | -------------------------------------------------------------------------------- /test/show_logs_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(show_logs_SUITE). 2 | -compile(export_all). 3 | -include_lib("common_test/include/ct.hrl"). 4 | -include_lib("eunit/include/eunit.hrl"). 5 | -include("macro_wrap.hrl"). 6 | -compile({parse_transform, lager_transform}). 7 | 8 | all() -> [{group, works}, {group, fails}, skip]. 9 | 10 | groups() -> 11 | [{all, [], [error_logger, logger, sasl, ctpal, eunit, lager]}, 12 | {works, [], [{group, all}]}, 13 | {fails, [], [{group, all}]}]. 14 | 15 | init_per_group(fails, Config) -> 16 | [{fail, true} | Config]; 17 | init_per_group(works, Config) -> 18 | [{fail, false} | Config]; 19 | init_per_group(all, Config) -> 20 | Config. 21 | 22 | end_per_group(_, Config) -> 23 | Config. 24 | 25 | init_per_testcase(skip, _Config) -> 26 | {skip, manual}; 27 | init_per_testcase(logger, Config) -> 28 | case erlang:function_exported(logger, module_info, 0) of 29 | true -> Config; 30 | false -> {skip, not_supported} 31 | end; 32 | init_per_testcase(lager, Config) -> 33 | {ok, Apps} = application:ensure_all_started(lager), 34 | [{apps, Apps} | Config]; 35 | init_per_testcase(_, Config) -> 36 | Config. 37 | 38 | end_per_testcase(lager, Config) -> 39 | [application:stop(App) || App <- lists:reverse(?config(apps, Config))], 40 | Config; 41 | end_per_testcase(_, Config) -> 42 | Config. 43 | 44 | error_logger(Config) -> 45 | error_logger:error_msg("error\n"), 46 | error_logger:warning_msg("warn\n"), 47 | error_logger:info_msg("info\n"), 48 | ?config(fail, Config) andalso error(fail). 49 | 50 | logger(Config) -> 51 | logger:alert("alert\n"), 52 | logger:critical("critical\n"), 53 | ?LOG_ERROR("error\n"), 54 | ?LOG_WARNING("warn\n"), 55 | ?LOG_INFO("info\n"), 56 | ?config(fail, Config) andalso error(fail). 57 | 58 | 59 | sasl(Config) -> 60 | application:start(sasl), 61 | application:start(tools), 62 | %ct:pal("~p~n",[sys:get_state(error_logger)]), 63 | application:stop(tools), 64 | application:stop(sasl), 65 | ?config(fail, Config) andalso error(fail). 66 | 67 | ctpal(Config) -> 68 | ct:pal("ct:pal call"), 69 | ?config(fail, Config) andalso error(fail). 70 | 71 | eunit(Config) -> 72 | ?assertMatch(false, ?config(fail, Config)), 73 | ok. 74 | 75 | lager(Config) -> 76 | lager:error("error\n", []), 77 | lager:warning("warn\n", []), 78 | lager:info("info\n", []), 79 | ?config(fail, Config) andalso error(fail). 80 | 81 | skip(_Config) -> 82 | ok. 83 | --------------------------------------------------------------------------------