Repository: couchdb-config Updated Branches: refs/heads/master 693abb635 -> 12dcbf571
Clean up config_tests This cleans up the config_tests both stylistically as well as removes some race conditions around message passing from the config handler. This also reformats and changes a lot of the tests so that we're using consistent patterns through out the file. Unfortunately foreach and foreachx are terrible constructs and require using the `?_test/1` macro which is a bit annoying but ended up being the least worst approach I could find. COUCHDB-3096 Project: http://git-wip-us.apache.org/repos/asf/couchdb-config/repo Commit: http://git-wip-us.apache.org/repos/asf/couchdb-config/commit/ab551818 Tree: http://git-wip-us.apache.org/repos/asf/couchdb-config/tree/ab551818 Diff: http://git-wip-us.apache.org/repos/asf/couchdb-config/diff/ab551818 Branch: refs/heads/master Commit: ab5518188a50420142b1903f3c1c3c27554c0587 Parents: 693abb6 Author: Paul J. Davis <[email protected]> Authored: Fri Aug 5 14:49:17 2016 -0500 Committer: Paul J. Davis <[email protected]> Committed: Mon Aug 8 19:20:02 2016 -0500 ---------------------------------------------------------------------- test/config_tests.erl | 520 +++++++++++++++++++++++++-------------------- 1 file changed, 287 insertions(+), 233 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/couchdb-config/blob/ab551818/test/config_tests.erl ---------------------------------------------------------------------- diff --git a/test/config_tests.erl b/test/config_tests.erl index 802e0e3..2c7d13f 100644 --- a/test/config_tests.erl +++ b/test/config_tests.erl @@ -11,27 +11,36 @@ % the License. -module(config_tests). - -behaviour(config_listener). --export([handle_config_change/5, handle_config_terminate/3]). -include_lib("couch/include/couch_eunit.hrl"). -include_lib("couch/include/couch_db.hrl"). --define(SHORT_TIMEOUT, 100). --define(TIMEOUT, 1000). + +-export([ + handle_config_change/5, + handle_config_terminate/3 +]). + + +-define(TIMEOUT, 4000). -define(CONFIG_FIXTURESDIR, filename:join([?BUILDDIR(), "src", "config", "test", "fixtures"])). + -define(CONFIG_FIXTURE_1, filename:join([?CONFIG_FIXTURESDIR, "config_tests_1.ini"])). + -define(CONFIG_FIXTURE_2, filename:join([?CONFIG_FIXTURESDIR, "config_tests_2.ini"])). + -define(CONFIG_DEFAULT_D, filename:join([?CONFIG_FIXTURESDIR, "default.d"])). + -define(CONFIG_LOCAL_D, filename:join([?CONFIG_FIXTURESDIR, "local.d"])). + -define(CONFIG_FIXTURE_TEMP, begin FileName = filename:join([?TEMPDIR, "config_temp.ini"]), @@ -44,179 +53,188 @@ -define(DEPS, [couch_stats, couch_log, config]). +-define(T(F), {erlang:fun_to_list(F), F}). +-define(FEXT(F), fun(_, _) -> F() end). + + + setup() -> setup(?CONFIG_CHAIN). + setup({temporary, Chain}) -> setup(Chain); + setup({persistent, Chain}) -> - setup(lists:append(Chain, [?CONFIG_FIXTURE_TEMP])); + setup(Chain ++ [?CONFIG_FIXTURE_TEMP]); + setup(Chain) -> ok = application:set_env(config, ini_files, Chain), test_util:start_applications(?DEPS). + setup_empty() -> setup([]). + setup_config_listener() -> setup(), - {ok, Pid} = spawn_listener(), - config:listen_for_changes(?MODULE, {Pid, self(), []}), - {Pid, self()}. + spawn_config_listener(). + + +teardown(Pid) when is_pid(Pid) -> + catch exit(Pid, kill), + teardown(undefined); -teardown({Pid, _}) -> - stop_listener(Pid), - [application:stop(App) || App <- ?DEPS]; teardown(_) -> [application:stop(App) || App <- ?DEPS]. + teardown(_, _) -> - [application:stop(App) || App <- ?DEPS]. + teardown(undefined). + -handle_config_change("remove_handler", _Key, _Value, _Persist, _State) -> +handle_config_change("remove_handler", _Key, _Value, _Persist, {Pid, _State}) -> remove_handler; -handle_config_change("update_state", Key, _Value, _Persist, {Listener, Subscriber, Items}) -> - NewState = {Listener, Subscriber, [Key|Items]}, - ok = reply(NewState, NewState), - {ok, NewState}; -handle_config_change(Section, Key, Value, Persist, State) -> - ok = reply({{Section, Key, Value, Persist}, State}, State), - {ok, State}. -handle_config_terminate(Self, Reason, State) -> - ok = reply({stop, Self, Reason, State}, State), - ok. -reply(Reply, {Listener, _, _}) -> - call_sync(Listener, {set, Reply}). +handle_config_change("update_state", Key, Value, Persist, {Pid, State}) -> + Pid ! {config_msg, {{"update_state", Key, Value, Persist}, State}}, + {ok, {Pid, Key}}; -wait_reply(Listener) -> - call_sync(Listener, get). +handle_config_change(Section, Key, Value, Persist, {Pid, State}) -> + Pid ! {config_msg, {{Section, Key, Value, Persist}, State}}, + {ok, {Pid, State}}. + + +handle_config_terminate(Self, Reason, {Pid, State}) -> + Pid ! {config_msg, {Self, Reason, State}}, + ok. -config_test_() -> - { - "CouchDB config tests", - [ - config_get_tests(), - config_set_tests(), - config_del_tests(), - config_override_tests(), - config_persistent_changes_tests(), - config_no_files_tests(), - config_listener_behaviour_tests() - ] - }. -config_get_tests() -> +config_get_test_() -> { "Config get tests", { foreach, - fun setup/0, fun teardown/1, + fun setup/0, + fun teardown/1, [ - should_load_all_configs(), - should_locate_daemons_section(), - should_locate_mrview_handler(), - should_return_undefined_atom_on_missed_section(), - should_return_undefined_atom_on_missed_option(), - should_return_custom_default_value_on_missed_option(), - should_only_return_default_on_missed_option(), - should_fail_to_get_binary_value(), - should_return_any_supported_default() + fun should_load_all_configs/0, + fun should_locate_daemons_section/0, + fun should_locate_mrview_handler/0, + fun should_return_undefined_atom_on_missed_section/0, + fun should_return_undefined_atom_on_missed_option/0, + fun should_return_custom_default_value_on_missed_option/0, + fun should_only_return_default_on_missed_option/0, + fun should_fail_to_get_binary_value/0, + fun should_return_any_supported_default/0 ] } }. -config_set_tests() -> + +config_set_test_() -> { "Config set tests", { foreach, - fun setup/0, fun teardown/1, + fun setup/0, + fun teardown/1, [ - should_update_option(), - should_create_new_section(), - should_fail_to_set_binary_value() + fun should_update_option/0, + fun should_create_new_section/0, + fun should_fail_to_set_binary_value/0 ] } }. -config_del_tests() -> + +config_del_test_() -> { "Config deletion tests", { foreach, - fun setup/0, fun teardown/1, + fun setup/0, + fun teardown/1, [ - should_return_undefined_atom_after_option_deletion(), - should_be_ok_on_deleting_unknown_options() + fun should_return_undefined_atom_after_option_deletion/0, + fun should_be_ok_on_deleting_unknown_options/0 ] } }. -config_override_tests() -> + +config_override_test_() -> { "Configs overide tests", { foreachx, - fun setup/1, fun teardown/2, + fun setup/1, + fun teardown/2, [ {{temporary, [?CONFIG_DEFAULT]}, - fun should_ensure_in_defaults/2}, - {{temporary, [?CONFIG_DEFAULT, ?CONFIG_FIXTURE_1]}, - fun should_override_options/2}, + fun should_ensure_in_defaults/2}, {{temporary, [?CONFIG_DEFAULT, ?CONFIG_FIXTURE_1]}, - fun should_override_options/2}, - {{temporary, [?CONFIG_DEFAULT, ?CONFIG_DEFAULT_D]}, - fun(_, _) -> ?_assertEqual("11", config:get("couchdb", "max_dbs_open")) end}, - {{temporary, [?CONFIG_DEFAULT, ?CONFIG_LOCAL_D]}, - fun(_, _) -> ?_assertEqual("12", config:get("couchdb", "max_dbs_open")) end}, - {{temporary, [?CONFIG_DEFAULT, ?CONFIG_DEFAULT_D, ?CONFIG_LOCAL_D]}, - fun(_, _) -> ?_assertEqual("12", config:get("couchdb", "max_dbs_open")) end}, + fun should_override_options/2}, {{temporary, [?CONFIG_DEFAULT, ?CONFIG_FIXTURE_2]}, - fun should_create_new_sections_on_override/2}, + fun should_create_new_sections_on_override/2}, {{temporary, [?CONFIG_DEFAULT, ?CONFIG_FIXTURE_1, - ?CONFIG_FIXTURE_2]}, - fun should_win_last_in_chain/2} + ?CONFIG_FIXTURE_2]}, + fun should_win_last_in_chain/2}, + {{temporary, [?CONFIG_DEFAULT, ?CONFIG_DEFAULT_D]}, + fun should_read_default_d/2}, + {{temporary, [?CONFIG_DEFAULT, ?CONFIG_LOCAL_D]}, + fun should_read_local_d/2}, + {{temporary, [?CONFIG_DEFAULT, ?CONFIG_DEFAULT_D, + ?CONFIG_LOCAL_D]}, + fun should_read_default_and_local_d/2} ] } }. -config_persistent_changes_tests() -> + +config_persistent_changes_test_() -> { "Config persistent changes", { foreachx, - fun setup/1, fun teardown/2, + fun setup/1, + fun teardown/2, [ {{persistent, [?CONFIG_DEFAULT]}, - fun should_write_changes/2}, + fun should_write_changes/2}, {{temporary, [?CONFIG_DEFAULT]}, - fun should_ensure_that_default_wasnt_modified/2}, + fun should_ensure_default_wasnt_modified/2}, {{temporary, [?CONFIG_FIXTURE_TEMP]}, - fun should_ensure_that_written_to_last_config_in_chain/2} + fun should_ensure_written_to_last_config_in_chain/2} ] } }. -config_no_files_tests() -> + +config_no_files_test_() -> { "Test config with no files", { foreach, - fun setup_empty/0, fun teardown/1, + fun setup_empty/0, + fun teardown/1, [ - should_ensure_that_no_ini_files_loaded(), - should_create_non_persistent_option(), - should_create_persistent_option() + fun should_ensure_that_no_ini_files_loaded/0, + fun should_create_non_persistent_option/0, + fun should_create_persistent_option/0 ] } }. -config_listener_behaviour_tests() -> + +config_listener_behaviour_test_() -> { "Test config_listener behaviour", { foreach, - fun setup_config_listener/0, fun teardown/1, + local, + fun setup_config_listener/0, + fun teardown/1, [ fun should_handle_value_change/1, fun should_pass_correct_state_to_handle_config_change/1, @@ -228,233 +246,269 @@ config_listener_behaviour_tests() -> } }. + should_load_all_configs() -> - ?_assert(length(config:all()) > 0). + ?assert(length(config:all()) > 0). + should_locate_daemons_section() -> - ?_assert(length(config:get("daemons")) > 0). + ?assert(length(config:get("daemons")) > 0). + should_locate_mrview_handler() -> - ?_assertEqual("{couch_mrview_http, handle_view_req}", - config:get("httpd_design_handlers", "_view")). + Expect = "{couch_mrview_http, handle_view_req}", + ?assertEqual(Expect, config:get("httpd_design_handlers", "_view")). + should_return_undefined_atom_on_missed_section() -> - ?_assertEqual(undefined, - config:get("foo", "bar")). + ?assertEqual(undefined, config:get("foo", "bar")). + should_return_undefined_atom_on_missed_option() -> - ?_assertEqual(undefined, - config:get("httpd", "foo")). + ?assertEqual(undefined, config:get("httpd", "foo")). + should_return_custom_default_value_on_missed_option() -> - ?_assertEqual("bar", - config:get("httpd", "foo", "bar")). + ?assertEqual("bar", config:get("httpd", "foo", "bar")). + should_only_return_default_on_missed_option() -> - ?_assertEqual("0", - config:get("httpd", "port", "bar")). + ?assertEqual("0", config:get("httpd", "port", "bar")). + should_fail_to_get_binary_value() -> - ?_assertException(error, badarg, - config:get(<<"foo">>, <<"bar">>, <<"baz">>)). + ?assertException(error, badarg, config:get(<<"a">>, <<"b">>, <<"c">>)). + should_return_any_supported_default() -> Values = [undefined, "list", true, false, 0.1, 1], - Tests = [{lists:flatten(io_lib:format("for type(~p)", [V])), V} - || V <- Values], - [{T, ?_assertEqual(V, config:get(<<"foo">>, <<"bar">>, V))} - || {T, V} <- Tests]. + lists:map(fun(V) -> + ?assertEqual(V, config:get(<<"foo">>, <<"bar">>, V)) + end, Values). + should_update_option() -> - ?_assertEqual("severe", - begin - ok = config:set("mock_log", "level", "severe", false), - config:get("mock_log", "level") - end). + ok = config:set("mock_log", "level", "severe", false), + ?assertEqual("severe", config:get("mock_log", "level")). + should_create_new_section() -> - ?_assertEqual("bang", - begin - undefined = config:get("new_section", "bizzle"), - ok = config:set("new_section", "bizzle", "bang", false), - config:get("new_section", "bizzle") - end). + ?assertEqual(undefined, config:get("new_section", "bizzle")), + ?assertEqual(ok, config:set("new_section", "bizzle", "bang", false)), + ?assertEqual("bang", config:get("new_section", "bizzle")). + should_fail_to_set_binary_value() -> - ?_assertException(error, badarg, - config:set(<<"foo">>, <<"bar">>, <<"baz">>, false)). + ?assertException(error, badarg, + config:set(<<"a">>, <<"b">>, <<"c">>, false)). + should_return_undefined_atom_after_option_deletion() -> - ?_assertEqual(undefined, - begin - ok = config:delete("mock_log", "level", false), - config:get("mock_log", "level") - end). + ?assertEqual(ok, config:delete("mock_log", "level", false)), + ?assertEqual(undefined, config:get("mock_log", "level")). + should_be_ok_on_deleting_unknown_options() -> - ?_assertEqual(ok, config:delete("zoo", "boo", false)). + ?assertEqual(ok, config:delete("zoo", "boo", false)). + should_ensure_in_defaults(_, _) -> ?_test(begin - ?assertEqual("500", - config:get("couchdb", "max_dbs_open")), - ?assertEqual("5986", - config:get("httpd", "port")), - ?assertEqual(undefined, - config:get("fizbang", "unicode")) + ?assertEqual("500", config:get("couchdb", "max_dbs_open")), + ?assertEqual("5986", config:get("httpd", "port")), + ?assertEqual(undefined, config:get("fizbang", "unicode")) end). + should_override_options(_, _) -> ?_test(begin - ?assertEqual("10", - config:get("couchdb", "max_dbs_open")), - ?assertEqual("4895", - config:get("httpd", "port")) + ?assertEqual("10", config:get("couchdb", "max_dbs_open")), + ?assertEqual("4895", config:get("httpd", "port")) + end). + + +should_read_default_d(_, _) -> + ?_test(begin + ?assertEqual("11", config:get("couchdb", "max_dbs_open")) + end). + + +should_read_local_d(_, _) -> + ?_test(begin + ?assertEqual("12", config:get("couchdb", "max_dbs_open")) + end). + + +should_read_default_and_local_d(_, _) -> + ?_test(begin + ?assertEqual("12", config:get("couchdb", "max_dbs_open")) end). + should_create_new_sections_on_override(_, _) -> ?_test(begin - ?assertEqual("80", - config:get("httpd", "port")), - ?assertEqual("normalized", - config:get("fizbang", "unicode")) + ?assertEqual("80", config:get("httpd", "port")), + ?assertEqual("normalized", config:get("fizbang", "unicode")) end). + should_win_last_in_chain(_, _) -> - ?_assertEqual("80", config:get("httpd", "port")). + ?_test(begin + ?assertEqual("80", config:get("httpd", "port")) + end). + should_write_changes(_, _) -> ?_test(begin - ?assertEqual("5986", - config:get("httpd", "port")), - ?assertEqual(ok, - config:set("httpd", "port", "8080")), - ?assertEqual("8080", - config:get("httpd", "port")), - ?assertEqual(ok, - config:delete("httpd", "bind_address", "8080")), - ?assertEqual(undefined, - config:get("httpd", "bind_address")) + ?assertEqual("5986", config:get("httpd", "port")), + ?assertEqual(ok, config:set("httpd", "port", "8080")), + ?assertEqual("8080", config:get("httpd", "port")), + ?assertEqual(ok, config:delete("httpd", "bind_address", "8080")), + ?assertEqual(undefined, config:get("httpd", "bind_address")) end). -should_ensure_that_default_wasnt_modified(_, _) -> + +should_ensure_default_wasnt_modified(_, _) -> ?_test(begin - ?assertEqual("5986", - config:get("httpd", "port")), - ?assertEqual("127.0.0.1", - config:get("httpd", "bind_address")) + ?assertEqual("5986", config:get("httpd", "port")), + ?assertEqual("127.0.0.1", config:get("httpd", "bind_address")) end). -should_ensure_that_written_to_last_config_in_chain(_, _) -> + +should_ensure_written_to_last_config_in_chain(_, _) -> ?_test(begin - ?assertEqual("8080", - config:get("httpd", "port")), - ?assertEqual(undefined, - config:get("httpd", "bind_address")) + ?assertEqual("8080", config:get("httpd", "port")), + ?assertEqual(undefined, config:get("httpd", "bind_address")) end). + should_ensure_that_no_ini_files_loaded() -> - ?_assertEqual(0, length(config:all())). + ?assertEqual(0, length(config:all())). + should_create_non_persistent_option() -> - ?_assertEqual("80", - begin - ok = config:set("httpd", "port", "80", false), - config:get("httpd", "port") - end). + ?_test(begin + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + ?assertEqual("80", config:get("httpd", "port")) + end). -should_create_persistent_option() -> - ?_assertEqual("127.0.0.1", - begin - ok = config:set("httpd", "bind_address", "127.0.0.1"), - config:get("httpd", "bind_address") - end). -should_handle_value_change({Pid, _}) -> +should_create_persistent_option() -> ?_test(begin - ok = config:set("httpd", "port", "80", false), - ?assertMatch({{"httpd", "port", "80", false}, _}, wait_reply(Pid)) + ?assertEqual(ok, config:set("httpd", "bind_address", "127.0.0.1")), + ?assertEqual("127.0.0.1", config:get("httpd", "bind_address")) end). -should_pass_correct_state_to_handle_config_change({Pid, _}) -> + + +should_handle_value_change(Pid) -> ?_test(begin - ok = config:set("httpd", "port", "80", false), - ?assertMatch({_, {Pid, _, []}}, wait_reply(Pid)), - ok = config:set("update_state", "foo", "any", false), - ?assertMatch({Pid, _, ["foo"]}, wait_reply(Pid)) + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + ?assertMatch({{"httpd", "port", "80", false}, _}, getmsg(Pid)) end). -should_pass_correct_state_to_handle_config_terminate({Pid, _}) -> + + +should_pass_correct_state_to_handle_config_change(Pid) -> ?_test(begin - %% prepare some state - ok = config:set("httpd", "port", "80", false), - ?assertMatch({_, {Pid, _, []}}, wait_reply(Pid)), - ok = config:set("update_state", "foo", "any", false), - ?assertMatch({Pid, _, ["foo"]}, wait_reply(Pid)), - - %% remove handler - ok = config:set("remove_handler", "any", "any", false), - Reply = wait_reply(Pid), - ?assertMatch({stop, _, remove_handler, _}, Reply), - - {stop, Subscriber, _Reason, State} = Reply, - ?assert(is_pid(Subscriber)), - ?assertMatch({Pid, Subscriber, ["foo"]}, State) + ?assertEqual(ok, config:set("update_state", "foo", "any", false)), + ?assertMatch({_, undefined}, getmsg(Pid)), + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + ?assertMatch({_, "foo"}, getmsg(Pid)) end). -should_pass_subscriber_pid_to_handle_config_terminate({Pid, SubscriberPid}) -> - ?_test(begin - ok = config:set("remove_handler", "any", "any", false), - Reply = wait_reply(Pid), - ?assertMatch({stop, _, remove_handler, _}, Reply), - {stop, Subscriber, _Reason, _State} = Reply, - ?assertMatch(SubscriberPid, Subscriber) - end). -should_not_call_handle_config_after_related_process_death({Pid, _}) -> + +should_pass_correct_state_to_handle_config_terminate(Pid) -> ?_test(begin - ok = config:set("remove_handler", "any", "any", false), - Reply = wait_reply(Pid), - ?assertMatch({stop, _, remove_handler, _}, Reply), + ?assertEqual(ok, config:set("update_state", "foo", "any", false)), + ?assertMatch({_, undefined}, getmsg(Pid)), + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + ?assertMatch({_, "foo"}, getmsg(Pid)), + ?assertEqual(ok, config:set("remove_handler", "any", "any", false)), + ?assertEqual({Pid, remove_handler, "foo"}, getmsg(Pid)) + end). - ok = config:set("httpd", "port", "80", false), - ?assertMatch(undefined, wait_reply(Pid)) + +should_pass_subscriber_pid_to_handle_config_terminate(Pid) -> + ?_test(begin + ?assertEqual(ok, config:set("remove_handler", "any", "any", false)), + ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)) end). -should_remove_handler_when_requested({Pid, _}) -> + + +should_not_call_handle_config_after_related_process_death(Pid) -> ?_test(begin - ?assertEqual(2, n_handlers()), + ?assertEqual(ok, config:set("remove_handler", "any", "any", false)), + ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)), + ?assertEqual(ok, config:set("httpd", "port", "80", false)), + Event = receive + {config_msg, _} -> got_msg + after 250 -> no_msg + end, + ?assertEqual(no_msg, Event) + end). - ok = config:set("remove_handler", "any", "any", false), - Reply = wait_reply(Pid), - ?assertMatch({stop, _, remove_handler, _}, Reply), +should_remove_handler_when_requested(Pid) -> + ?_test(begin + ?assertEqual(2, n_handlers()), + ?assertEqual(ok, config:set("remove_handler", "any", "any", false)), + ?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)), ?assertEqual(1, n_handlers()) end). -call_sync(Listener, Msg) -> - Ref = make_ref(), - Listener ! {Ref, self(), Msg}, + +spawn_config_listener() -> + Self = self(), + Pid = erlang:spawn(fun() -> + ok = config:listen_for_changes(?MODULE, {self(), undefined}), + Self ! registered, + loop(undefined) + end), receive - {ok, Ref, Reply} -> Reply + registered -> ok after ?TIMEOUT -> - throw({error, {timeout, call_sync}}) - end. + erlang:error({timeout, config_handler_register}) + end, + Pid. -spawn_listener() -> - {ok, spawn(fun() -> loop(undefined) end)}. -stop_listener(Listener) -> - call_sync(Listener, stop). +loop(undefined) -> + receive + {config_msg, _} = Msg -> + loop(Msg); + {get_msg, _, _} = Msg -> + loop(Msg); + Msg -> + erlang:error({invalid_message, Msg}) + end; + +loop({get_msg, From, Ref}) -> + receive + {config_msg, _} = Msg -> + From ! {Ref, Msg}; + Msg -> + erlang:error({invalid_message, Msg}) + end, + loop(undefined); + +loop({config_msg, _} = Msg) -> + receive + {get_msg, From, Ref} -> + From ! {Ref, Msg}; + Msg -> + erlang:error({invalid_message, Msg}) + end, + loop(undefined). -loop(State) -> + +getmsg(Pid) -> + Ref = erlang:make_ref(), + Pid ! {get_msg, self(), Ref}, receive - {Ref, From, stop} -> - From ! {ok, Ref, ok}, - ok; - {Ref, From, {set, Value}} -> - From ! {ok, Ref, ok}, - loop(Value); - {Ref, From, get} -> - From ! {ok, Ref, State}, - loop(undefined) + {Ref, {config_msg, Msg}} -> Msg + after ?TIMEOUT -> + erlang:error({timeout, config_msg}) end. + n_handlers() -> length(gen_event:which_handlers(config_event)).
