blob: 7a5b56d0f6ddefbcbb0cb4455337766a566b2b70 [file] [log] [blame]
% Licensed under the Apache License, Version 2.0 (the "License"); you may not
% use this file except in compliance with the License. You may obtain a copy of
% the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
% License for the specific language governing permissions and limitations under
% the License.
-module(config_tests).
-behaviour(config_listener).
-include_lib("couch/include/couch_eunit.hrl").
-include_lib("couch/include/couch_db.hrl").
-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"]),
{ok, Fd} = file:open(FileName, write),
ok = file:truncate(Fd),
ok = file:close(Fd),
FileName
end).
-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(Chain ++ [?CONFIG_FIXTURE_TEMP]);
setup(Chain) ->
meck:new(couch_log),
meck:expect(couch_log, error, fun(_, _) -> ok end),
meck:expect(couch_log, notice, fun(_, _) -> ok end),
meck:expect(couch_log, debug, fun(_, _) -> ok end),
ok = application:set_env(config, ini_files, Chain),
test_util:start_applications([config]).
setup_empty() ->
setup([]).
setup_config_listener() ->
Apps = setup(),
Pid = spawn_config_listener(),
{Apps, Pid}.
setup_config_notifier(Subscription) ->
Apps = setup(),
Pid = spawn_config_notifier(Subscription),
{Apps, Pid}.
teardown({Apps, Pid}) when is_pid(Pid) ->
catch exit(Pid, kill),
teardown(Apps);
teardown(Apps) when is_list(Apps) ->
meck:unload(),
test_util:stop_applications(Apps).
teardown(_, {Apps, Pid}) when is_pid(Pid) ->
catch exit(Pid, kill),
teardown(Apps);
teardown(_, Apps) ->
teardown(Apps).
handle_config_change("remove_handler", _Key, _Value, _Persist, {_Pid, _State}) ->
remove_handler;
handle_config_change("update_state", Key, Value, Persist, {Pid, State}) ->
Pid ! {config_msg, {{"update_state", Key, Value, Persist}, State}},
{ok, {Pid, Key}};
handle_config_change("throw_error", _Key, _Value, _Persist, {_Pid, _State}) ->
throw(this_is_an_error);
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_get_test_() ->
{
"Config get tests",
{
foreach,
fun setup/0,
fun teardown/1,
[
fun should_load_all_configs/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_test_() ->
{
"Config set tests",
{
foreach,
fun setup/0,
fun teardown/1,
[
fun should_update_option/0,
fun should_create_new_section/0,
fun should_fail_to_set_binary_value/0
]
}
}.
config_del_test_() ->
{
"Config deletion tests",
{
foreach,
fun setup/0,
fun teardown/1,
[
fun should_return_undefined_atom_after_option_deletion/0,
fun should_be_ok_on_deleting_unknown_options/0
]
}
}.
config_features_test_() ->
{
"Config features tests",
{
foreach,
fun setup/0,
fun teardown/1,
[
fun should_enable_features/0,
fun should_disable_features/0
]
}
}.
config_override_test_() ->
{
"Configs overide tests",
{
foreachx,
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},
{{temporary, [?CONFIG_DEFAULT, ?CONFIG_FIXTURE_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},
{{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_test_() ->
{
"Config persistent changes",
{
foreachx,
fun setup/1,
fun teardown/2,
[
{{persistent, [?CONFIG_DEFAULT]},
fun should_write_changes/2},
{{temporary, [?CONFIG_DEFAULT]},
fun should_ensure_default_wasnt_modified/2},
{{temporary, [?CONFIG_FIXTURE_TEMP]},
fun should_ensure_written_to_last_config_in_chain/2}
]
}
}.
config_no_files_test_() ->
{
"Test config with no files",
{
foreach,
fun setup_empty/0,
fun teardown/1,
[
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_test_() ->
{
"Test config_listener behaviour",
{
foreach,
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,
fun should_pass_correct_state_to_handle_config_terminate/1,
fun should_pass_subscriber_pid_to_handle_config_terminate/1,
fun should_not_call_handle_config_after_related_process_death/1,
fun should_remove_handler_when_requested/1,
fun should_remove_handler_when_pid_exits/1,
fun should_stop_monitor_on_error/1
]
}
}.
config_notifier_behaviour_test_() ->
{
"Test config_notifier behaviour",
{
foreachx,
local,
fun setup_config_notifier/1,
fun teardown/2,
[
{all, fun should_notify/2},
{["section_foo"], fun should_notify/2},
{[{"section_foo", "key_bar"}], fun should_notify/2},
{["section_foo"], fun should_not_notify/2},
{[{"section_foo", "key_bar"}], fun should_not_notify/2},
{all, fun should_unsubscribe_when_subscriber_gone/2},
{all, fun should_not_add_duplicate/2}
]
}
}.
should_load_all_configs() ->
?assert(length(config:all()) > 0).
should_return_undefined_atom_on_missed_section() ->
?assertEqual(undefined, config:get("foo", "bar")).
should_return_undefined_atom_on_missed_option() ->
?assertEqual(undefined, config:get("httpd", "foo")).
should_return_custom_default_value_on_missed_option() ->
?assertEqual("bar", config:get("httpd", "foo", "bar")).
should_only_return_default_on_missed_option() ->
?assertEqual("0", config:get("httpd", "port", "bar")).
should_fail_to_get_binary_value() ->
?assertException(error, badarg, config:get(<<"a">>, <<"b">>, <<"c">>)).
should_return_any_supported_default() ->
Values = [undefined, "list", true, false, 0.1, 1],
lists:map(fun(V) ->
?assertEqual(V, config:get(<<"foo">>, <<"bar">>, V))
end, Values).
should_update_option() ->
ok = config:set("mock_log", "level", "severe", false),
?assertEqual("severe", config:get("mock_log", "level")).
should_create_new_section() ->
?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(<<"a">>, <<"b">>, <<"c">>, false)).
should_return_undefined_atom_after_option_deletion() ->
?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)).
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"))
end).
should_override_options(_, _) ->
?_test(begin
?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"))
end).
should_win_last_in_chain(_, _) ->
?_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"))
end).
should_ensure_default_wasnt_modified(_, _) ->
?_test(begin
?assertEqual("5986", config:get("httpd", "port")),
?assertEqual("127.0.0.1", config:get("httpd", "bind_address"))
end).
should_ensure_written_to_last_config_in_chain(_, _) ->
?_test(begin
?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())).
should_create_non_persistent_option() ->
?_test(begin
?assertEqual(ok, config:set("httpd", "port", "80", false)),
?assertEqual("80", config:get("httpd", "port"))
end).
should_create_persistent_option() ->
?_test(begin
?assertEqual(ok, config:set("httpd", "bind_address", "127.0.0.1")),
?assertEqual("127.0.0.1", config:get("httpd", "bind_address"))
end).
should_handle_value_change({_Apps, Pid}) ->
?_test(begin
?assertEqual(ok, config:set("httpd", "port", "80", false)),
?assertMatch({{"httpd", "port", "80", false}, _}, getmsg(Pid))
end).
should_pass_correct_state_to_handle_config_change({_Apps, Pid}) ->
?_test(begin
?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_correct_state_to_handle_config_terminate({_Apps, Pid}) ->
?_test(begin
?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).
should_pass_subscriber_pid_to_handle_config_terminate({_Apps, Pid}) ->
?_test(begin
?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid))
end).
should_not_call_handle_config_after_related_process_death({_Apps, Pid}) ->
?_test(begin
?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).
should_remove_handler_when_requested({_Apps, Pid}) ->
?_test(begin
?assertEqual(1, n_handlers()),
?assertEqual(ok, config:set("remove_handler", "any", "any", false)),
?assertEqual({Pid, remove_handler, undefined}, getmsg(Pid)),
?assertEqual(0, n_handlers())
end).
should_remove_handler_when_pid_exits({_Apps, Pid}) ->
?_test(begin
?assertEqual(1, n_handlers()),
% Monitor the config_listener_mon process
{monitored_by, [Mon]} = process_info(Pid, monitored_by),
MonRef = erlang:monitor(process, Mon),
% Kill the process synchronously
PidRef = erlang:monitor(process, Pid),
exit(Pid, kill),
receive
{'DOWN', PidRef, _, _, _} -> ok
after ?TIMEOUT ->
erlang:error({timeout, config_listener_death})
end,
% Wait for the config_listener_mon process to
% exit to indicate the handler has been removed.
receive
{'DOWN', MonRef, _, _, normal} -> ok
after ?TIMEOUT ->
erlang:error({timeout, config_listener_mon_death})
end,
?assertEqual(0, n_handlers())
end).
should_stop_monitor_on_error({_Apps, Pid}) ->
?_test(begin
?assertEqual(1, n_handlers()),
% Monitor the config_listener_mon process
{monitored_by, [Mon]} = process_info(Pid, monitored_by),
MonRef = erlang:monitor(process, Mon),
% Have the process throw an error
?assertEqual(ok, config:set("throw_error", "foo", "bar", false)),
% Make sure handle_config_terminate is called
?assertEqual({Pid, {error, this_is_an_error}, undefined}, getmsg(Pid)),
% Wait for the config_listener_mon process to
% exit to indicate the handler has been removed
% due to an error
receive
{'DOWN', MonRef, _, _, shutdown} -> ok
after ?TIMEOUT ->
erlang:error({timeout, config_listener_mon_shutdown})
end,
?assertEqual(0, n_handlers())
end).
should_notify(Subscription, {_Apps, Pid}) ->
{to_string(Subscription), ?_test(begin
?assertEqual(ok, config:set("section_foo", "key_bar", "any", false)),
?assertEqual({config_change,"section_foo", "key_bar", "any", false}, getmsg(Pid)),
ok
end)}.
should_not_notify([{Section, _}] = Subscription, {_Apps, Pid}) ->
{to_string(Subscription), ?_test(begin
?assertEqual(ok, config:set(Section, "any", "any", false)),
?assertError({timeout, config_msg}, getmsg(Pid)),
ok
end)};
should_not_notify(Subscription, {_Apps, Pid}) ->
{to_string(Subscription), ?_test(begin
?assertEqual(ok, config:set("any", "any", "any", false)),
?assertError({timeout, config_msg}, getmsg(Pid)),
ok
end)}.
should_unsubscribe_when_subscriber_gone(_Subscription, {_Apps, Pid}) ->
?_test(begin
?assertEqual(1, n_notifiers()),
?assert(is_process_alive(Pid)),
% Monitor subscriber process
MonRef = erlang:monitor(process, Pid),
exit(Pid, kill),
% Wait for the subscriber process to exit
receive
{'DOWN', MonRef, _, _, _} -> ok
after ?TIMEOUT ->
erlang:error({timeout, config_notifier_shutdown})
end,
?assertNot(is_process_alive(Pid)),
?assertEqual(0, n_notifiers()),
ok
end).
should_not_add_duplicate(_, _) ->
?_test(begin
?assertEqual(1, n_notifiers()), %% spawned from setup
?assertMatch(ok, config:subscribe_for_changes(all)),
?assertEqual(2, n_notifiers()),
?assertMatch(ok, config:subscribe_for_changes(all)),
?assertEqual(2, n_notifiers()),
ok
end).
should_enable_features() ->
?assertEqual([], config:features()),
?assertEqual(ok, config:enable_feature(snek)),
?assertEqual([snek], config:features()),
?assertEqual(ok, config:enable_feature(snek)),
?assertEqual([snek], config:features()),
?assertEqual(ok, config:enable_feature(dogo)),
?assertEqual([dogo, snek], config:features()).
should_disable_features() ->
?assertEqual([], config:features()),
config:enable_feature(snek),
?assertEqual([snek], config:features()),
?assertEqual(ok, config:disable_feature(snek)),
?assertEqual([], config:features()),
?assertEqual(ok, config:disable_feature(snek)),
?assertEqual([], config:features()).
spawn_config_listener() ->
Self = self(),
Pid = erlang:spawn(fun() ->
ok = config:listen_for_changes(?MODULE, {self(), undefined}),
Self ! registered,
loop(undefined)
end),
receive
registered -> ok
after ?TIMEOUT ->
erlang:error({timeout, config_handler_register})
end,
Pid.
spawn_config_notifier(Subscription) ->
Self = self(),
Pid = erlang:spawn(fun() ->
ok = config:subscribe_for_changes(Subscription),
Self ! registered,
loop(undefined)
end),
receive
registered -> ok
after ?TIMEOUT ->
erlang:error({timeout, config_handler_register})
end,
Pid.
loop(undefined) ->
receive
{config_msg, _} = Msg ->
loop(Msg);
{config_change, _, _, _, _} = Msg ->
loop({config_msg, 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};
{config_change, _, _, _, _} = 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).
getmsg(Pid) ->
Ref = erlang:make_ref(),
Pid ! {get_msg, self(), Ref},
receive
{Ref, {config_msg, Msg}} -> Msg
after ?TIMEOUT ->
erlang:error({timeout, config_msg})
end.
n_handlers() ->
Handlers = gen_event:which_handlers(config_event),
length([Pid || {config_listener, {?MODULE, Pid}} <- Handlers]).
n_notifiers() ->
Handlers = gen_event:which_handlers(config_event),
length([Pid || {config_notifier, Pid} <- Handlers]).
to_string(Term) ->
lists:flatten(io_lib:format("~p", [Term])).