You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by da...@apache.org on 2016/07/22 09:49:43 UTC

[4/5] couch-log commit: updated refs/heads/master to da0e489

Add test suite for couch_log

COUCHDB-3067


Project: http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/commit/16a46572
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/tree/16a46572
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/diff/16a46572

Branch: refs/heads/master
Commit: 16a46572b92bf0b53b3aa94bc209cae0ba2257c1
Parents: c572aae
Author: Paul J. Davis <pa...@gmail.com>
Authored: Wed Jul 20 12:53:50 2016 -0500
Committer: Paul J. Davis <pa...@gmail.com>
Committed: Fri Jul 22 04:39:08 2016 -0500

----------------------------------------------------------------------
 include/couch_log.hrl                   |   3 +
 rebar.config                            |   2 +
 test/couch_log_config_listener_test.erl |  56 ++
 test/couch_log_config_test.erl          | 110 ++++
 test/couch_log_error_logger_h_test.erl  |  45 ++
 test/couch_log_formatter_test.erl       | 768 +++++++++++++++++++++++++++
 test/couch_log_monitor_test.erl         |  67 +++
 test/couch_log_server_test.erl          | 118 ++++
 test/couch_log_test.erl                 |  85 +++
 test/couch_log_test_util.erl            | 153 ++++++
 test/couch_log_util_test.erl            |  55 ++
 test/couch_log_writer_ets.erl           |  49 ++
 test/couch_log_writer_file_test.erl     | 161 ++++++
 test/couch_log_writer_stderr_test.erl   |  58 ++
 test/couch_log_writer_syslog_test.erl   | 122 +++++
 test/couch_log_writer_test.erl          |  54 ++
 16 files changed, 1906 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/include/couch_log.hrl
----------------------------------------------------------------------
diff --git a/include/couch_log.hrl b/include/couch_log.hrl
index a472c0c..fa544a8 100644
--- a/include/couch_log.hrl
+++ b/include/couch_log.hrl
@@ -17,3 +17,6 @@
     msg_id,
     time_stamp
 }).
+
+
+-define(COUCH_LOG_TEST_TABLE, couch_log_test_table).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/rebar.config
----------------------------------------------------------------------
diff --git a/rebar.config b/rebar.config
new file mode 100644
index 0000000..e0d1844
--- /dev/null
+++ b/rebar.config
@@ -0,0 +1,2 @@
+{cover_enabled, true}.
+{cover_print_enabled, true}.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_config_listener_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_config_listener_test.erl b/test/couch_log_config_listener_test.erl
new file mode 100644
index 0000000..9a8e16d
--- /dev/null
+++ b/test/couch_log_config_listener_test.erl
@@ -0,0 +1,56 @@
+% 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(couch_log_config_listener_test).
+
+
+-include_lib("couch_log/include/couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(HANDLER, {config_listener, couch_log_config_listener}).
+
+
+couch_log_config_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun check_restart_listener/0,
+            fun check_ignore_non_log/0
+        ]
+    }.
+
+
+check_restart_listener() ->
+    Handlers1 = gen_event:which_handlers(config_event),
+    ?assert(lists:member(?HANDLER, Handlers1)),
+
+    gen_event:delete_handler(config_event, ?HANDLER, testing),
+
+    Handlers2 = gen_event:which_handlers(config_event),
+    ?assert(not lists:member(?HANDLER, Handlers2)),
+
+    timer:sleep(1000),
+
+    Handlers3 = gen_event:which_handlers(config_event),
+    ?assert(lists:member(?HANDLER, Handlers3)).
+
+
+check_ignore_non_log() ->
+    Run = fun() ->
+        couch_log_test_util:with_config_listener(fun() ->
+            config:set("foo", "bar", "baz"),
+            couch_log_test_util:wait_for_config()
+        end)
+    end,
+    ?assertError(config_change_timeout, Run()).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_config_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_config_test.erl b/test/couch_log_config_test.erl
new file mode 100644
index 0000000..c4677f3
--- /dev/null
+++ b/test/couch_log_config_test.erl
@@ -0,0 +1,110 @@
+% 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(couch_log_config_test).
+
+
+-include_lib("couch_log/include/couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+couch_log_config_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun check_level/0,
+            fun check_max_message_size/0,
+            fun check_bad_level/0,
+            fun check_bad_max_message_size/0
+        ]
+    }.
+
+
+check_level() ->
+    % Default level is info
+    ?assertEqual(info, couch_log_config:get(level)),
+    ?assertEqual(2, couch_log_config:get(level_int)),
+
+    couch_log_test_util:with_config_listener(fun() ->
+        config:set("log", "level", "emerg"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(emergency, couch_log_config:get(level)),
+        ?assertEqual(8, couch_log_config:get(level_int)),
+
+        config:set("log", "level", "debug"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(debug, couch_log_config:get(level)),
+        ?assertEqual(1, couch_log_config:get(level_int)),
+
+        config:delete("log", "level"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(info, couch_log_config:get(level)),
+        ?assertEqual(2, couch_log_config:get(level_int))
+    end).
+
+
+check_max_message_size() ->
+    % Default is 16000
+    ?assertEqual(16000, couch_log_config:get(max_message_size)),
+
+    couch_log_test_util:with_config_listener(fun() ->
+        config:set("log", "max_message_size", "1024"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(1024, couch_log_config:get(max_message_size)),
+
+        config:delete("log", "max_message_size"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(16000, couch_log_config:get(max_message_size))
+    end).
+
+
+check_bad_level() ->
+    % Default level is info
+    ?assertEqual(info, couch_log_config:get(level)),
+    ?assertEqual(2, couch_log_config:get(level_int)),
+
+    couch_log_test_util:with_config_listener(fun() ->
+        config:set("log", "level", "debug"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(debug, couch_log_config:get(level)),
+        ?assertEqual(1, couch_log_config:get(level_int)),
+
+        config:set("log", "level", "this is not a valid level name"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(info, couch_log_config:get(level)),
+        ?assertEqual(2, couch_log_config:get(level_int)),
+
+        config:delete("log", "level"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(info, couch_log_config:get(level)),
+        ?assertEqual(2, couch_log_config:get(level_int))
+    end).
+
+
+check_bad_max_message_size() ->
+    % Default level is 16000
+    ?assertEqual(16000, couch_log_config:get(max_message_size)),
+
+    couch_log_test_util:with_config_listener(fun() ->
+        config:set("log", "max_message_size", "1024"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(1024, couch_log_config:get(max_message_size)),
+
+        config:set("log", "max_message_size", "this is not a valid size"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(16000, couch_log_config:get(max_message_size)),
+
+        config:delete("log", "max_message_size"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(16000, couch_log_config:get(max_message_size))
+    end).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_error_logger_h_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_error_logger_h_test.erl b/test/couch_log_error_logger_h_test.erl
new file mode 100644
index 0000000..b78598f
--- /dev/null
+++ b/test/couch_log_error_logger_h_test.erl
@@ -0,0 +1,45 @@
+% 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(couch_log_error_logger_h_test).
+
+
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(HANDLER, couch_log_error_logger_h).
+
+
+couch_log_error_logger_h_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun handler_ignores_unknown_messages/0,
+            fun coverage_test/0
+        ]
+    }.
+
+
+handler_ignores_unknown_messages() ->
+    Handlers1 = gen_event:which_handlers(error_logger),
+    ?assert(lists:member(?HANDLER, Handlers1)),
+    ?assertEqual(ignored, gen_event:call(error_logger, ?HANDLER, foo)),
+
+    error_logger ! this_is_a_message,
+    Handlers2 = gen_event:which_handlers(error_logger),
+    ?assert(lists:member(?HANDLER, Handlers2)).
+
+
+coverage_test() ->
+    Resp = couch_log_error_logger_h:code_change(foo, bazinga, baz),
+    ?assertEqual({ok, bazinga}, Resp).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_formatter_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_formatter_test.erl b/test/couch_log_formatter_test.erl
new file mode 100644
index 0000000..1e8457b
--- /dev/null
+++ b/test/couch_log_formatter_test.erl
@@ -0,0 +1,768 @@
+% 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(couch_log_formatter_test).
+
+
+-include("couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+truncate_fmt_test() ->
+    Msg = [0 || _ <- lists:seq(1, 1048576)],
+    Entry = couch_log_formatter:format(info, self(), "~w", [Msg]),
+    ?assert(length(Entry#log_entry.msg) =< 16000).
+
+
+truncate_test() ->
+    Msg = [0 || _ <- lists:seq(1, 1048576)],
+    Entry = couch_log_formatter:format(info, self(), Msg),
+    ?assert(length(Entry#log_entry.msg) =< 16000).
+
+
+gen_server_error_test() ->
+    Pid = self(),
+    Event = {
+        error,
+        erlang:group_leader(),
+        {
+            Pid,
+            "** Generic server and some stuff",
+            [a_gen_server, {foo, bar}, server_state, some_reason]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid
+        },
+        do_format(Event)
+    ),
+    do_matches(do_format(Event), [
+        "gen_server a_gen_server terminated",
+        "with reason: some_reason",
+        "last msg: {foo,bar}",
+        "state: server_state"
+    ]).
+
+
+gen_fsm_error_test() ->
+    Pid = self(),
+    Event = {
+        error,
+        erlang:group_leader(),
+        {
+            Pid,
+            "** State machine did a thing",
+            [a_gen_fsm, {ohai,there}, state_name, curr_state, barf]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid
+        },
+        do_format(Event)
+    ),
+    do_matches(do_format(Event), [
+        "gen_fsm a_gen_fsm in state state_name",
+        "with reason: barf",
+        "last msg: {ohai,there}",
+        "state: curr_state"
+    ]).
+
+
+gen_event_error_test() ->
+    Pid = self(),
+    Event = {
+        error,
+        erlang:group_leader(),
+        {
+            Pid,
+            "** gen_event handler did a thing",
+            [
+                handler_id,
+                a_gen_event,
+                {ohai,there},
+                curr_state,
+                barf
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid
+        },
+        do_format(Event)
+    ),
+    do_matches(do_format(Event), [
+        "gen_event handler_id installed in a_gen_event",
+        "reason: barf",
+        "last msg: {ohai,there}",
+        "state: curr_state"
+    ]).
+
+
+normal_error_test() ->
+    Pid = self(),
+    Event = {
+        error,
+        erlang:group_leader(),
+        {
+            Pid,
+            "format thing: ~w ~w",
+            [
+                first_arg,
+                second_arg
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid,
+            msg = "format thing: first_arg second_arg"
+        },
+        do_format(Event)
+    ).
+
+
+error_report_std_error_test() ->
+    Pid = self(),
+    Event = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            std_error,
+            [foo, {bar, baz}]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid,
+            msg = "foo, bar: baz"
+        },
+        do_format(Event)
+    ).
+
+
+supervisor_report_test() ->
+    Pid = self(),
+    % A standard supervisor report
+    Event1 = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            supervisor_report,
+            [
+                {supervisor, sup_name},
+                {offender, [
+                    {id, sup_child},
+                    {pid, list_to_pid("<0.1.0>")},
+                    {mfargs, {some_mod, some_fun, 3}}
+                ]},
+                {reason, a_reason},
+                {errorContext, some_context}
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid
+        },
+        do_format(Event1)
+    ),
+    do_matches(do_format(Event1), [
+        "Supervisor sup_name",
+        "had child sup_child started with some_mod:some_fun/3 at <0.1.0> exit",
+        "with reason a_reason",
+        "in context some_context"
+    ]),
+    % Slightly older using name instead of id
+    % in the offender blob.
+    Event2 = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            supervisor_report,
+            [
+                {supervisor, sup_name},
+                {offender, [
+                    {name, sup_child},
+                    {pid, list_to_pid("<0.1.0>")},
+                    {mfargs, {some_mod, some_fun, 3}}
+                ]},
+                {reason, a_reason},
+                {errorContext, some_context}
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid
+        },
+        do_format(Event2)
+    ),
+    do_matches(do_format(Event2), [
+        "Supervisor sup_name",
+        "had child sup_child started with some_mod:some_fun/3 at <0.1.0> exit",
+        "with reason a_reason",
+        "in context some_context"
+    ]),
+    % A supervisor_bridge
+    Event3 = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            supervisor_report,
+            [
+                {supervisor, sup_name},
+                {offender, [
+                    {mod, bridge_mod},
+                    {pid, list_to_pid("<0.1.0>")}
+                ]},
+                {reason, a_reason},
+                {errorContext, some_context}
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid
+        },
+        do_format(Event3)
+    ),
+    do_matches(do_format(Event3), [
+        "Supervisor sup_name",
+        "had child at module bridge_mod at <0.1.0> exit",
+        "with reason a_reason",
+        "in context some_context"
+    ]),
+    % Any other supervisor report
+    Event4 = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            supervisor_report,
+            [foo, {a, thing}, bang]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid,
+            msg = "SUPERVISOR REPORT foo, a: thing, bang"
+        },
+        do_format(Event4)
+    ).
+
+
+crash_report_test() ->
+    Pid = self(),
+    % A standard crash report
+    Event1 = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            crash_report,
+            [
+                [
+                    {pid, list_to_pid("<0.2.0>")},
+                    {error_info, {
+                        exit,
+                        undef,
+                        [{mod_name, fun_name, [a, b]}]
+                    }}
+                ],
+                [list_to_pid("<0.3.0>"), list_to_pid("<0.4.0>")]
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = Pid
+        },
+        do_format(Event1)
+    ),
+    do_matches(do_format(Event1), [
+        "Process <0.2.0>",
+        "with 2 neighbors",
+        "exited",
+        "reason: call to undefined function mod_name:fun_name\\(a, b\\)"
+    ]),
+    % A registered process crash report
+    Event2 = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            crash_report,
+            [
+                [
+                    {pid, list_to_pid("<0.2.0>")},
+                    {registered_name, couch_log_server},
+                    {error_info, {
+                        exit,
+                        undef,
+                        [{mod_name, fun_name, [a, b]}]
+                    }}
+                ],
+                [list_to_pid("<0.3.0>"), list_to_pid("<0.4.0>")]
+            ]
+        }
+    },
+    do_matches(do_format(Event2), [
+        "Process couch_log_server \\(<0.2.0>\\)"
+    ]),
+    % A non-exit crash report
+    Event3 = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            crash_report,
+            [
+                [
+                    {pid, list_to_pid("<0.2.0>")},
+                    {registered_name, couch_log_server},
+                    {error_info, {
+                        killed,
+                        undef,
+                        [{mod_name, fun_name, [a, b]}]
+                    }}
+                ],
+                [list_to_pid("<0.3.0>"), list_to_pid("<0.4.0>")]
+            ]
+        }
+    },
+    do_matches(do_format(Event3), [
+        "crashed"
+    ]),
+    % A extra report info
+    Event4 = {
+        error_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            crash_report,
+            [
+                [
+                    {pid, list_to_pid("<0.2.0>")},
+                    {error_info, {
+                        killed,
+                        undef,
+                        [{mod_name, fun_name, [a, b]}]
+                    }},
+                    {another, entry},
+                    yep
+                ],
+                [list_to_pid("<0.3.0>"), list_to_pid("<0.4.0>")]
+            ]
+        }
+    },
+    do_matches(do_format(Event4), [
+        "; another: entry, yep"
+    ]).
+
+
+warning_report_test() ->
+    Pid = self(),
+    % A warning message
+    Event1 = {
+        warning_msg,
+        erlang:group_leader(),
+        {
+            Pid,
+            "a ~s string ~w",
+            ["format", 7]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = warning,
+            pid = Pid,
+            msg = "a format string 7"
+        },
+        do_format(Event1)
+    ),
+    % A warning report
+    Event2 = {
+        warning_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            std_warning,
+            [list, 'of', {things, indeed}]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = warning,
+            pid = Pid,
+            msg = "list, of, things: indeed"
+        },
+        do_format(Event2)
+    ).
+
+
+info_report_test() ->
+    Pid = self(),
+    % An info message
+    Event1 = {
+        info_msg,
+        erlang:group_leader(),
+        {
+            Pid,
+            "an info ~s string ~w",
+            ["format", 7]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = info,
+            pid = Pid,
+            msg = "an info format string 7"
+        },
+        do_format(Event1)
+    ),
+    % Application exit info
+    Event2 = {
+        info_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            std_info,
+            [
+                {type, no_idea},
+                {application, couch_log},
+                {exited, red_sox_are_on}
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = info,
+            pid = Pid,
+            msg = "Application couch_log exited with reason: red_sox_are_on"
+        },
+        do_format(Event2)
+    ),
+    % Any other std_info message
+    Event3 = {
+        info_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            std_info,
+            [
+                {type, no_idea},
+                {application, couch_log}
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = info,
+            pid = Pid,
+            msg = "type: no_idea, application: couch_log"
+        },
+        do_format(Event3)
+    ),
+    % Non-list other report
+    Event4 = {
+        info_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            std_info,
+            dang
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = info,
+            pid = Pid,
+            msg = "dang"
+        },
+        do_format(Event4)
+    ).
+
+
+progress_report_test() ->
+    Pid = self(),
+    % Application started
+    Event1 = {
+        info_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            progress,
+            [{started_at, 'nonode@nohost'}, {application, app_name}]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = info,
+            pid = Pid,
+            msg = "Application app_name started on node nonode@nohost"
+        },
+        do_format(Event1)
+    ),
+    % Supervisor started child
+    Event2 = {
+        info_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            progress,
+            [
+                {supervisor, sup_dude},
+                {started, [
+                    {mfargs, {mod_name, fun_name, 1}},
+                    {pid, list_to_pid("<0.5.0>")}
+                ]}
+            ]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = debug,
+            pid = Pid,
+            msg = "Supervisor sup_dude started mod_name:fun_name/1"
+                    " at pid <0.5.0>"
+        },
+        do_format(Event2)
+    ),
+    %  Other progress report
+    Event3 = {
+        info_report,
+        erlang:group_leader(),
+        {
+            Pid,
+            progress,
+            [a, {thing, boop}, here]
+        }
+    },
+    ?assertMatch(
+        #log_entry{
+            level = info,
+            pid = Pid,
+            msg = "PROGRESS REPORT a, thing: boop, here"
+        },
+        do_format(Event3)
+    ).
+
+
+log_unknown_event_test() ->
+    Pid = self(),
+    ?assertMatch(
+        #log_entry{
+            level = warning,
+            pid = Pid,
+            msg = "Unexpected error_logger event an_unknown_event"
+        },
+        do_format(an_unknown_event)
+    ).
+
+
+format_reason_test_() ->
+    Cases = [
+        {
+            {'function not exported', [{a, b, 2}, {c, d, 1}, {e, f, 2}]},
+            "call to unexported function a:b/2 at c:d/1 <= e:f/2"
+        },
+        {
+            {'function not exported', [{a, b, 2, []}, {c, d, 1}, {e, f, 2}]},
+            "call to unexported function a:b/2 at c:d/1 <= e:f/2"
+        },
+        {
+            {undef, [{a, b, 2, []}, {c, d, 1}, {e, f, 2}]},
+            "call to undefined function a:b/2 at c:d/1 <= e:f/2"
+        },
+        {
+            {bad_return, {{a, b, 2}, {'EXIT', killed}}},
+            "bad return value {'EXIT',killed} from a:b/2"
+        },
+        {
+            {bad_return_value, foo},
+            "bad return value foo"
+        },
+        {
+            {{bad_return_value, foo}, {h, i, 0}},
+            "bad return value foo at h:i/0"
+        },
+        {
+            {{badrecord, {foo, 1, 4}}, [{h, i, 0}, {j, k, [a, b]}]},
+            "bad record {foo,1,4} at h:i/0 <= j:k/2"
+        },
+        {
+            {{case_clause, bingo}, [{j, k, 3}, {z, z, 0}]},
+            "no case clause matching bingo at j:k/3 <= z:z/0"
+        },
+        {
+            {function_clause, [{j, k, [a, 2]}, {y, x, 1}]},
+            "no function clause matching j:k(a, 2) at y:x/1"
+        },
+        {
+            {if_clause, [{j, k, [a, 2]}, {y, x, 1}]},
+            "no true branch found while evaluating if expression at j:k/2 <= y:x/1"
+        },
+        {
+            {{try_clause, bango}, [{j, k, [a, 2]}, {y, x, 1}]},
+            "no try clause matching bango at j:k/2 <= y:x/1"
+        },
+        {
+            {badarith, [{j, k, [a, 2]}, {y, x, 1}]},
+            "bad arithmetic expression at j:k/2 <= y:x/1"
+        },
+        {
+            {{badmatch, bongo}, [{j, k, [a, 2]}, {y, x, 1}]},
+            "no match of right hand value bongo at j:k/2 <= y:x/1"
+        },
+        {
+            {emfile, [{j, k, [a, 2]}, {y, x, 1}]},
+            "maximum number of file descriptors exhausted, check ulimit -n; j:k/2 <= y:x/1"
+        },
+        {
+            {system_limit, [{erlang, open_port, []}, {y, x, 1}]},
+            "system limit: maximum number of ports exceeded at y:x/1"
+        },
+        {
+            {system_limit, [{erlang, spawn, []}, {y, x, 1}]},
+            "system limit: maximum number of processes exceeded at y:x/1"
+        },
+        {
+            {system_limit, [{erlang, spawn_opt, []}, {y, x, 1}]},
+            "system limit: maximum number of processes exceeded at y:x/1"
+        },
+        {
+            {system_limit, [{erlang, list_to_atom, ["foo"]}, {y, x, 1}]},
+            "system limit: tried to create an atom larger than 255, or maximum atom count exceeded at y:x/1"
+        },
+        {
+            {system_limit, [{ets, new, []}, {y, x, 1}]},
+            "system limit: maximum number of ETS tables exceeded at y:x/1"
+        },
+        {
+            {system_limit, [{couch_log, totes_logs, []}, {y, x, 1}]},
+            "system limit: couch_log:totes_logs() at y:x/1"
+        },
+        {
+            {badarg, [{j, k, [a, 2]}, {y, x, 1}]},
+            "bad argument in call to j:k(a, 2) at y:x/1"
+        },
+        {
+            {{badarg, [{j, k, [a, 2]}, {y, x, 1}]}, some_ignored_thing},
+            "bad argument in call to j:k(a, 2) at y:x/1"
+        },
+        {
+            {{badarity, {fun erlang:spawn/1, [a, b]}}, [{y, x, 1}]},
+            "function called with wrong arity of 2 instead of 1 at y:x/1"
+        },
+        {
+            {noproc, [{y, x, 1}]},
+            "no such process or port in call to y:x/1"
+        },
+        {
+            {{badfun, 2}, [{y, x, 1}]},
+            "bad function 2 called at y:x/1"
+        },
+        {
+            {a_reason, [{y, x, 1}]},
+            "a_reason at y:x/1"
+        },
+        {
+            {a_reason, [{y, x, 1, [{line, 4}]}]},
+            "a_reason at y:x/1(line:4)"
+        }
+    ],
+    [
+        {Msg, fun() -> ?assertEqual(
+            Msg,
+            lists:flatten(couch_log_formatter:format_reason(Reason))
+        ) end}
+        || {Reason, Msg} <- Cases
+    ].
+
+
+coverage_test() ->
+    % MFA's that aren't
+    ?assertEqual(["foo"], couch_log_formatter:format_mfa(foo)),
+
+    % Traces with line numbers
+    Trace = [{x, y, [a], [{line, 4}]}],
+    ?assertEqual(
+        "x:y/1(line:4)",
+        lists:flatten(couch_log_formatter:format_trace(Trace))
+    ),
+
+    % Excercising print_silly_list
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            msg = "foobar"
+        },
+        do_format({
+            error_report,
+            erlang:group_leader(),
+            {self(), std_error, "foobar"}
+        })
+    ),
+
+    % Excercising print_silly_list
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            msg = "dang"
+        },
+        do_format({
+            error_report,
+            erlang:group_leader(),
+            {self(), std_error, dang}
+        })
+    ).
+
+
+do_format(Event) ->
+    E = couch_log_formatter:format(Event),
+    E#log_entry{
+        msg = lists:flatten(E#log_entry.msg),
+        msg_id = lists:flatten(E#log_entry.msg_id),
+        time_stamp = lists:flatten(E#log_entry.time_stamp)
+    }.
+
+
+do_matches(_, []) ->
+    ok;
+
+do_matches(#log_entry{msg = Msg} = E, [Pattern | RestPatterns]) ->
+    case re:run(Msg, Pattern) of
+        {match, _} ->
+            ok;
+        nomatch ->
+            Err1 = io_lib:format("'~s' does not match '~s'", [Pattern, Msg]),
+            Err2 = lists:flatten(Err1),
+            ?assertEqual(nomatch, Err2)
+    end,
+    do_matches(E, RestPatterns).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_monitor_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_monitor_test.erl b/test/couch_log_monitor_test.erl
new file mode 100644
index 0000000..eec0085
--- /dev/null
+++ b/test/couch_log_monitor_test.erl
@@ -0,0 +1,67 @@
+% 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(couch_log_monitor_test).
+
+
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(HANDLER, couch_log_error_logger_h).
+
+
+couch_log_monitor_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun monitor_ignores_unknown_messages/0,
+            fun monitor_restarts_handler/0,
+            fun coverage_test/0
+        ]
+    }.
+
+
+monitor_ignores_unknown_messages() ->
+    Pid1 = get_monitor_pid(),
+
+    ?assertEqual(ignored, gen_server:call(Pid1, do_foo_please)),
+
+    gen_server:cast(Pid1, do_bar_please),
+    Pid1 ! do_baz_please,
+    timer:sleep(250),
+    ?assert(is_process_alive(Pid1)).
+
+
+monitor_restarts_handler() ->
+    Pid1 = get_monitor_pid(),
+    error_logger:delete_report_handler(?HANDLER),
+    timer:sleep(250),
+
+    ?assert(not is_process_alive(Pid1)),
+
+    Pid2 = get_monitor_pid(),
+    ?assert(is_process_alive(Pid2)),
+
+    Handlers = gen_event:which_handlers(error_logger),
+    ?assert(lists:member(?HANDLER, Handlers)).
+
+
+coverage_test() ->
+    Resp = couch_log_monitor:code_change(foo, bazinga, baz),
+    ?assertEqual({ok, bazinga}, Resp).
+
+
+get_monitor_pid() ->
+    Children = supervisor:which_children(couch_log_sup),
+    [MonPid] = [Pid || {couch_log_monitor, Pid, _, _} <- Children, is_pid(Pid)],
+    MonPid.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_server_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_server_test.erl b/test/couch_log_server_test.erl
new file mode 100644
index 0000000..7af570e
--- /dev/null
+++ b/test/couch_log_server_test.erl
@@ -0,0 +1,118 @@
+% 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(couch_log_server_test).
+
+
+-include("couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+couch_log_server_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun check_can_reconfigure/0,
+            fun check_can_restart/0,
+            fun check_can_cast_log_entry/0,
+            fun check_logs_ignored_messages/0
+        ]
+    }.
+
+
+check_can_reconfigure() ->
+    couch_log:error("a message", []),
+    ?assertEqual(0, couch_log_test_util:last_log_key()),
+    ?assertEqual(ok, couch_log_server:reconfigure()),
+    ?assertEqual('$end_of_table', couch_log_test_util:last_log_key()),
+
+    couch_log_test_util:with_config_listener(fun() ->
+        couch_log:error("another message", []),
+        ?assertEqual(0, couch_log_test_util:last_log_key()),
+        config:set("log", "some_key", "some_val"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual('$end_of_table', couch_log_test_util:last_log_key())
+    end).
+
+
+check_can_restart() ->
+    Pid1 = whereis(couch_log_server),
+    Ref = erlang:monitor(process, Pid1),
+    ?assert(is_process_alive(Pid1)),
+
+    supervisor:terminate_child(couch_log_sup, couch_log_server),
+    supervisor:restart_child(couch_log_sup, couch_log_server),
+
+    receive
+        {'DOWN', Ref, _, _, _} -> ok
+    after 1000 ->
+        erlang:error(timeout_restarting_couch_log_server)
+    end,
+
+    ?assert(not is_process_alive(Pid1)),
+
+    Pid2 = whereis(couch_log_server),
+    ?assertNotEqual(Pid2, Pid1),
+    ?assert(is_process_alive(Pid2)).
+
+
+check_can_cast_log_entry() ->
+    Entry = #log_entry{
+        level = critical,
+        pid = self(),
+        msg = "this will be casted",
+        msg_id = "----",
+        time_stamp = "2016-07-20-almost-my-birthday"
+    },
+    ok = gen_server:cast(couch_log_server, {log, Entry}),
+    timer:sleep(500), % totes gross
+    ?assertEqual(Entry, couch_log_test_util:last_log()).
+
+
+check_logs_ignored_messages() ->
+    gen_server:call(couch_log_server, a_call),
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = couch_log_server,
+            msg = "couch_log_server ignored a_call"
+        },
+        couch_log_test_util:last_log()
+    ),
+
+    gen_server:cast(couch_log_server, a_cast),
+    timer:sleep(500), % yes gross
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = couch_log_server,
+            msg = "couch_log_server ignored a_cast"
+        },
+        couch_log_test_util:last_log()
+    ),
+
+    couch_log_server ! an_info,
+    timer:sleep(500), % still gross
+    ?assertMatch(
+        #log_entry{
+            level = error,
+            pid = couch_log_server,
+            msg = "couch_log_server ignored an_info"
+        },
+        couch_log_test_util:last_log()
+    ).
+
+
+coverage_test() ->
+    Resp = couch_log_server:code_change(foo, bazinga, baz),
+    ?assertEqual({ok, bazinga}, Resp).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_test.erl b/test/couch_log_test.erl
new file mode 100644
index 0000000..1777730
--- /dev/null
+++ b/test/couch_log_test.erl
@@ -0,0 +1,85 @@
+% 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(couch_log_test).
+
+
+-include_lib("couch_log/include/couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+couch_log_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        gen() ++ [fun check_set_level/0]
+    }.
+
+
+check_set_level() ->
+    couch_log:set_level(crit),
+    ?assertEqual("crit", config:get("log", "level")).
+
+
+levels() ->
+    [
+        debug,
+        info,
+        notice,
+        warning,
+        error,
+        critical,
+        alert,
+        emergency,
+        none
+    ].
+
+
+gen() ->
+    lists:map(fun(L) ->
+        Name = "Test log level: " ++ couch_log_util:level_to_string(L),
+        {Name, fun() -> check_levels(L, levels()) end}
+    end, levels() -- [none]).
+
+
+check_levels(_, []) ->
+    ok;
+
+check_levels(TestLevel, [CfgLevel | RestLevels]) ->
+    TestInt = couch_log_util:level_to_integer(TestLevel),
+    CfgInt = couch_log_util:level_to_integer(CfgLevel),
+    Pid = self(),
+    Msg = new_msg(),
+    LastKey = couch_log_test_util:last_log_key(),
+    couch_log_test_util:with_level(CfgLevel, fun() ->
+        couch_log:TestLevel(Msg, []),
+        case TestInt >= CfgInt of
+            true ->
+                ?assertMatch(
+                    #log_entry{
+                        level = TestLevel,
+                        pid = Pid,
+                        msg = Msg
+                    },
+                    couch_log_test_util:last_log()
+                );
+            false ->
+                ?assertEqual(LastKey, couch_log_test_util:last_log_key())
+        end
+    end),
+    check_levels(TestLevel, RestLevels).
+
+
+new_msg() ->
+    random:seed(os:timestamp()),
+    Bin = list_to_binary([random:uniform(255) || _ <- lists:seq(1, 16)]),
+    couch_util:to_hex(Bin).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_test_util.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_test_util.erl b/test/couch_log_test_util.erl
new file mode 100644
index 0000000..2503669
--- /dev/null
+++ b/test/couch_log_test_util.erl
@@ -0,0 +1,153 @@
+% 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(couch_log_test_util).
+-compile(export_all).
+
+
+-include("couch_log.hrl").
+
+
+start() ->
+    remove_error_loggers(),
+    application:set_env(config, ini_files, config_files()),
+    application:start(config),
+    ignore_common_loggers(),
+    application:start(couch_log).
+
+
+stop(_) ->
+    application:stop(config),
+    application:stop(couch_log).
+
+
+with_level(Name, Fun) ->
+    with_config_listener(fun() ->
+        try
+            LevelStr = couch_log_util:level_to_string(Name),
+            config:set("log", "level", LevelStr, false),
+            wait_for_config(),
+            Fun()
+        after
+            config:delete("log", "level", false)
+        end
+    end).
+
+
+with_config_listener(Fun) ->
+    Listener = self(),
+    try
+        add_listener(Listener),
+        Fun()
+    after
+        rem_listener(Listener)
+    end.
+
+
+wait_for_config() ->
+    receive
+        couch_log_config_change_finished -> ok
+    after 1000 ->
+        erlang:error(config_change_timeout)
+    end.
+
+
+with_meck(Mods, Fun) ->
+    lists:foreach(fun(M) ->
+        case M of
+            {Name, Opts} -> meck:new(Name, Opts);
+            Name -> meck:new(Name)
+        end
+    end, Mods),
+    try
+        Fun()
+    after
+        lists:foreach(fun(M) ->
+            case M of
+                {Name, _} -> meck:unload(Name);
+                Name -> meck:unload(Name)
+            end
+        end, Mods)
+    end.
+
+
+ignore_common_loggers() ->
+    IgnoreSet = [
+        application_controller,
+        config,
+        config_event
+    ],
+    lists:foreach(fun(Proc) ->
+        disable_logs_from(Proc)
+    end, IgnoreSet).
+
+
+disable_logs_from(Pid) when is_pid(Pid) ->
+    Ignored = case application:get_env(couch_log, ignored_pids) of
+        {ok, L} when is_list(L) ->
+            lists:usort([Pid | L]);
+        _E ->
+            [Pid]
+    end,
+    IgnoredAlive = [P || P <- Ignored, is_process_alive(P)],
+    application:set_env(couch_log, ignored_pids, IgnoredAlive);
+
+disable_logs_from(Name) when is_atom(Name) ->
+    case whereis(Name) of
+        P when is_pid(P) ->
+            disable_logs_from(P);
+        undefined ->
+            erlang:error({unknown_pid_name, Name})
+    end.
+
+
+last_log_key() ->
+    ets:last(?COUCH_LOG_TEST_TABLE).
+
+
+last_log() ->
+    [{_, Entry}] = ets:lookup(?COUCH_LOG_TEST_TABLE, last_log_key()),
+    Entry.
+
+
+remove_error_loggers() ->
+    lists:foreach(fun(Handler) ->
+        error_logger:delete_report_handler(Handler)
+    end, gen_event:which_handlers(error_logger)).
+
+
+config_files() ->
+    Path = filename:dirname(code:which(?MODULE)),
+    Name = filename:join(Path, "couch_log_test.ini"),
+    ok = file:write_file(Name, "[log]\nwriter = ets\n"),
+    [Name].
+
+
+add_listener(Listener) ->
+    Listeners = case application:get_env(couch_log, config_listeners) of
+        {ok, L} when is_list(L) ->
+            lists:usort([Listener | L]);
+        _ ->
+            [Listener]
+    end,
+    application:set_env(couch_log, config_listeners, Listeners).
+
+
+rem_listener(Listener) ->
+    Listeners = case application:get_env(couch_lig, config_listeners) of
+        {ok, L} when is_list(L) ->
+            L -- [Listener];
+        _ ->
+            []
+    end,
+    application:set_env(couch_log, config_listeners, Listeners).
+

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_util_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_util_test.erl b/test/couch_log_util_test.erl
new file mode 100644
index 0000000..e97911a
--- /dev/null
+++ b/test/couch_log_util_test.erl
@@ -0,0 +1,55 @@
+% 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(couch_log_util_test).
+
+
+-include_lib("couch_log/include/couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+get_message_id_test() ->
+    ?assertEqual("--------", couch_log_util:get_msg_id()),
+    erlang:put(nonce, "deadbeef"),
+    ?assertEqual("deadbeef", couch_log_util:get_msg_id()),
+    erlang:put(nonce, undefined).
+
+
+level_to_atom_test() ->
+    lists:foreach(fun(L) ->
+        ?assert(is_atom(couch_log_util:level_to_atom(L))),
+        ?assert(is_integer(couch_log_util:level_to_integer(L))),
+        ?assert(is_list(couch_log_util:level_to_string(L)))
+    end, levels()).
+
+
+string_p_test() ->
+    ?assertEqual(false, couch_log_util:string_p([])),
+    ?assertEqual(false, couch_log_util:string_p([[false]])),
+    ?assertEqual(true, couch_log_util:string_p([$\n])),
+    ?assertEqual(true, couch_log_util:string_p([$\r])),
+    ?assertEqual(true, couch_log_util:string_p([$\t])),
+    ?assertEqual(true, couch_log_util:string_p([$\v])),
+    ?assertEqual(true, couch_log_util:string_p([$\b])),
+    ?assertEqual(true, couch_log_util:string_p([$\f])),
+    ?assertEqual(true, couch_log_util:string_p([$\e])).
+
+
+levels() ->
+    [
+        1, 2, 3, 4, 5, 6, 7, 8, 9,
+        "1", "2", "3", "4", "5", "6", "7", "8", "9",
+        debug, info, notice, warning, warn, error, err,
+        critical, crit, alert, emergency, emerg, none,
+        "debug", "info", "notice", "warning", "warn", "error", "err",
+        "critical", "crit", "alert", "emergency", "emerg", "none"
+    ].

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_writer_ets.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_writer_ets.erl b/test/couch_log_writer_ets.erl
new file mode 100644
index 0000000..d5fd327
--- /dev/null
+++ b/test/couch_log_writer_ets.erl
@@ -0,0 +1,49 @@
+% 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(couch_log_writer_ets).
+-behaviour(couch_log_writer).
+
+
+-export([
+    init/0,
+    terminate/2,
+    write/2
+]).
+
+
+-include("couch_log.hrl").
+
+
+init() ->
+    ets:new(?COUCH_LOG_TEST_TABLE, [named_table, public, ordered_set]),
+    {ok, 0}.
+
+
+terminate(_, _St) ->
+    ets:delete(?COUCH_LOG_TEST_TABLE),
+    ok.
+
+
+write(Entry0, St) ->
+    Entry = Entry0#log_entry{
+        msg = lists:flatten(Entry0#log_entry.msg),
+        time_stamp = lists:flatten(Entry0#log_entry.time_stamp)
+    },
+    Ignored = application:get_env(couch_log, ignored_pids, []),
+    case lists:member(Entry#log_entry.pid, Ignored) of
+        true ->
+            {ok, St};
+        false ->
+            ets:insert(?COUCH_LOG_TEST_TABLE, {St, Entry}),
+            {ok, St + 1}
+    end.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_writer_file_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_writer_file_test.erl b/test/couch_log_writer_file_test.erl
new file mode 100644
index 0000000..6d3f3ec
--- /dev/null
+++ b/test/couch_log_writer_file_test.erl
@@ -0,0 +1,161 @@
+% 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(couch_log_writer_file_test).
+
+
+-include_lib("kernel/include/file.hrl").
+-include_lib("couch_log/include/couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(WRITER, couch_log_writer_file).
+
+
+couch_log_writer_file_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun check_init_terminate/0,
+            fun() ->
+                couch_log_test_util:with_meck(
+                    [{filelib, [unstick]}],
+                    fun check_ensure_dir_fail/0
+                )
+            end,
+            fun() ->
+                couch_log_test_util:with_meck(
+                    [{file, [unstick, passthrough]}],
+                    fun check_open_fail/0
+                )
+            end,
+            fun() ->
+                couch_log_test_util:with_meck(
+                    [{file, [unstick, passthrough]}],
+                    fun check_read_file_info_fail/0
+                )
+            end,
+            fun check_file_write/0,
+            fun check_buffered_file_write/0,
+            fun check_reopen/0
+        ]
+    }.
+
+
+check_init_terminate() ->
+    {ok, St} = ?WRITER:init(),
+    ok = ?WRITER:terminate(stop, St).
+
+
+check_ensure_dir_fail() ->
+    meck:expect(filelib, ensure_dir, 1, {error, eperm}),
+    ?assertEqual({error, eperm}, ?WRITER:init()),
+    ?assert(meck:called(filelib, ensure_dir, 1)),
+    ?assert(meck:validate(filelib)).
+
+
+check_open_fail() ->
+    meck:expect(file, open, 2, {error, enotfound}),
+    ?assertEqual({error, enotfound}, ?WRITER:init()),
+    ?assert(meck:called(file, open, 2)),
+    ?assert(meck:validate(file)).
+
+
+check_read_file_info_fail() ->
+    RFI = fun
+        ("./couch.log") -> {error, enoent};
+        (Path) -> meck:passthrough([Path])
+    end,
+    meck:expect(file, read_file_info, RFI),
+    ?assertEqual({error, enoent}, ?WRITER:init()),
+    ?assert(meck:called(file, read_file_info, 1)),
+    ?assert(meck:validate(file)).
+
+
+check_file_write() ->
+    % Make sure we have an empty log for this test
+    IsFile = filelib:is_file("./couch.log"),
+    if not IsFile -> ok; true ->
+        file:delete("./couch.log")
+    end,
+
+    Entry = #log_entry{
+        level = info,
+        pid = list_to_pid("<0.1.0>"),
+        msg = "stuff",
+        msg_id = "msg_id",
+        time_stamp = "time_stamp"
+    },
+    {ok, St} = ?WRITER:init(),
+    {ok, NewSt} = ?WRITER:write(Entry, St),
+    ok = ?WRITER:terminate(stop, NewSt),
+
+    {ok, Data} = file:read_file("./couch.log"),
+    Expect = <<"[info] time_stamp nonode@nohost <0.1.0> msg_id stuff\n">>,
+    ?assertEqual(Expect, Data).
+
+
+check_buffered_file_write() ->
+    % Make sure we have an empty log for this test
+    IsFile = filelib:is_file("./couch.log"),
+    if not IsFile -> ok; true ->
+        file:delete("./couch.log")
+    end,
+
+    config:set("log", "write_buffer", "1024"),
+    config:set("log", "write_delay", "10"),
+
+    try
+        Entry = #log_entry{
+            level = info,
+            pid = list_to_pid("<0.1.0>"),
+            msg = "stuff",
+            msg_id = "msg_id",
+            time_stamp = "time_stamp"
+        },
+        {ok, St} = ?WRITER:init(),
+        {ok, NewSt} = ?WRITER:write(Entry, St),
+        ok = ?WRITER:terminate(stop, NewSt)
+    after
+        config:delete("log", "write_buffer"),
+        config:delete("log", "write_delay")
+    end,
+
+    {ok, Data} = file:read_file("./couch.log"),
+    Expect = <<"[info] time_stamp nonode@nohost <0.1.0> msg_id stuff\n">>,
+    ?assertEqual(Expect, Data).
+
+
+check_reopen() ->
+    {ok, St1} = clear_clock(?WRITER:init()),
+    {ok, St2} = clear_clock(couch_log_writer_file:maybe_reopen(St1)),
+    ?assertEqual(St1, St2),
+
+    % Delete file
+    file:delete("./couch.log"),
+    {ok, St3} = clear_clock(couch_log_writer_file:maybe_reopen(St2)),
+    ?assert(element(3, St3) /= element(3, St2)),
+
+    % Recreate file
+    file:delete("./couch.log"),
+    file:write_file("./couch.log", ""),
+    {ok, St4} = clear_clock(couch_log_writer_file:maybe_reopen(St3)),
+    ?assert(element(3, St4) /= element(3, St2)).
+
+
+clear_clock({ok, St}) ->
+    {ok, clear_clock(St)};
+
+clear_clock(St) ->
+    {st, Path, Fd, INode, _} = St,
+    {st, Path, Fd, INode, {0, 0, 0}}.

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_writer_stderr_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_writer_stderr_test.erl b/test/couch_log_writer_stderr_test.erl
new file mode 100644
index 0000000..1e99263
--- /dev/null
+++ b/test/couch_log_writer_stderr_test.erl
@@ -0,0 +1,58 @@
+% 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(couch_log_writer_stderr_test).
+
+
+-include_lib("couch_log/include/couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(WRITER, couch_log_writer_stderr).
+
+
+couch_log_writer_stderr_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun check_init_terminate/0,
+            fun() ->
+                couch_log_test_util:with_meck(
+                    [{io, [unstick]}],
+                    fun check_write/0
+                )
+            end
+        ]
+    }.
+
+
+check_init_terminate() ->
+    {ok, St} = ?WRITER:init(),
+    ok = ?WRITER:terminate(stop, St).
+
+
+check_write() ->
+    meck:expect(io, format, 3, ok),
+
+    Entry = #log_entry{
+        level = debug,
+        pid = list_to_pid("<0.1.0>"),
+        msg = "stuff",
+        msg_id = "msg_id",
+        time_stamp = "time_stamp"
+    },
+    {ok, St} = ?WRITER:init(),
+    {ok, NewSt} = ?WRITER:write(Entry, St),
+    ok = ?WRITER:terminate(stop, NewSt),
+
+    ?assert(meck:validate(io)).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_writer_syslog_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_writer_syslog_test.erl b/test/couch_log_writer_syslog_test.erl
new file mode 100644
index 0000000..c32b5c6
--- /dev/null
+++ b/test/couch_log_writer_syslog_test.erl
@@ -0,0 +1,122 @@
+% 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(couch_log_writer_syslog_test).
+
+
+-include_lib("couch_log/include/couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+-define(WRITER, couch_log_writer_syslog).
+
+
+couch_log_writer_syslog_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun check_init_terminate/0,
+            fun() ->
+                couch_log_test_util:with_meck(
+                    [{io, [unstick]}],
+                    fun check_stderr_write/0
+                )
+            end,
+            fun() ->
+                couch_log_test_util:with_meck(
+                    [{gen_udp, [unstick]}],
+                    fun check_udp_send/0
+                )
+            end
+        ]
+    }.
+
+
+check_init_terminate() ->
+    {ok, St} = ?WRITER:init(),
+    ok = ?WRITER:terminate(stop, St).
+
+
+check_stderr_write() ->
+    meck:expect(io, format, 3, ok),
+
+    Entry = #log_entry{
+        level = debug,
+        pid = list_to_pid("<0.1.0>"),
+        msg = "stuff",
+        msg_id = "msg_id",
+        time_stamp = "time_stamp"
+    },
+    {ok, St} = ?WRITER:init(),
+    {ok, NewSt} = ?WRITER:write(Entry, St),
+    ok = ?WRITER:terminate(stop, NewSt),
+
+    ?assert(meck:called(io, format, 3)),
+    ?assert(meck:validate(io)).
+
+
+check_udp_send() ->
+    meck:expect(gen_udp, open, 1, {ok, socket}),
+    meck:expect(gen_udp, send, 4, ok),
+    meck:expect(gen_udp, close, fun(socket) -> ok end),
+
+    config:set("log", "syslog_host", "localhost"),
+    try
+        Entry = #log_entry{
+            level = debug,
+            pid = list_to_pid("<0.1.0>"),
+            msg = "stuff",
+            msg_id = "msg_id",
+            time_stamp = "time_stamp"
+        },
+        {ok, St} = ?WRITER:init(),
+        {ok, NewSt} = ?WRITER:write(Entry, St),
+        ok = ?WRITER:terminate(stop, NewSt)
+    after
+        config:delete("log", "syslog_host")
+    end,
+
+    ?assert(meck:called(gen_udp, open, 1)),
+    ?assert(meck:called(gen_udp, send, 4)),
+    ?assert(meck:called(gen_udp, close, 1)),
+    ?assert(meck:validate(gen_udp)).
+
+
+facility_test() ->
+    Names = [
+        "kern", "user", "mail", "daemon", "auth", "syslog", "lpr",
+        "news", "uucp", "clock", "authpriv", "ftp", "ntp", "audit",
+        "alert", "cron", "local0", "local1", "local2", "local3",
+        "local4", "local5", "local6", "local7"
+    ],
+    lists:foldl(fun(Name, Id) ->
+        IdStr = lists:flatten(io_lib:format("~w", [Id])),
+        ?assertEqual(Id bsl 3, couch_log_writer_syslog:get_facility(Name)),
+        ?assertEqual(Id bsl 3, couch_log_writer_syslog:get_facility(IdStr)),
+        Id + 1
+    end, 0, Names),
+    ?assertEqual(23 bsl 3, couch_log_writer_syslog:get_facility("foo")),
+    ?assertEqual(23 bsl 3, couch_log_writer_syslog:get_facility("-1")),
+    ?assertEqual(23 bsl 3, couch_log_writer_syslog:get_facility("24")).
+
+
+level_test() ->
+    Levels = [
+        emergency, alert, critical, error,
+        warning, notice, info, debug
+    ],
+    lists:foldl(fun(Name, Id) ->
+        ?assertEqual(Id, couch_log_writer_syslog:get_level(Name)),
+        Id + 1
+    end, 0, Levels),
+    ?assertEqual(3, couch_log_writer_syslog:get_level(foo)).

http://git-wip-us.apache.org/repos/asf/couchdb-couch-log/blob/16a46572/test/couch_log_writer_test.erl
----------------------------------------------------------------------
diff --git a/test/couch_log_writer_test.erl b/test/couch_log_writer_test.erl
new file mode 100644
index 0000000..d0bb347
--- /dev/null
+++ b/test/couch_log_writer_test.erl
@@ -0,0 +1,54 @@
+% 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(couch_log_writer_test).
+
+
+-include_lib("couch_log/include/couch_log.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+
+couch_log_writer_test_() ->
+    {setup,
+        fun couch_log_test_util:start/0,
+        fun couch_log_test_util:stop/1,
+        [
+            fun check_writer_change/0
+        ]
+    }.
+
+
+check_writer_change() ->
+    % Change to file and back
+    couch_log_test_util:with_config_listener(fun() ->
+        config:set("log", "writer", "file"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(undefined, ets:info(?COUCH_LOG_TEST_TABLE)),
+        ?assert(is_pid(whereis(couch_log_server))),
+
+        config:set("log", "writer", "couch_log_writer_ets"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(0, ets:info(?COUCH_LOG_TEST_TABLE, size))
+    end),
+
+    % Using a bad setting doesn't break things
+    couch_log_test_util:with_config_listener(fun() ->
+        config:set("log", "writer", "hopefully not an atom or module"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(undefined, ets:info(?COUCH_LOG_TEST_TABLE)),
+        ?assert(is_pid(whereis(couch_log_server))),
+
+        config:set("log", "writer", "couch_log_writer_ets"),
+        couch_log_test_util:wait_for_config(),
+        ?assertEqual(0, ets:info(?COUCH_LOG_TEST_TABLE, size))
+    end).
+