You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by va...@apache.org on 2022/10/26 06:09:19 UTC
[couchdb] 01/01: Integrate config app into main repo
This is an automated email from the ASF dual-hosted git repository.
vatamane pushed a commit to branch integrate-config
in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit efb6ea75c013c2ba0f503649736c6678eda1f0a7
Author: Nick Vatamaniuc <va...@apache.org>
AuthorDate: Wed Oct 26 02:03:44 2022 -0400
Integrate config app into main repo
As per concensus in ML discussion https://lists.apache.org/thread/9dphqb6mjh1v234v15rcft7mfpjx9223
---
.gitignore | 1 -
rebar.config.script | 2 +-
src/config/LICENSE | 202 ++++++
src/config/rebar.config | 4 +
src/config/src/config.app.src.script | 32 +
src/config/src/config_app.erl | 66 ++
src/config/src/config_listener.erl | 71 ++
src/config/src/config_listener_mon.erl | 90 +++
src/config/src/config_notifier.erl | 78 +++
src/config/src/config_sup.erl | 50 ++
src/config/src/config_util.erl | 75 ++
src/config/src/config_writer.erl | 79 +++
src/config/test/config_tests.erl | 855 +++++++++++++++++++++++
src/config/test/fixtures/config_default_test.ini | 23 +
src/config/test/fixtures/config_tests_1.ini | 22 +
src/config/test/fixtures/config_tests_2.ini | 22 +
src/config/test/fixtures/default.d/extra.ini | 19 +
src/config/test/fixtures/local.d/extra.ini | 19 +
18 files changed, 1708 insertions(+), 2 deletions(-)
diff --git a/.gitignore b/.gitignore
index 518ec7b19..e47d83e21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,7 +42,6 @@ share/www
src/b64url/
src/bear/
src/certifi/
-src/config/
src/couch/priv/couch_js/**/config.h
src/couch/priv/couchjs
src/couch/priv/couchspawnkillable
diff --git a/rebar.config.script b/rebar.config.script
index bfae0c85f..3b91431f5 100644
--- a/rebar.config.script
+++ b/rebar.config.script
@@ -108,6 +108,7 @@ os:putenv("COUCHDB_APPS_CONFIG_DIR", filename:join([COUCHDB_ROOT, "rel/apps"])).
SubDirs = [
%% must be compiled first as it has a custom behavior
"src/couch_epi",
+ "src/config",
"src/couch_log",
"src/chttpd",
"src/couch",
@@ -141,7 +142,6 @@ SubDirs = [
DepDescs = [
%% Independent Apps
-{config, "config", {tag, "2.2.1"}},
{b64url, "b64url", {tag, "1.0.3"}},
{ets_lru, "ets-lru", {tag, "1.1.0"}},
{khash, "khash", {tag, "1.1.0"}},
diff --git a/src/config/LICENSE b/src/config/LICENSE
new file mode 100644
index 000000000..f6cd2bc80
--- /dev/null
+++ b/src/config/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/src/config/rebar.config b/src/config/rebar.config
new file mode 100644
index 000000000..791b035ea
--- /dev/null
+++ b/src/config/rebar.config
@@ -0,0 +1,4 @@
+% vim: set ft=erlang : -*- erlang -*- % Magic lines for code editors
+
+{cover_enabled, true}.
+{cover_print_enabled, true}.
diff --git a/src/config/src/config.app.src.script b/src/config/src/config.app.src.script
new file mode 100644
index 000000000..e4faf276f
--- /dev/null
+++ b/src/config/src/config.app.src.script
@@ -0,0 +1,32 @@
+% 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.
+
+ConfigPath = filename:join([os:getenv("COUCHDB_APPS_CONFIG_DIR"), "config.config"]),
+AppEnv = case filelib:is_file(ConfigPath) of
+ true ->
+ {ok, Result} = file:consult(ConfigPath),
+ Result;
+ false ->
+ []
+end.
+
+{application, config, [
+ {description, "INI file configuration system for Apache CouchDB"},
+ {vsn, git},
+ {registered, [
+ config,
+ config_event
+ ]},
+ {applications, [kernel, stdlib]},
+ {mod, {config_app, []}},
+ {env, AppEnv}
+]}.
diff --git a/src/config/src/config_app.erl b/src/config/src/config_app.erl
new file mode 100644
index 000000000..8ba58e6d2
--- /dev/null
+++ b/src/config/src/config_app.erl
@@ -0,0 +1,66 @@
+% 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_app).
+
+-behaviour(application).
+
+%% Application callbacks
+-export([start/2, stop/1]).
+
+%% ===================================================================
+%% Application callbacks
+%% ===================================================================
+
+start(_StartType, _StartArgs) ->
+ config_sup:start_link(get_ini_files()).
+
+stop(_State) ->
+ ok.
+
+get_ini_files() ->
+ IniFiles = hd([L || L <- [command_line(), env(), default()], L =/= skip]),
+ lists:flatmap(fun expand_dirs/1, IniFiles).
+
+env() ->
+ case application:get_env(config, ini_files) of
+ undefined ->
+ skip;
+ {ok, IniFiles} ->
+ IniFiles
+ end.
+
+command_line() ->
+ case init:get_argument(couch_ini) of
+ error ->
+ skip;
+ {ok, [IniFiles]} ->
+ IniFiles
+ end.
+
+default() ->
+ Etc = filename:join(code:root_dir(), "etc"),
+ Default = [
+ filename:join(Etc, "default.ini"),
+ filename:join(Etc, "default.d"),
+ filename:join(Etc, "local.ini"),
+ filename:join(Etc, "local.d")
+ ],
+ lists:filter(fun filelib:is_file/1, Default).
+
+expand_dirs(File) ->
+ case filelib:is_dir(File) of
+ true ->
+ lists:sort(filelib:wildcard(File ++ "/*.ini"));
+ false ->
+ [File]
+ end.
diff --git a/src/config/src/config_listener.erl b/src/config/src/config_listener.erl
new file mode 100644
index 000000000..756609b81
--- /dev/null
+++ b/src/config/src/config_listener.erl
@@ -0,0 +1,71 @@
+% 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_listener).
+
+-behaviour(gen_event).
+-vsn(2).
+
+%% Public interface
+-export([start/2]).
+-export([start/3]).
+
+%% Required gen_event interface
+-export([init/1, handle_event/2, handle_call/2, handle_info/2, terminate/2,
+ code_change/3]).
+
+
+-callback handle_config_change(
+ Sec :: string(),
+ Key :: string(),
+ Value :: string(),
+ Persist :: boolean(),
+ State :: term()
+) ->
+ {ok, term()} | remove_handler.
+
+-callback handle_config_terminate(
+ Subscriber :: pid(),
+ Reason :: term(),
+ State :: term()
+) ->
+ term().
+
+
+start(Module, State) ->
+ start(Module, Module, State).
+
+start(Module, Id, State) ->
+ gen_event:add_sup_handler(config_event, {?MODULE, Id}, {Module, State}).
+
+init({Module, State}) ->
+ {ok, {Module, State}}.
+
+handle_event({config_change, Sec, Key, Value, Persist}, {Module, {From, State}}) ->
+ case Module:handle_config_change(Sec, Key, Value, Persist, State) of
+ {ok, NewState} ->
+ {ok, {Module, {From, NewState}}};
+ remove_handler ->
+ remove_handler
+ end.
+
+handle_call(_Request, St) ->
+ {ok, ignored, St}.
+
+handle_info(_Info, St) ->
+ {ok, St}.
+
+terminate(Reason, {Module, {Subscriber, State}}) ->
+ Module:handle_config_terminate(Subscriber, Reason, State).
+
+code_change(_OldVsn, St, _Extra) ->
+ {ok, St}.
diff --git a/src/config/src/config_listener_mon.erl b/src/config/src/config_listener_mon.erl
new file mode 100644
index 000000000..1037a8011
--- /dev/null
+++ b/src/config/src/config_listener_mon.erl
@@ -0,0 +1,90 @@
+% 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_listener_mon).
+-behaviour(gen_server).
+-vsn(1).
+
+
+-export([
+ subscribe/2,
+ start_link/2
+]).
+
+
+-export([
+ init/1,
+ terminate/2,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ code_change/3
+]).
+
+
+-record(st, {
+ pid,
+ ref
+}).
+
+
+start_link(Module, InitSt) ->
+ proc_lib:start_link(?MODULE, init, [{self(), Module, InitSt}]).
+
+subscribe(Module, InitSt) ->
+ case proc_lib:start(?MODULE, init, [{self(), Module, InitSt}]) of
+ {ok, _} -> ok;
+ Else -> Else
+ end.
+
+init({Pid, Mod, InitSt}) ->
+ Ref = erlang:monitor(process, Pid),
+ case config_listener:start(Mod, {Mod, Pid}, {Pid, InitSt}) of
+ ok ->
+ proc_lib:init_ack({ok, self()}),
+ gen_server:enter_loop(?MODULE, [], #st{pid = Pid, ref = Ref});
+ Else ->
+ proc_lib:init_ack(Else)
+ end.
+
+
+terminate(_Reason, _St) ->
+ ok.
+
+
+handle_call(_Message, _From, St) ->
+ {reply, ignored, St}.
+
+
+handle_cast(_Message, St) ->
+ {noreply, St}.
+
+
+handle_info({'DOWN', Ref, _, _, _}, #st{ref = Ref} = St) ->
+ {stop, normal, St};
+
+handle_info({gen_event_EXIT, {config_listener, Module}, Reason}, St) ->
+ Level = case Reason of
+ normal -> debug;
+ shutdown -> debug;
+ _ -> error
+ end,
+ Fmt = "config_listener(~p) for ~p stopped with reason: ~r~n",
+ couch_log:Level(Fmt, [Module, St#st.pid, Reason]),
+ {stop, shutdown, St};
+
+handle_info(_, St) ->
+ {noreply, St}.
+
+
+code_change(_OldVsn, St, _Extra) ->
+ {ok, St}.
diff --git a/src/config/src/config_notifier.erl b/src/config/src/config_notifier.erl
new file mode 100644
index 000000000..6add8d609
--- /dev/null
+++ b/src/config/src/config_notifier.erl
@@ -0,0 +1,78 @@
+% 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_notifier).
+
+-behaviour(gen_event).
+-vsn(1).
+
+%% Public interface
+-export([subscribe/1]).
+-export([subscribe/2]).
+
+%% gen_event interface
+-export([
+ init/1,
+ handle_event/2,
+ handle_call/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3
+]).
+
+subscribe(Subscription) ->
+ subscribe(self(), Subscription).
+
+subscribe(Subscriber, Subscription) ->
+ case lists:member(Subscriber, handlers()) of
+ true ->
+ ok;
+ false ->
+ gen_event:add_sup_handler(
+ config_event, {?MODULE, Subscriber}, {Subscriber, Subscription})
+ end.
+
+init({Subscriber, Subscription}) ->
+ {ok, {Subscriber, Subscription}}.
+
+handle_event({config_change, _, _, _, _} = Event, {Subscriber, Subscription}) ->
+ maybe_notify(Event, Subscriber, Subscription),
+ {ok, {Subscriber, Subscription}}.
+
+handle_call(_Request, St) ->
+ {ok, ignored, St}.
+
+handle_info(_Info, St) ->
+ {ok, St}.
+
+terminate(_Reason, {_Subscriber, _Subscription}) ->
+ ok.
+
+code_change(_OldVsn, St, _Extra) ->
+ {ok, St}.
+
+maybe_notify(Event, Subscriber, all) ->
+ Subscriber ! Event;
+maybe_notify({config_change, Sec, Key, _, _} = Event, Subscriber, Subscription) ->
+ case should_notify(Sec, Key, Subscription) of
+ true ->
+ Subscriber ! Event;
+ false ->
+ ok
+ end.
+
+should_notify(Sec, Key, Subscription) ->
+ lists:any(fun(S) -> S =:= Sec orelse S =:= {Sec, Key} end, Subscription).
+
+handlers() ->
+ AllHandlers = gen_event:which_handlers(config_event),
+ [Id || {?MODULE, Id} <- AllHandlers].
diff --git a/src/config/src/config_sup.erl b/src/config/src/config_sup.erl
new file mode 100644
index 000000000..a595b3ce1
--- /dev/null
+++ b/src/config/src/config_sup.erl
@@ -0,0 +1,50 @@
+% 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_sup).
+-behaviour(supervisor).
+
+%% API
+-export([start_link/1]).
+
+%% Supervisor callbacks
+-export([init/1]).
+
+%% ===================================================================
+%% API functions
+%% ===================================================================
+
+start_link(IniFiles) ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, IniFiles).
+
+%% ===================================================================
+%% Supervisor callbacks
+%% ===================================================================
+
+init(IniFiles) ->
+ Children = [
+ {config,
+ {config, start_link, [IniFiles]},
+ permanent,
+ 5000,
+ worker,
+ [config]
+ },
+ {config_event,
+ {gen_event, start_link, [{local, config_event}]},
+ permanent,
+ 5000,
+ worker,
+ dynamic
+ }
+ ],
+ {ok, {{one_for_one, 5, 10}, Children}}.
diff --git a/src/config/src/config_util.erl b/src/config/src/config_util.erl
new file mode 100644
index 000000000..bce22ab4f
--- /dev/null
+++ b/src/config/src/config_util.erl
@@ -0,0 +1,75 @@
+% 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_util).
+
+-export([abs_pathname/1]).
+-export([abs_pathname/2]).
+-export([implode/2]).
+
+% given a pathname "../foo/bar/" it gives back the fully qualified
+% absolute pathname.
+abs_pathname(" " ++ Filename) ->
+ % strip leading whitspace
+ abs_pathname(Filename);
+abs_pathname([$/ |_]=Filename) ->
+ Filename;
+abs_pathname(Filename) ->
+ {ok, Cwd} = file:get_cwd(),
+ {Filename2, Args} = separate_cmd_args(Filename, ""),
+ abs_pathname(Filename2, Cwd) ++ Args.
+
+abs_pathname(Filename, Dir) ->
+ Name = filename:absname(Filename, Dir ++ "/"),
+ OutFilename = filename:join(fix_path_list(filename:split(Name), [])),
+ % If the filename is a dir (last char slash, put back end slash
+ case string:right(Filename,1) of
+ "/" ->
+ OutFilename ++ "/";
+ "\\" ->
+ OutFilename ++ "/";
+ _Else->
+ OutFilename
+ end.
+
+implode(List, Sep) ->
+ implode(List, Sep, []).
+
+implode([], _Sep, Acc) ->
+ lists:flatten(lists:reverse(Acc));
+implode([H], Sep, Acc) ->
+ implode([], Sep, [H|Acc]);
+implode([H|T], Sep, Acc) ->
+ implode(T, Sep, [Sep,H|Acc]).
+
+% if this as an executable with arguments, seperate out the arguments
+% ""./foo\ bar.sh -baz=blah" -> {"./foo\ bar.sh", " -baz=blah"}
+separate_cmd_args("", CmdAcc) ->
+ {lists:reverse(CmdAcc), ""};
+separate_cmd_args("\\ " ++ Rest, CmdAcc) -> % handle skipped value
+ separate_cmd_args(Rest, " \\" ++ CmdAcc);
+separate_cmd_args(" " ++ Rest, CmdAcc) ->
+ {lists:reverse(CmdAcc), " " ++ Rest};
+separate_cmd_args([Char|Rest], CmdAcc) ->
+ separate_cmd_args(Rest, [Char | CmdAcc]).
+
+% takes a heirarchical list of dirs and removes the dots ".", double dots
+% ".." and the corresponding parent dirs.
+fix_path_list([], Acc) ->
+ lists:reverse(Acc);
+fix_path_list([".."|Rest], [_PrevAcc|RestAcc]) ->
+ fix_path_list(Rest, RestAcc);
+fix_path_list(["."|Rest], Acc) ->
+ fix_path_list(Rest, Acc);
+fix_path_list([Dir | Rest], Acc) ->
+ fix_path_list(Rest, [Dir | Acc]).
+
diff --git a/src/config/src/config_writer.erl b/src/config/src/config_writer.erl
new file mode 100644
index 000000000..fb6ef148e
--- /dev/null
+++ b/src/config/src/config_writer.erl
@@ -0,0 +1,79 @@
+% 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.
+
+%% @doc Saves a Key/Value pair to a ini file. The Key consists of a Section
+%% and Option combination. If that combination is found in the ini file
+%% the new value replaces the old value. If only the Section is found the
+%% Option and value combination is appended to the Section. If the Section
+%% does not yet exist in the ini file, it is added and the Option/Value
+%% pair is appended.
+%% @see config
+
+-module(config_writer).
+
+-export([save_to_file/2]).
+
+%% @spec save_to_file(
+%% Config::{{Section::string(), Option::string()}, Value::string()},
+%% File::filename()) -> ok
+%% @doc Saves a Section/Key/Value triple to the ini file File::filename()
+save_to_file({{Section, Key}, Value}, File) ->
+ {ok, OldFileContents} = file:read_file(File),
+ Lines = re:split(OldFileContents, "\r\n|\n|\r|\032", [{return, list}]),
+
+ SectionLine = "[" ++ Section ++ "]",
+ {ok, Pattern} = re:compile(["^(\\Q", Key, "\\E\\s*=)|\\[[a-zA-Z0-9\.\_-]*\\]"]),
+
+ NewLines = process_file_lines(Lines, [], SectionLine, Pattern, Key, Value),
+ NewFileContents = reverse_and_add_newline(strip_empty_lines(NewLines), []),
+ file:write_file(File, NewFileContents).
+
+
+process_file_lines([Section|Rest], SeenLines, Section, Pattern, Key, Value) ->
+ process_section_lines(Rest, [Section|SeenLines], Pattern, Key, Value);
+
+process_file_lines([Line|Rest], SeenLines, Section, Pattern, Key, Value) ->
+ process_file_lines(Rest, [Line|SeenLines], Section, Pattern, Key, Value);
+
+process_file_lines([], SeenLines, Section, _Pattern, Key, Value) ->
+ % Section wasn't found. Append it with the option here.
+ [Key ++ " = " ++ Value, Section, "" | strip_empty_lines(SeenLines)].
+
+
+process_section_lines([Line|Rest], SeenLines, Pattern, Key, Value) ->
+ case re:run(Line, Pattern, [{capture, all_but_first}]) of
+ nomatch -> % Found nothing interesting. Move on.
+ process_section_lines(Rest, [Line|SeenLines], Pattern, Key, Value);
+ {match, []} -> % Found another section. Append the option here.
+ lists:reverse(Rest) ++
+ [Line, "", Key ++ " = " ++ Value | strip_empty_lines(SeenLines)];
+ {match, _} -> % Found the option itself. Replace it.
+ lists:reverse(Rest) ++ [Key ++ " = " ++ Value | SeenLines]
+ end;
+
+process_section_lines([], SeenLines, _Pattern, Key, Value) ->
+ % Found end of file within the section. Append the option here.
+ [Key ++ " = " ++ Value | strip_empty_lines(SeenLines)].
+
+
+reverse_and_add_newline([Line|Rest], Content) ->
+ reverse_and_add_newline(Rest, [Line, "\n", Content]);
+
+reverse_and_add_newline([], Content) ->
+ Content.
+
+
+strip_empty_lines(["" | Rest]) ->
+ strip_empty_lines(Rest);
+
+strip_empty_lines(All) ->
+ All.
diff --git a/src/config/test/config_tests.erl b/src/config/test/config_tests.erl
new file mode 100644
index 000000000..dd38fc667
--- /dev/null
+++ b/src/config/test/config_tests.erl
@@ -0,0 +1,855 @@
+% 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).
+
+-export([
+ handle_config_change/5,
+ handle_config_terminate/3
+]).
+
+-include_lib("couch/include/couch_eunit.hrl").
+-include_lib("couch/include/couch_db.hrl").
+
+-define(TIMEOUT, 4000).
+-define(RESTART_TIMEOUT_IN_MILLISEC, 3000).
+
+-define(CONFIG_FIXTURESDIR,
+ filename:join([?BUILDDIR(), "src", "config", "test", "fixtures"])).
+
+-define(CONFIG_DEFAULT_TEST,
+ filename:join([?CONFIG_FIXTURESDIR, "config_default_test.ini"])).
+
+-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,
+ [
+ {"enable", fun should_enable_features/0},
+ {"disable", fun should_disable_features/0},
+ {"restart config", fun should_keep_features_on_config_restart/0}
+ ]
+ }
+ }.
+
+
+
+config_override_test_() ->
+ {
+ "Configs overide tests",
+ {
+ foreachx,
+ fun setup/1,
+ fun teardown/2,
+ [
+ {{temporary, [?CONFIG_DEFAULT_TEST]},
+ fun should_ensure_in_defaults/2},
+ {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_FIXTURE_1]},
+ fun should_override_options/2},
+ {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_FIXTURE_2]},
+ fun should_create_new_sections_on_override/2},
+ {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_FIXTURE_1,
+ ?CONFIG_FIXTURE_2]},
+ fun should_win_last_in_chain/2},
+ {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_DEFAULT_D]},
+ fun should_read_default_d/2},
+ {{temporary, [?CONFIG_DEFAULT_TEST, ?CONFIG_LOCAL_D]},
+ fun should_read_local_d/2},
+ {{temporary, [?CONFIG_DEFAULT_TEST, ?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_TEST]},
+ fun should_write_changes/2},
+ {{temporary, [?CONFIG_DEFAULT_TEST]},
+ 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},
+ {all, fun should_notify_on_config_reload/2},
+ {all, fun should_notify_on_config_reload_flush/2}
+ ]
+ }
+ }.
+
+
+config_key_has_regex_test_() ->
+ {
+ "Test key with regex can be compiled and written to file",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [
+ fun should_handle_regex_patterns_in_key/0
+ ]
+ }
+ }.
+
+
+config_access_right_test_() ->
+ {
+ "Test config file access right",
+ {
+ foreach,
+ fun setup/0,
+ fun teardown/1,
+ [
+ fun should_write_config_to_file/0,
+ fun should_delete_config_from_file/0,
+ fun should_not_write_config_to_file/0,
+ fun should_not_delete_config_from_file/0
+ ]
+ }
+ }.
+
+
+should_write_config_to_file() ->
+ ?assertEqual(ok, config:set("admins", "foo", "500", true)).
+
+
+should_handle_regex_patterns_in_key() ->
+ ?assertEqual(ok, config:set("sect1", "pat||*", "true", true)),
+ ?assertEqual([{"pat||*", "true"}], config:get("sect1")).
+
+
+should_delete_config_from_file() ->
+ ?assertEqual(ok, config:delete("admins", "foo", true)).
+
+
+should_not_write_config_to_file() ->
+ meck:new(config_writer),
+ meck:expect(config_writer, save_to_file, fun(_, _) -> {error, eacces} end),
+ ?assertEqual({error, eacces}, config:set("admins", "foo", "500", true)),
+ meck:unload(config_writer).
+
+
+should_not_delete_config_from_file() ->
+ meck:new(config_writer),
+ meck:expect(config_writer, save_to_file, fun(_, _) -> {error, eacces} end),
+ ?assertEqual({error, eacces}, config:delete("admins", "foo", true)),
+ meck:unload(config_writer).
+
+
+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() ->
+ [config:disable_feature(F) || F <- config: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() ->
+ [config:disable_feature(F) || F <- config: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()).
+
+should_keep_features_on_config_restart() ->
+ [config:disable_feature(F) || F <- config:features()],
+ ?assertEqual([], config:features()),
+
+ config:enable_feature(snek),
+ ?assertEqual([snek], config:features()),
+ with_process_restart(config),
+ ?assertEqual([snek], config:features()).
+
+should_notify_on_config_reload(Subscription, {_Apps, Pid}) ->
+ {to_string(Subscription), ?_test(begin
+ ?assertEqual(ok, config:set("section_foo", "key_bar", "any", true)),
+ ?assertEqual({config_change,"section_foo", "key_bar", "any", true}, getmsg(Pid)),
+ ?assertEqual(ok, config:set("section_foo", "key_bar", "not_any", false)),
+ ?assertEqual({config_change,"section_foo", "key_bar", "not_any", false}, getmsg(Pid)),
+ ?assertEqual(ok, config:reload()),
+ ?assertEqual({config_change,"section_foo", "key_bar", "any", true}, getmsg(Pid)),
+ ok
+ end)}.
+
+should_notify_on_config_reload_flush(Subscription, {_Apps, Pid}) ->
+ {to_string(Subscription), ?_test(begin
+ ?assertEqual(ok, config:set("section_foo_temp", "key_bar", "any", false)),
+ ?assertEqual({config_change,"section_foo_temp", "key_bar", "any", false}, getmsg(Pid)),
+ ?assertEqual(ok, config:reload()),
+ ?assertEqual({config_change,"section_foo_temp", "key_bar", deleted, true}, getmsg(Pid)),
+ ok
+ end)}.
+
+
+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])).
+
+with_process_restart(Name) ->
+ ok = stop_sync(whereis(Name), ?TIMEOUT),
+ Now = now_us(),
+ wait_process_restart(
+ Name, ?RESTART_TIMEOUT_IN_MILLISEC * 1000, 50, Now, Now).
+
+wait_process_restart(_Name, Timeout, _Delay, Started, Prev)
+ when Prev - Started > Timeout ->
+ timeout;
+wait_process_restart(Name, Timeout, Delay, Started, _Prev) ->
+ case whereis(Name) of
+ undefined ->
+ ok = timer:sleep(Delay),
+ wait_process_restart(Name, Timeout, Delay, Started, now_us());
+ Pid ->
+ Pid
+ end.
+
+stop_sync(Pid, Timeout) when is_pid(Pid) ->
+ MRef = erlang:monitor(process, Pid),
+ try
+ begin
+ catch unlink(Pid),
+ exit(Pid, kill),
+ receive
+ {'DOWN', MRef, _, _, _} ->
+ ok
+ after Timeout ->
+ timeout
+ end
+ end
+ after
+ erlang:demonitor(MRef, [flush])
+ end;
+stop_sync(_, _) -> error(badarg).
+
+
+now_us() ->
+ {MegaSecs, Secs, MicroSecs} = os:timestamp(),
+ (MegaSecs * 1000000 + Secs) * 1000000 + MicroSecs.
diff --git a/src/config/test/fixtures/config_default_test.ini b/src/config/test/fixtures/config_default_test.ini
new file mode 100644
index 000000000..bac4405e5
--- /dev/null
+++ b/src/config/test/fixtures/config_default_test.ini
@@ -0,0 +1,23 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements. See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership. The ASF licenses this file
+; to you 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.
+
+[couchdb]
+max_dbs_open = 500
+
+[httpd]
+port = 5986
+bind_address = 127.0.0.1
diff --git a/src/config/test/fixtures/config_tests_1.ini b/src/config/test/fixtures/config_tests_1.ini
new file mode 100644
index 000000000..55451dade
--- /dev/null
+++ b/src/config/test/fixtures/config_tests_1.ini
@@ -0,0 +1,22 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements. See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership. The ASF licenses this file
+; to you 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.
+
+[couchdb]
+max_dbs_open=10
+
+[httpd]
+port=4895
diff --git a/src/config/test/fixtures/config_tests_2.ini b/src/config/test/fixtures/config_tests_2.ini
new file mode 100644
index 000000000..5f46357f5
--- /dev/null
+++ b/src/config/test/fixtures/config_tests_2.ini
@@ -0,0 +1,22 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements. See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership. The ASF licenses this file
+; to you 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.
+
+[httpd]
+port = 80
+
+[fizbang]
+unicode = normalized
diff --git a/src/config/test/fixtures/default.d/extra.ini b/src/config/test/fixtures/default.d/extra.ini
new file mode 100644
index 000000000..fda68b32c
--- /dev/null
+++ b/src/config/test/fixtures/default.d/extra.ini
@@ -0,0 +1,19 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements. See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership. The ASF licenses this file
+; to you 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.
+
+[couchdb]
+max_dbs_open=11
diff --git a/src/config/test/fixtures/local.d/extra.ini b/src/config/test/fixtures/local.d/extra.ini
new file mode 100644
index 000000000..d6a6d4661
--- /dev/null
+++ b/src/config/test/fixtures/local.d/extra.ini
@@ -0,0 +1,19 @@
+; Licensed to the Apache Software Foundation (ASF) under one
+; or more contributor license agreements. See the NOTICE file
+; distributed with this work for additional information
+; regarding copyright ownership. The ASF licenses this file
+; to you 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.
+
+[couchdb]
+max_dbs_open=12