summaryrefslogtreecommitdiff
path: root/apps/couch/test/etap
diff options
context:
space:
mode:
Diffstat (limited to 'apps/couch/test/etap')
-rwxr-xr-xapps/couch/test/etap/001-load.t68
-rw-r--r--apps/couch/test/etap/002-icu-driver.t33
-rwxr-xr-xapps/couch/test/etap/010-file-basics.t107
-rwxr-xr-xapps/couch/test/etap/011-file-headers.t145
-rwxr-xr-xapps/couch/test/etap/020-btree-basics.t234
-rwxr-xr-xapps/couch/test/etap/021-btree-reductions.t142
-rwxr-xr-xapps/couch/test/etap/030-doc-from-json.t234
-rwxr-xr-xapps/couch/test/etap/031-doc-to-json.t194
-rwxr-xr-xapps/couch/test/etap/040-util.t80
-rw-r--r--apps/couch/test/etap/041-uuid-gen-seq.ini19
-rw-r--r--apps/couch/test/etap/041-uuid-gen-utc.ini19
-rwxr-xr-xapps/couch/test/etap/041-uuid-gen.t116
-rwxr-xr-xapps/couch/test/etap/050-stream.t88
-rwxr-xr-xapps/couch/test/etap/060-kt-merging.t115
-rwxr-xr-xapps/couch/test/etap/061-kt-missing-leaves.t65
-rwxr-xr-xapps/couch/test/etap/062-kt-remove-leaves.t69
-rwxr-xr-xapps/couch/test/etap/063-kt-get-leaves.t98
-rwxr-xr-xapps/couch/test/etap/064-kt-counting.t46
-rwxr-xr-xapps/couch/test/etap/065-kt-stemming.t42
-rw-r--r--apps/couch/test/etap/080-config-get-set.ini8
-rwxr-xr-xapps/couch/test/etap/080-config-get-set.t129
-rw-r--r--apps/couch/test/etap/081-config-default.ini6
-rw-r--r--apps/couch/test/etap/081-config-override.1.ini22
-rw-r--r--apps/couch/test/etap/081-config-override.2.ini22
-rwxr-xr-xapps/couch/test/etap/081-config-override.t213
-rw-r--r--apps/couch/test/etap/082-config-default.ini6
-rwxr-xr-xapps/couch/test/etap/082-config-register.t95
-rwxr-xr-xapps/couch/test/etap/083-config-no-files.t56
-rwxr-xr-xapps/couch/test/etap/090-task-status.t209
-rwxr-xr-xapps/couch/test/etap/100-ref-counter.t114
-rwxr-xr-xapps/couch/test/etap/120-stats-collect.inactive150
-rw-r--r--apps/couch/test/etap/121-stats-aggregates.cfg19
-rwxr-xr-xapps/couch/test/etap/121-stats-aggregates.inactive171
-rw-r--r--apps/couch/test/etap/121-stats-aggregates.ini20
-rw-r--r--apps/couch/test/etap/etap.beambin0 -> 13880 bytes
-rw-r--r--apps/couch/test/etap/run.tpl27
36 files changed, 3181 insertions, 0 deletions
diff --git a/apps/couch/test/etap/001-load.t b/apps/couch/test/etap/001-load.t
new file mode 100755
index 00000000..73bf66d3
--- /dev/null
+++ b/apps/couch/test/etap/001-load.t
@@ -0,0 +1,68 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+% Test that we can load each module.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(37),
+ Modules = [
+ couch_btree,
+ couch_config,
+ couch_config_writer,
+ couch_db,
+ couch_db_update_notifier,
+ couch_db_update_notifier_sup,
+ couch_db_updater,
+ couch_doc,
+ couch_event_sup,
+ couch_external_manager,
+ couch_external_server,
+ couch_file,
+ couch_httpd,
+ couch_httpd_db,
+ couch_httpd_external,
+ couch_httpd_misc_handlers,
+ couch_httpd_show,
+ couch_httpd_stats_handlers,
+ couch_httpd_view,
+ couch_key_tree,
+ couch_log,
+ couch_os_process,
+ couch_query_servers,
+ couch_ref_counter,
+ couch_rep,
+ couch_rep_sup,
+ couch_server,
+ couch_server_sup,
+ couch_stats_aggregator,
+ couch_stats_collector,
+ couch_stream,
+ couch_task_status,
+ couch_util,
+ couch_view,
+ couch_view_compactor,
+ couch_view_group,
+ couch_view_updater
+ ],
+
+ lists:foreach(
+ fun(Module) ->
+ etap:loaded_ok(
+ Module,
+ lists:concat(["Loaded: ", Module])
+ )
+ end, Modules),
+ etap:end_tests().
diff --git a/apps/couch/test/etap/002-icu-driver.t b/apps/couch/test/etap/002-icu-driver.t
new file mode 100644
index 00000000..4167aeeb
--- /dev/null
+++ b/apps/couch/test/etap/002-icu-driver.t
@@ -0,0 +1,33 @@
+#!/usr/bin/env escript
+% 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.
+
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(3),
+ etap:is(
+ erl_ddll:load(code:priv_dir(couch), "couch_icu_driver"),
+ ok,
+ "Started couch_icu_driver."
+ ),
+ etap:is(
+ couch_util:collate(<<"foo">>, <<"bar">>),
+ 1,
+ "Can collate stuff"
+ ),
+ etap:is(
+ couch_util:collate(<<"A">>, <<"aa">>),
+ -1,
+ "Collate's non-ascii style."
+ ),
+ etap:end_tests().
diff --git a/apps/couch/test/etap/010-file-basics.t b/apps/couch/test/etap/010-file-basics.t
new file mode 100755
index 00000000..a3599f1a
--- /dev/null
+++ b/apps/couch/test/etap/010-file-basics.t
@@ -0,0 +1,107 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+% 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.
+
+filename() -> test_util:build_file("test/etap/temp.010").
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(19),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail()
+ end,
+ ok.
+
+test() ->
+ etap:is({error, enoent}, couch_file:open("not a real file"),
+ "Opening a non-existant file should return an enoent error."),
+
+ etap:fun_is(
+ fun({ok, _}) -> true; (_) -> false end,
+ couch_file:open(filename() ++ ".1", [create, invalid_option]),
+ "Invalid flags to open are ignored."
+ ),
+
+ {ok, Fd} = couch_file:open(filename() ++ ".0", [create, overwrite]),
+ etap:ok(is_pid(Fd),
+ "Returned file descriptor is a Pid"),
+
+ etap:is({ok, 0}, couch_file:bytes(Fd),
+ "Newly created files have 0 bytes."),
+
+ etap:is({ok, 0}, couch_file:append_term(Fd, foo),
+ "Appending a term returns the previous end of file position."),
+
+ {ok, Size} = couch_file:bytes(Fd),
+ etap:is_greater(Size, 0,
+ "Writing a term increased the file size."),
+
+ etap:is({ok, Size}, couch_file:append_binary(Fd, <<"fancy!">>),
+ "Appending a binary returns the current file size."),
+
+ etap:is({ok, foo}, couch_file:pread_term(Fd, 0),
+ "Reading the first term returns what we wrote: foo"),
+
+ etap:is({ok, <<"fancy!">>}, couch_file:pread_binary(Fd, Size),
+ "Reading back the binary returns what we wrote: <<\"fancy\">>."),
+
+ etap:is({ok, <<131, 100, 0, 3, 102, 111, 111>>},
+ couch_file:pread_binary(Fd, 0),
+ "Reading a binary at a term position returns the term as binary."
+ ),
+
+ {ok, BinPos} = couch_file:append_binary(Fd, <<131,100,0,3,102,111,111>>),
+ etap:is({ok, foo}, couch_file:pread_term(Fd, BinPos),
+ "Reading a term from a written binary term representation succeeds."),
+
+ BigBin = list_to_binary(lists:duplicate(100000, 0)),
+ {ok, BigBinPos} = couch_file:append_binary(Fd, BigBin),
+ etap:is({ok, BigBin}, couch_file:pread_binary(Fd, BigBinPos),
+ "Reading a large term from a written representation succeeds."),
+
+ ok = couch_file:write_header(Fd, hello),
+ etap:is({ok, hello}, couch_file:read_header(Fd),
+ "Reading a header succeeds."),
+
+ {ok, BigBinPos2} = couch_file:append_binary(Fd, BigBin),
+ etap:is({ok, BigBin}, couch_file:pread_binary(Fd, BigBinPos2),
+ "Reading a large term from a written representation succeeds 2."),
+
+ % append_binary == append_iolist?
+ % Possible bug in pread_iolist or iolist() -> append_binary
+ {ok, IOLPos} = couch_file:append_binary(Fd, ["foo", $m, <<"bam">>]),
+ etap:is({ok, [<<"foombam">>]}, couch_file:pread_iolist(Fd, IOLPos),
+ "Reading an results in a binary form of the written iolist()"),
+
+ % XXX: How does on test fsync?
+ etap:is(ok, couch_file:sync(Fd),
+ "Syncing does not cause an error."),
+
+ etap:is(ok, couch_file:truncate(Fd, Size),
+ "Truncating a file succeeds."),
+
+ %etap:is(eof, (catch couch_file:pread_binary(Fd, Size)),
+ % "Reading data that was truncated fails.")
+ etap:skip(fun() -> ok end,
+ "No idea how to test reading beyond EOF"),
+
+ etap:is({ok, foo}, couch_file:pread_term(Fd, 0),
+ "Truncating does not affect data located before the truncation mark."),
+
+ etap:is(ok, couch_file:close(Fd),
+ "Files close properly."),
+ ok.
diff --git a/apps/couch/test/etap/011-file-headers.t b/apps/couch/test/etap/011-file-headers.t
new file mode 100755
index 00000000..4705f629
--- /dev/null
+++ b/apps/couch/test/etap/011-file-headers.t
@@ -0,0 +1,145 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+%%! -pa ./src/couchdb -sasl errlog_type error -boot start_sasl -noshell
+
+% 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.
+
+filename() -> test_util:build_file("test/etap/temp.011").
+sizeblock() -> 4096. % Need to keep this in sync with couch_file.erl
+
+main(_) ->
+ test_util:init_code_path(),
+ {S1, S2, S3} = now(),
+ random:seed(S1, S2, S3),
+
+ etap:plan(17),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail()
+ end,
+ ok.
+
+test() ->
+ {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
+
+ etap:is({ok, 0}, couch_file:bytes(Fd),
+ "File should be initialized to contain zero bytes."),
+
+ etap:is(ok, couch_file:write_header(Fd, {<<"some_data">>, 32}),
+ "Writing a header succeeds."),
+
+ {ok, Size1} = couch_file:bytes(Fd),
+ etap:is_greater(Size1, 0,
+ "Writing a header allocates space in the file."),
+
+ etap:is({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd),
+ "Reading the header returns what we wrote."),
+
+ etap:is(ok, couch_file:write_header(Fd, [foo, <<"more">>]),
+ "Writing a second header succeeds."),
+
+ {ok, Size2} = couch_file:bytes(Fd),
+ etap:is_greater(Size2, Size1,
+ "Writing a second header allocates more space."),
+
+ etap:is({ok, [foo, <<"more">>]}, couch_file:read_header(Fd),
+ "Reading the second header does not return the first header."),
+
+ % Delete the second header.
+ ok = couch_file:truncate(Fd, Size1),
+
+ etap:is({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd),
+ "Reading the header after a truncation returns a previous header."),
+
+ couch_file:write_header(Fd, [foo, <<"more">>]),
+ etap:is({ok, Size2}, couch_file:bytes(Fd),
+ "Rewriting the same second header returns the same second size."),
+
+ ok = couch_file:close(Fd),
+
+ % Now for the fun stuff. Try corrupting the second header and see
+ % if we recover properly.
+
+ % Destroy the 0x1 byte that marks a header
+ check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
+ etap:isnt(Expect, couch_file:read_header(CouchFd),
+ "Should return a different header before corruption."),
+ file:pwrite(RawFd, HeaderPos, <<0>>),
+ etap:is(Expect, couch_file:read_header(CouchFd),
+ "Corrupting the byte marker should read the previous header.")
+ end),
+
+ % Corrupt the size.
+ check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
+ etap:isnt(Expect, couch_file:read_header(CouchFd),
+ "Should return a different header before corruption."),
+ % +1 for 0x1 byte marker
+ file:pwrite(RawFd, HeaderPos+1, <<10/integer>>),
+ etap:is(Expect, couch_file:read_header(CouchFd),
+ "Corrupting the size should read the previous header.")
+ end),
+
+ % Corrupt the MD5 signature
+ check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
+ etap:isnt(Expect, couch_file:read_header(CouchFd),
+ "Should return a different header before corruption."),
+ % +5 = +1 for 0x1 byte and +4 for term size.
+ file:pwrite(RawFd, HeaderPos+5, <<"F01034F88D320B22">>),
+ etap:is(Expect, couch_file:read_header(CouchFd),
+ "Corrupting the MD5 signature should read the previous header.")
+ end),
+
+ % Corrupt the data
+ check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
+ etap:isnt(Expect, couch_file:read_header(CouchFd),
+ "Should return a different header before corruption."),
+ % +21 = +1 for 0x1 byte, +4 for term size and +16 for MD5 sig
+ file:pwrite(RawFd, HeaderPos+21, <<"some data goes here!">>),
+ etap:is(Expect, couch_file:read_header(CouchFd),
+ "Corrupting the header data should read the previous header.")
+ end),
+
+ ok.
+
+check_header_recovery(CheckFun) ->
+ {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
+ {ok, RawFd} = file:open(filename(), [read, write, raw, binary]),
+
+ {ok, _} = write_random_data(Fd),
+ ExpectHeader = {some_atom, <<"a binary">>, 756},
+ ok = couch_file:write_header(Fd, ExpectHeader),
+
+ {ok, HeaderPos} = write_random_data(Fd),
+ ok = couch_file:write_header(Fd, {2342, <<"corruption! greed!">>}),
+
+ CheckFun(Fd, RawFd, {ok, ExpectHeader}, HeaderPos),
+
+ ok = file:close(RawFd),
+ ok = couch_file:close(Fd),
+ ok.
+
+write_random_data(Fd) ->
+ write_random_data(Fd, 100 + random:uniform(1000)).
+
+write_random_data(Fd, 0) ->
+ {ok, Bytes} = couch_file:bytes(Fd),
+ {ok, (1 + Bytes div sizeblock()) * sizeblock()};
+write_random_data(Fd, N) ->
+ Choices = [foo, bar, <<"bizzingle">>, "bank", ["rough", stuff]],
+ Term = lists:nth(random:uniform(4) + 1, Choices),
+ {ok, _} = couch_file:append_term(Fd, Term),
+ write_random_data(Fd, N-1).
+
diff --git a/apps/couch/test/etap/020-btree-basics.t b/apps/couch/test/etap/020-btree-basics.t
new file mode 100755
index 00000000..536c9fb1
--- /dev/null
+++ b/apps/couch/test/etap/020-btree-basics.t
@@ -0,0 +1,234 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+%%! -pa ./src/couchdb -sasl errlog_type error -boot start_sasl -noshell
+
+% 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.
+
+filename() -> test_util:build_file("test/etap/temp.020").
+rows() -> 250.
+
+-record(btree, {fd, root, extract_kv, assemble_kv, less, reduce}).
+
+main(_) ->
+ test_util:init_code_path(),
+ couch_config:start_link([]),
+ etap:plan(51),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail()
+ end,
+ ok.
+
+%% @todo Determine if this number should be greater to see if the btree was
+%% broken into multiple nodes. AKA "How do we appropiately detect if multiple
+%% nodes were created."
+test()->
+ Sorted = [{Seq, random:uniform()} || Seq <- lists:seq(1, rows())],
+ etap:ok(test_kvs(Sorted), "Testing sorted keys"),
+ etap:ok(test_kvs(lists:reverse(Sorted)), "Testing reversed sorted keys"),
+ etap:ok(test_kvs(shuffle(Sorted)), "Testing shuffled keys."),
+ etap:ok(test_fold_reductions(Sorted), "Testing fold_reduce."),
+ ok.
+
+test_kvs(KeyValues) ->
+ ReduceFun = fun
+ (reduce, KVs) ->
+ length(KVs);
+ (rereduce, Reds) ->
+ lists:sum(Reds)
+ end,
+
+ Keys = [K || {K, _} <- KeyValues],
+
+ {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
+ {ok, Btree} = couch_btree:open(nil, Fd),
+ etap:ok(is_record(Btree, btree), "Created btree is really a btree record"),
+ etap:is(Btree#btree.fd, Fd, "Btree#btree.fd is set correctly."),
+ etap:is(Btree#btree.root, nil, "Btree#btree.root is set correctly."),
+
+ Btree1 = couch_btree:set_options(Btree, [{reduce, ReduceFun}]),
+ etap:is(Btree1#btree.reduce, ReduceFun, "Reduce function was set"),
+ {ok, _, EmptyRes} = couch_btree:foldl(Btree1, fun(_, X) -> {ok, X+1} end, 0),
+ etap:is(EmptyRes, 0, "Folding over an empty btree"),
+
+ {ok, Btree2} = couch_btree:add_remove(Btree1, KeyValues, []),
+ etap:ok(test_btree(Btree2, KeyValues),
+ "Adding all keys at once returns a complete btree."),
+
+ etap:fun_is(
+ fun
+ ({ok, {kp_node, _}}) -> true;
+ (_) -> false
+ end,
+ couch_file:pread_term(Fd, element(1, Btree2#btree.root)),
+ "Btree root pointer is a kp_node."
+ ),
+
+ {ok, Btree3} = couch_btree:add_remove(Btree2, [], Keys),
+ etap:ok(test_btree(Btree3, []),
+ "Removing all keys at once returns an empty btree."),
+
+ Btree4 = lists:foldl(fun(KV, BtAcc) ->
+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []),
+ BtAcc2
+ end, Btree3, KeyValues),
+ etap:ok(test_btree(Btree4, KeyValues),
+ "Adding all keys one at a time returns a complete btree."),
+
+ Btree5 = lists:foldl(fun({K, _}, BtAcc) ->
+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [], [K]),
+ BtAcc2
+ end, Btree4, KeyValues),
+ etap:ok(test_btree(Btree5, []),
+ "Removing all keys one at a time returns an empty btree."),
+
+ KeyValuesRev = lists:reverse(KeyValues),
+ Btree6 = lists:foldl(fun(KV, BtAcc) ->
+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []),
+ BtAcc2
+ end, Btree5, KeyValuesRev),
+ etap:ok(test_btree(Btree6, KeyValues),
+ "Adding all keys in reverse order returns a complete btree."),
+
+ {_, Rem2Keys0, Rem2Keys1} = lists:foldl(fun(X, {Count, Left, Right}) ->
+ case Count rem 2 == 0 of
+ true-> {Count+1, [X | Left], Right};
+ false -> {Count+1, Left, [X | Right]}
+ end
+ end, {0, [], []}, KeyValues),
+
+ etap:ok(test_add_remove(Btree6, Rem2Keys0, Rem2Keys1),
+ "Add/Remove every other key."),
+
+ etap:ok(test_add_remove(Btree6, Rem2Keys1, Rem2Keys0),
+ "Add/Remove opposite every other key."),
+
+ {ok, Btree7} = couch_btree:add_remove(Btree6, [], [K||{K,_}<-Rem2Keys1]),
+ {ok, Btree8} = couch_btree:add_remove(Btree7, [], [K||{K,_}<-Rem2Keys0]),
+ etap:ok(test_btree(Btree8, []),
+ "Removing both halves of every other key returns an empty btree."),
+
+ %% Third chunk (close out)
+ etap:is(couch_file:close(Fd), ok, "closing out"),
+ true.
+
+test_btree(Btree, KeyValues) ->
+ ok = test_key_access(Btree, KeyValues),
+ ok = test_lookup_access(Btree, KeyValues),
+ ok = test_final_reductions(Btree, KeyValues),
+ true.
+
+test_add_remove(Btree, OutKeyValues, RemainingKeyValues) ->
+ Btree2 = lists:foldl(fun({K, _}, BtAcc) ->
+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [], [K]),
+ BtAcc2
+ end, Btree, OutKeyValues),
+ true = test_btree(Btree2, RemainingKeyValues),
+
+ Btree3 = lists:foldl(fun(KV, BtAcc) ->
+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []),
+ BtAcc2
+ end, Btree2, OutKeyValues),
+ true = test_btree(Btree3, OutKeyValues ++ RemainingKeyValues).
+
+test_key_access(Btree, List) ->
+ FoldFun = fun(Element, {[HAcc|TAcc], Count}) ->
+ case Element == HAcc of
+ true -> {ok, {TAcc, Count + 1}};
+ _ -> {ok, {TAcc, Count + 1}}
+ end
+ end,
+ Length = length(List),
+ Sorted = lists:sort(List),
+ {ok, _, {[], Length}} = couch_btree:foldl(Btree, FoldFun, {Sorted, 0}),
+ {ok, _, {[], Length}} = couch_btree:fold(Btree, FoldFun, {Sorted, 0}, [{dir, rev}]),
+ ok.
+
+test_lookup_access(Btree, KeyValues) ->
+ FoldFun = fun({Key, Value}, {Key, Value}) -> {stop, true} end,
+ lists:foreach(fun({Key, Value}) ->
+ [{ok, {Key, Value}}] = couch_btree:lookup(Btree, [Key]),
+ {ok, _, true} = couch_btree:foldl(Btree, FoldFun, {Key, Value}, [{start_key, Key}])
+ end, KeyValues).
+
+test_final_reductions(Btree, KeyValues) ->
+ KVLen = length(KeyValues),
+ FoldLFun = fun(_X, LeadingReds, Acc) ->
+ CountToStart = KVLen div 3 + Acc,
+ CountToStart = couch_btree:final_reduce(Btree, LeadingReds),
+ {ok, Acc+1}
+ end,
+ FoldRFun = fun(_X, LeadingReds, Acc) ->
+ CountToEnd = KVLen - KVLen div 3 + Acc,
+ CountToEnd = couch_btree:final_reduce(Btree, LeadingReds),
+ {ok, Acc+1}
+ end,
+ {LStartKey, _} = case KVLen of
+ 0 -> {nil, nil};
+ _ -> lists:nth(KVLen div 3 + 1, lists:sort(KeyValues))
+ end,
+ {RStartKey, _} = case KVLen of
+ 0 -> {nil, nil};
+ _ -> lists:nth(KVLen div 3, lists:sort(KeyValues))
+ end,
+ {ok, _, FoldLRed} = couch_btree:foldl(Btree, FoldLFun, 0, [{start_key, LStartKey}]),
+ {ok, _, FoldRRed} = couch_btree:fold(Btree, FoldRFun, 0, [{dir, rev}, {start_key, RStartKey}]),
+ KVLen = FoldLRed + FoldRRed,
+ ok.
+
+test_fold_reductions(KeyValues) ->
+
+ {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
+ {ok, Btree} = couch_btree:open(nil, Fd),
+ {ok, Btree2} = couch_btree:add_remove(Btree, KeyValues, []),
+
+ GroupFun = fun(Key1,Key2) -> Key1 == Key2 end,
+
+ FoldLFun = fun(_X, _LeadingReds, Acc) ->
+ {ok, Acc + 1} end,
+ {ok, Res1} =
+ couch_btree:fold_reduce(Btree2,
+ FoldLFun,0,
+ [{key_group_fun, GroupFun},
+ {start_key,126},
+ {end_key,250}]),
+ etap:is(Res1,125,"including the end key ought to return half the keys"),
+ {ok, Res2} =
+ couch_btree:fold_reduce(Btree2,
+ FoldLFun,0,
+ [{key_group_fun, GroupFun},
+ {start_key,126},
+ {end_key_gt,250}]),
+ etap:is(Res2,124,"not including the end key ought to return one less than half the keys"),
+ true.
+
+
+shuffle(List) ->
+ randomize(round(math:log(length(List)) + 0.5), List).
+
+randomize(1, List) ->
+ randomize(List);
+randomize(T, List) ->
+ lists:foldl(fun(_E, Acc) ->
+ randomize(Acc)
+ end, randomize(List), lists:seq(1, (T - 1))).
+
+randomize(List) ->
+ D = lists:map(fun(A) ->
+ {random:uniform(), A}
+ end, List),
+ {_, D1} = lists:unzip(lists:keysort(1, D)),
+ D1.
diff --git a/apps/couch/test/etap/021-btree-reductions.t b/apps/couch/test/etap/021-btree-reductions.t
new file mode 100755
index 00000000..30ffd530
--- /dev/null
+++ b/apps/couch/test/etap/021-btree-reductions.t
@@ -0,0 +1,142 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+%%! -pa ./src/couchdb -sasl errlog_type error -boot start_sasl -noshell
+
+% 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.
+
+filename() -> "./apps/couch/test/etap/temp.021".
+rows() -> 1000.
+
+main(_) ->
+ test_util:init_code_path(),
+ couch_config:start_link([]),
+ etap:plan(8),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail()
+ end,
+ ok.
+
+test()->
+ ReduceFun = fun
+ (reduce, KVs) -> length(KVs);
+ (rereduce, Reds) -> lists:sum(Reds)
+ end,
+
+ {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
+ {ok, Btree} = couch_btree:open(nil, Fd, [{reduce, ReduceFun}]),
+
+ % Create a list, of {"even", Value} or {"odd", Value} pairs.
+ {_, EvenOddKVs} = lists:foldl(fun(Idx, {Key, Acc}) ->
+ case Key of
+ "even" -> {"odd", [{{Key, Idx}, 1} | Acc]};
+ _ -> {"even", [{{Key, Idx}, 1} | Acc]}
+ end
+ end, {"odd", []}, lists:seq(1, rows())),
+
+ {ok, Btree2} = couch_btree:add_remove(Btree, EvenOddKVs, []),
+
+ GroupFun = fun({K1, _}, {K2, _}) -> K1 == K2 end,
+ FoldFun = fun(GroupedKey, Unreduced, Acc) ->
+ {ok, [{GroupedKey, couch_btree:final_reduce(Btree2, Unreduced)} | Acc]}
+ end,
+
+ {SK1, EK1} = {{"even", -1}, {"even", foo}},
+ {SK2, EK2} = {{"odd", -1}, {"odd", foo}},
+
+ etap:fun_is(
+ fun
+ ({ok, [{{"odd", _}, 500}, {{"even", _}, 500}]}) ->
+ true;
+ (_) ->
+ false
+ end,
+ couch_btree:fold_reduce(Btree2, FoldFun, [], [{key_group_fun, GroupFun}]),
+ "Reduction works with no specified direction, startkey, or endkey."
+ ),
+
+ etap:fun_is(
+ fun
+ ({ok, [{{"odd", _}, 500}, {{"even", _}, 500}]}) ->
+ true;
+ (_) ->
+ false
+ end,
+ couch_btree:fold_reduce(Btree2, FoldFun, [], [{key_group_fun, GroupFun}, {dir, fwd}]),
+ "Reducing forward works with no startkey or endkey."
+ ),
+
+ etap:fun_is(
+ fun
+ ({ok, [{{"even", _}, 500}, {{"odd", _}, 500}]}) ->
+ true;
+ (_) ->
+ false
+ end,
+ couch_btree:fold_reduce(Btree2, FoldFun, [], [{key_group_fun, GroupFun}, {dir, rev}]),
+ "Reducing backwards works with no startkey or endkey."
+ ),
+
+ etap:fun_is(
+ fun
+ ({ok, [{{"odd", _}, 500}, {{"even", _}, 500}]}) ->
+ true;
+ (_) ->
+ false
+ end,
+ couch_btree:fold_reduce(Btree2, FoldFun, [], [{dir, fwd}, {key_group_fun, GroupFun}, {start_key, SK1}, {end_key, EK2}]),
+ "Reducing works over the entire range with startkey and endkey set."
+ ),
+
+ etap:fun_is(
+ fun
+ ({ok, [{{"even", _}, 500}]}) -> true;
+ (_) -> false
+ end,
+ couch_btree:fold_reduce(Btree2, FoldFun, [], [{dir, fwd}, {key_group_fun, GroupFun}, {start_key, SK1}, {end_key, EK1}]),
+ "Reducing foward over first half works with a startkey and endkey."
+ ),
+
+ etap:fun_is(
+ fun
+ ({ok, [{{"odd", _}, 500}]}) -> true;
+ (_) -> false
+ end,
+ couch_btree:fold_reduce(Btree2, FoldFun, [], [{dir, fwd}, {key_group_fun, GroupFun}, {start_key, SK2}, {end_key, EK2}]),
+ "Reducing foward over second half works with second startkey and endkey"
+ ),
+
+ etap:fun_is(
+ fun
+ ({ok, [{{"odd", _}, 500}]}) -> true;
+ (_) -> false
+ end,
+ couch_btree:fold_reduce(Btree2, FoldFun, [], [{dir, rev}, {key_group_fun, GroupFun}, {start_key, EK2}, {end_key, SK2}]),
+ "Reducing in reverse works after swapping the startkey and endkey."
+ ),
+
+ etap:fun_is(
+ fun
+ ({ok, [{{"even", _}, 500}, {{"odd", _}, 500}]}) ->
+ true;
+ (_) ->
+ false
+ end,
+ couch_btree:fold_reduce(Btree2, FoldFun, [], [{dir, rev}, {key_group_fun, GroupFun}, {start_key, EK2}, {end_key, SK1}]),
+ "Reducing in reverse results in reversed accumulator."
+ ),
+
+ couch_file:close(Fd).
diff --git a/apps/couch/test/etap/030-doc-from-json.t b/apps/couch/test/etap/030-doc-from-json.t
new file mode 100755
index 00000000..8dd5fa1e
--- /dev/null
+++ b/apps/couch/test/etap/030-doc-from-json.t
@@ -0,0 +1,234 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+%%! -pa ./src/couchdb -pa ./src/mochiweb -sasl errlog_type false -noshell
+
+% 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.
+
+%% XXX: Figure out how to -include("couch_db.hrl")
+-record(doc, {id= <<"">>, revs={0, []}, body={[]},
+ atts=[], deleted=false, meta=[]}).
+-record(att, {name, type, att_len, disk_len, md5= <<>>, revpos=0, data,
+ encoding=identity}).
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(26),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail()
+ end,
+ ok.
+
+test() ->
+ ok = test_from_json_success(),
+ ok = test_from_json_errors(),
+ ok.
+
+test_from_json_success() ->
+ Cases = [
+ {
+ {[]},
+ #doc{},
+ "Return an empty document for an empty JSON object."
+ },
+ {
+ {[{<<"_id">>, <<"zing!">>}]},
+ #doc{id= <<"zing!">>},
+ "Parses document ids."
+ },
+ {
+ {[{<<"_id">>, <<"_design/foo">>}]},
+ #doc{id= <<"_design/foo">>},
+ "_design/document ids."
+ },
+ {
+ {[{<<"_id">>, <<"_local/bam">>}]},
+ #doc{id= <<"_local/bam">>},
+ "_local/document ids."
+ },
+ {
+ {[{<<"_rev">>, <<"4-230234">>}]},
+ #doc{revs={4, [<<"230234">>]}},
+ "_rev stored in revs."
+ },
+ {
+ {[{<<"soap">>, 35}]},
+ #doc{body={[{<<"soap">>, 35}]}},
+ "Non underscore prefixed fields stored in body."
+ },
+ {
+ {[{<<"_attachments">>, {[
+ {<<"my_attachment.fu">>, {[
+ {<<"stub">>, true},
+ {<<"content_type">>, <<"application/awesome">>},
+ {<<"length">>, 45}
+ ]}},
+ {<<"noahs_private_key.gpg">>, {[
+ {<<"data">>, <<"SSBoYXZlIGEgcGV0IGZpc2gh">>},
+ {<<"content_type">>, <<"application/pgp-signature">>}
+ ]}}
+ ]}}]},
+ #doc{atts=[
+ #att{
+ name = <<"my_attachment.fu">>,
+ data = stub,
+ type = <<"application/awesome">>,
+ att_len = 45,
+ disk_len = 45,
+ revpos = nil
+ },
+ #att{
+ name = <<"noahs_private_key.gpg">>,
+ data = <<"I have a pet fish!">>,
+ type = <<"application/pgp-signature">>,
+ att_len = 18,
+ disk_len = 18,
+ revpos = 0
+ }
+ ]},
+ "Attachments are parsed correctly."
+ },
+ {
+ {[{<<"_deleted">>, true}]},
+ #doc{deleted=true},
+ "_deleted controls the deleted field."
+ },
+ {
+ {[{<<"_deleted">>, false}]},
+ #doc{},
+ "{\"_deleted\": false} is ok."
+ },
+ {
+ {[
+ {<<"_revisions">>, {[
+ {<<"start">>, 4},
+ {<<"ids">>, [<<"foo1">>, <<"phi3">>, <<"omega">>]}
+ ]}},
+ {<<"_rev">>, <<"6-something">>}
+ ]},
+ #doc{revs={4, [<<"foo1">>, <<"phi3">>, <<"omega">>]}},
+ "_revisions attribute are preferred to _rev."
+ },
+ {
+ {[{<<"_revs_info">>, dropping}]},
+ #doc{},
+ "Drops _revs_info."
+ },
+ {
+ {[{<<"_local_seq">>, dropping}]},
+ #doc{},
+ "Drops _local_seq."
+ },
+ {
+ {[{<<"_conflicts">>, dropping}]},
+ #doc{},
+ "Drops _conflicts."
+ },
+ {
+ {[{<<"_deleted_conflicts">>, dropping}]},
+ #doc{},
+ "Drops _deleted_conflicts."
+ }
+ ],
+
+ lists:foreach(fun({EJson, Expect, Mesg}) ->
+ etap:is(couch_doc:from_json_obj(EJson), Expect, Mesg)
+ end, Cases),
+ ok.
+
+test_from_json_errors() ->
+ Cases = [
+ {
+ [],
+ {bad_request, "Document must be a JSON object"},
+ "arrays are invalid"
+ },
+ {
+ 4,
+ {bad_request, "Document must be a JSON object"},
+ "integers are invalid"
+ },
+ {
+ true,
+ {bad_request, "Document must be a JSON object"},
+ "literals are invalid"
+ },
+ {
+ {[{<<"_id">>, {[{<<"foo">>, 5}]}}]},
+ {bad_request, <<"Document id must be a string">>},
+ "Document id must be a string."
+ },
+ {
+ {[{<<"_id">>, <<"_random">>}]},
+ {bad_request,
+ <<"Only reserved document ids may start with underscore.">>},
+ "Disallow arbitrary underscore prefixed docids."
+ },
+ {
+ {[{<<"_rev">>, 5}]},
+ {bad_request, <<"Invalid rev format">>},
+ "_rev must be a string"
+ },
+ {
+ {[{<<"_rev">>, "foobar"}]},
+ {bad_request, <<"Invalid rev format">>},
+ "_rev must be %d-%s"
+ },
+ {
+ {[{<<"_rev">>, "foo-bar"}]},
+ "Error if _rev's integer expection is broken."
+ },
+ {
+ {[{<<"_revisions">>, {[{<<"start">>, true}]}}]},
+ {doc_validation, "_revisions.start isn't an integer."},
+ "_revisions.start must be an integer."
+ },
+ {
+ {[{<<"_revisions">>, {[
+ {<<"start">>, 0},
+ {<<"ids">>, 5}
+ ]}}]},
+ {doc_validation, "_revisions.ids isn't a array."},
+ "_revions.ids must be a list."
+ },
+ {
+ {[{<<"_revisions">>, {[
+ {<<"start">>, 0},
+ {<<"ids">>, [5]}
+ ]}}]},
+ {doc_validation, "RevId isn't a string"},
+ "Revision ids must be strings."
+ },
+ {
+ {[{<<"_something">>, 5}]},
+ {doc_validation, <<"Bad special document member: _something">>},
+ "Underscore prefix fields are reserved."
+ }
+ ],
+
+ lists:foreach(fun
+ ({EJson, Expect, Mesg}) ->
+ Error = (catch couch_doc:from_json_obj(EJson)),
+ etap:is(Error, Expect, Mesg);
+ ({EJson, Mesg}) ->
+ try
+ couch_doc:from_json_obj(EJson),
+ etap:ok(false, "Conversion failed to raise an exception.")
+ catch
+ _:_ -> etap:ok(true, Mesg)
+ end
+ end, Cases),
+ ok.
diff --git a/apps/couch/test/etap/031-doc-to-json.t b/apps/couch/test/etap/031-doc-to-json.t
new file mode 100755
index 00000000..6f2ae7a2
--- /dev/null
+++ b/apps/couch/test/etap/031-doc-to-json.t
@@ -0,0 +1,194 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+%%! -pa ./src/couchdb -pa ./src/mochiweb -sasl errlog_type false -noshell
+
+% 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.
+
+%% XXX: Figure out how to -include("couch_db.hrl")
+-record(doc, {id= <<"">>, revs={0, []}, body={[]},
+ atts=[], deleted=false, meta=[]}).
+-record(att, {name, type, att_len, disk_len, md5= <<>>, revpos=0, data,
+ encoding=identity}).
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(12),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail()
+ end,
+ ok.
+
+test() ->
+ ok = test_to_json_success(),
+ ok.
+
+test_to_json_success() ->
+ Cases = [
+ {
+ #doc{},
+ {[{<<"_id">>, <<"">>}]},
+ "Empty docs are {\"_id\": \"\"}"
+ },
+ {
+ #doc{id= <<"foo">>},
+ {[{<<"_id">>, <<"foo">>}]},
+ "_id is added."
+ },
+ {
+ #doc{revs={5, ["foo"]}},
+ {[{<<"_id">>, <<>>}, {<<"_rev">>, <<"5-foo">>}]},
+ "_rev is added."
+ },
+ {
+ [revs],
+ #doc{revs={5, [<<"first">>, <<"second">>]}},
+ {[
+ {<<"_id">>, <<>>},
+ {<<"_rev">>, <<"5-first">>},
+ {<<"_revisions">>, {[
+ {<<"start">>, 5},
+ {<<"ids">>, [<<"first">>, <<"second">>]}
+ ]}}
+ ]},
+ "_revisions include with revs option"
+ },
+ {
+ #doc{body={[{<<"foo">>, <<"bar">>}]}},
+ {[{<<"_id">>, <<>>}, {<<"foo">>, <<"bar">>}]},
+ "Arbitrary fields are added."
+ },
+ {
+ #doc{deleted=true, body={[{<<"foo">>, <<"bar">>}]}},
+ {[{<<"_id">>, <<>>}, {<<"foo">>, <<"bar">>}, {<<"_deleted">>, true}]},
+ "Deleted docs no longer drop body members."
+ },
+ {
+ #doc{meta=[
+ {revs_info, 4, [{<<"fin">>, deleted}, {<<"zim">>, missing}]}
+ ]},
+ {[
+ {<<"_id">>, <<>>},
+ {<<"_revs_info">>, [
+ {[{<<"rev">>, <<"4-fin">>}, {<<"status">>, <<"deleted">>}]},
+ {[{<<"rev">>, <<"3-zim">>}, {<<"status">>, <<"missing">>}]}
+ ]}
+ ]},
+ "_revs_info field is added correctly."
+ },
+ {
+ #doc{meta=[{local_seq, 5}]},
+ {[{<<"_id">>, <<>>}, {<<"_local_seq">>, 5}]},
+ "_local_seq is added as an integer."
+ },
+ {
+ #doc{meta=[{conflicts, [{3, <<"yep">>}, {1, <<"snow">>}]}]},
+ {[
+ {<<"_id">>, <<>>},
+ {<<"_conflicts">>, [<<"3-yep">>, <<"1-snow">>]}
+ ]},
+ "_conflicts is added as an array of strings."
+ },
+ {
+ #doc{meta=[{deleted_conflicts, [{10923, <<"big_cowboy_hat">>}]}]},
+ {[
+ {<<"_id">>, <<>>},
+ {<<"_deleted_conflicts">>, [<<"10923-big_cowboy_hat">>]}
+ ]},
+ "_deleted_conflicsts is added as an array of strings."
+ },
+ {
+ #doc{atts=[
+ #att{
+ name = <<"big.xml">>,
+ type = <<"xml/sucks">>,
+ data = fun() -> ok end,
+ revpos = 1,
+ att_len = 400,
+ disk_len = 400
+ },
+ #att{
+ name = <<"fast.json">>,
+ type = <<"json/ftw">>,
+ data = <<"{\"so\": \"there!\"}">>,
+ revpos = 1,
+ att_len = 16,
+ disk_len = 16
+ }
+ ]},
+ {[
+ {<<"_id">>, <<>>},
+ {<<"_attachments">>, {[
+ {<<"big.xml">>, {[
+ {<<"content_type">>, <<"xml/sucks">>},
+ {<<"revpos">>, 1},
+ {<<"length">>, 400},
+ {<<"stub">>, true}
+ ]}},
+ {<<"fast.json">>, {[
+ {<<"content_type">>, <<"json/ftw">>},
+ {<<"revpos">>, 1},
+ {<<"length">>, 16},
+ {<<"stub">>, true}
+ ]}}
+ ]}}
+ ]},
+ "Attachments attached as stubs only include a length."
+ },
+ {
+ [attachments],
+ #doc{atts=[
+ #att{
+ name = <<"stuff.txt">>,
+ type = <<"text/plain">>,
+ data = fun() -> <<"diet pepsi">> end,
+ revpos = 1,
+ att_len = 10,
+ disk_len = 10
+ },
+ #att{
+ name = <<"food.now">>,
+ type = <<"application/food">>,
+ revpos = 1,
+ data = <<"sammich">>
+ }
+ ]},
+ {[
+ {<<"_id">>, <<>>},
+ {<<"_attachments">>, {[
+ {<<"stuff.txt">>, {[
+ {<<"content_type">>, <<"text/plain">>},
+ {<<"revpos">>, 1},
+ {<<"data">>, <<"ZGlldCBwZXBzaQ==">>}
+ ]}},
+ {<<"food.now">>, {[
+ {<<"content_type">>, <<"application/food">>},
+ {<<"revpos">>, 1},
+ {<<"data">>, <<"c2FtbWljaA==">>}
+ ]}}
+ ]}}
+ ]},
+ "Attachments included inline with attachments option."
+ }
+ ],
+
+ lists:foreach(fun
+ ({Doc, EJson, Mesg}) ->
+ etap:is(couch_doc:to_json_obj(Doc, []), EJson, Mesg);
+ ({Options, Doc, EJson, Mesg}) ->
+ etap:is(couch_doc:to_json_obj(Doc, Options), EJson, Mesg)
+ end, Cases),
+ ok.
diff --git a/apps/couch/test/etap/040-util.t b/apps/couch/test/etap/040-util.t
new file mode 100755
index 00000000..8f80db87
--- /dev/null
+++ b/apps/couch/test/etap/040-util.t
@@ -0,0 +1,80 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ application:start(crypto),
+
+ etap:plan(14),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ % to_existing_atom
+ etap:is(true, couch_util:to_existing_atom(true), "An atom is an atom."),
+ etap:is(foo, couch_util:to_existing_atom(<<"foo">>),
+ "A binary foo is the atom foo."),
+ etap:is(foobarbaz, couch_util:to_existing_atom("foobarbaz"),
+ "A list of atoms is one munged atom."),
+
+ % implode
+ etap:is([1, 38, 2, 38, 3], couch_util:implode([1,2,3],"&"),
+ "use & as separator in list."),
+
+ % trim
+ Strings = [" foo", "foo ", "\tfoo", " foo ", "foo\t", "foo\n", "\nfoo"],
+ etap:ok(lists:all(fun(S) -> couch_util:trim(S) == "foo" end, Strings),
+ "everything here trimmed should be foo."),
+
+ % abs_pathname
+ {ok, Cwd} = file:get_cwd(),
+ etap:is(Cwd ++ "/foo", couch_util:abs_pathname("./foo"),
+ "foo is in this directory."),
+
+ % should_flush
+ etap:ok(not couch_util:should_flush(),
+ "Not using enough memory to flush."),
+ AcquireMem = fun() ->
+ IntsToAGazillion = lists:seq(1, 200000),
+ LotsOfData = lists:map(
+ fun(Int) -> {Int, <<"foobar">>} end,
+ lists:seq(1, 500000)),
+ etap:ok(couch_util:should_flush(),
+ "Allocation 200K tuples puts us above the memory threshold.")
+ end,
+ AcquireMem(),
+
+ etap:ok(not couch_util:should_flush(),
+ "Checking to flush invokes GC."),
+
+ % verify
+ etap:is(true, couch_util:verify("It4Vooya", "It4Vooya"),
+ "String comparison."),
+ etap:is(false, couch_util:verify("It4VooyaX", "It4Vooya"),
+ "String comparison (unequal lengths)."),
+ etap:is(true, couch_util:verify(<<"ahBase3r">>, <<"ahBase3r">>),
+ "Binary comparison."),
+ etap:is(false, couch_util:verify(<<"ahBase3rX">>, <<"ahBase3r">>),
+ "Binary comparison (unequal lengths)."),
+ etap:is(false, couch_util:verify(nil, <<"ahBase3r">>),
+ "Binary comparison with atom."),
+
+ ok.
diff --git a/apps/couch/test/etap/041-uuid-gen-seq.ini b/apps/couch/test/etap/041-uuid-gen-seq.ini
new file mode 100644
index 00000000..94cebc6f
--- /dev/null
+++ b/apps/couch/test/etap/041-uuid-gen-seq.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.
+
+[uuids]
+algorithm = sequential
diff --git a/apps/couch/test/etap/041-uuid-gen-utc.ini b/apps/couch/test/etap/041-uuid-gen-utc.ini
new file mode 100644
index 00000000..c2b83831
--- /dev/null
+++ b/apps/couch/test/etap/041-uuid-gen-utc.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.
+
+[uuids]
+algorithm = utc_random
diff --git a/apps/couch/test/etap/041-uuid-gen.t b/apps/couch/test/etap/041-uuid-gen.t
new file mode 100755
index 00000000..84c93a44
--- /dev/null
+++ b/apps/couch/test/etap/041-uuid-gen.t
@@ -0,0 +1,116 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+seq_alg_config() ->
+ filename:join([code:lib_dir(couch, test), "etap/041-uuid-gen-seq.ini"]).
+
+utc_alg_config() ->
+ filename:join([code:lib_dir(couch, test), "etap/041-uuid-gen-utc.ini"]).
+
+% Run tests and wait for the gen_servers to shutdown
+run_test(IniFiles, Test) ->
+ couch_config_event:start_link(),
+ {ok, Pid} = couch_config:start_link(IniFiles),
+ erlang:monitor(process, Pid),
+ couch_uuids:start(),
+ Test(),
+ couch_uuids:stop(),
+ couch_config:stop(),
+ receive
+ {'DOWN', _, _, Pid, _} -> ok;
+ _Other -> etap:diag("OTHER: ~p~n", [_Other])
+ after
+ 1000 -> throw({timeout_error, config_stop})
+ end.
+
+main(_) ->
+ test_util:init_code_path(),
+ application:start(crypto),
+ etap:plan(5),
+
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+
+ TestUnique = fun() ->
+ etap:is(
+ test_unique(10000, couch_uuids:new()),
+ true,
+ "Can generate 10K unique IDs"
+ )
+ end,
+ %run_test([default_config()], TestUnique),
+ run_test([seq_alg_config()], TestUnique),
+ run_test([utc_alg_config()], TestUnique),
+
+ TestMonotonic = fun () ->
+ etap:is(
+ couch_uuids:new() < couch_uuids:new(),
+ true,
+ "should produce monotonically increasing ids"
+ )
+ end,
+ run_test([seq_alg_config()], TestMonotonic),
+ run_test([utc_alg_config()], TestMonotonic),
+
+ % Pretty sure that the average of a uniform distribution is the
+ % midpoint of the range. Thus, to exceed a threshold, we need
+ % approximately Total / (Range/2 + RangeMin) samples.
+ %
+ % In our case this works out to be 8194. (0xFFF000 / 0x7FF)
+ % These tests just fudge the limits for a good generator at 25%
+ % in either direction. Technically it should be possible to generate
+ % bounds that will show if your random number generator is not
+ % sufficiently random but I hated statistics in school.
+ TestRollOver = fun() ->
+ UUID = binary_to_list(couch_uuids:new()),
+ Prefix = element(1, lists:split(26, UUID)),
+ N = gen_until_pref_change(Prefix,0),
+ etap:diag("N is: ~p~n",[N]),
+ etap:is(
+ N >= 5000 andalso N =< 11000,
+ true,
+ "should roll over every so often."
+ )
+ end,
+ run_test([seq_alg_config()], TestRollOver).
+
+test_unique(0, _) ->
+ true;
+test_unique(N, UUID) ->
+ case couch_uuids:new() of
+ UUID ->
+ etap:diag("N: ~p~n", [N]),
+ false;
+ Else -> test_unique(N-1, Else)
+ end.
+
+get_prefix(UUID) ->
+ element(1, lists:split(26, binary_to_list(UUID))).
+
+gen_until_pref_change(_, Count) when Count > 8251 ->
+ Count;
+gen_until_pref_change(Prefix, N) ->
+ case get_prefix(couch_uuids:new()) of
+ Prefix -> gen_until_pref_change(Prefix, N+1);
+ _ -> N
+ end.
diff --git a/apps/couch/test/etap/050-stream.t b/apps/couch/test/etap/050-stream.t
new file mode 100755
index 00000000..03949690
--- /dev/null
+++ b/apps/couch/test/etap/050-stream.t
@@ -0,0 +1,88 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(13),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+read_all(Fd, PosList) ->
+ Data = couch_stream:foldl(Fd, PosList, fun(Bin, Acc) -> [Bin, Acc] end, []),
+ iolist_to_binary(Data).
+
+test() ->
+ {ok, Fd} = couch_file:open("apps/couch/test/etap/temp.050",
+ [create,overwrite]),
+ {ok, Stream} = couch_stream:open(Fd),
+
+ etap:is(ok, couch_stream:write(Stream, <<"food">>),
+ "Writing to streams works."),
+
+ etap:is(ok, couch_stream:write(Stream, <<"foob">>),
+ "Consecutive writing to streams works."),
+
+ etap:is(ok, couch_stream:write(Stream, <<>>),
+ "Writing an empty binary does nothing."),
+
+ {Ptrs, Length, _, _, _} = couch_stream:close(Stream),
+ etap:is(Ptrs, [0], "Close returns the file pointers."),
+ etap:is(Length, 8, "Close also returns the number of bytes written."),
+ etap:is(<<"foodfoob">>, read_all(Fd, Ptrs), "Returned pointers are valid."),
+
+ % Remeber where we expect the pointer to be.
+ {ok, ExpPtr} = couch_file:bytes(Fd),
+ {ok, Stream2} = couch_stream:open(Fd),
+ OneBits = <<1:(8*10)>>,
+ etap:is(ok, couch_stream:write(Stream2, OneBits),
+ "Successfully wrote 80 1 bits."),
+
+ ZeroBits = <<0:(8*10)>>,
+ etap:is(ok, couch_stream:write(Stream2, ZeroBits),
+ "Successfully wrote 80 0 bits."),
+
+ {Ptrs2, Length2, _, _, _} = couch_stream:close(Stream2),
+ etap:is(Ptrs2, [ExpPtr], "Closing stream returns the file pointers."),
+ etap:is(Length2, 20, "Length written is 160 bytes."),
+
+ AllBits = iolist_to_binary([OneBits,ZeroBits]),
+ etap:is(AllBits, read_all(Fd, Ptrs2), "Returned pointers are valid."),
+
+ % Stream more the 4K chunk size.
+ {ok, ExpPtr2} = couch_file:bytes(Fd),
+ {ok, Stream3} = couch_stream:open(Fd),
+ Acc2 = lists:foldl(fun(_, Acc) ->
+ Data = <<"a1b2c">>,
+ couch_stream:write(Stream3, Data),
+ [Data | Acc]
+ end, [], lists:seq(1, 1024)),
+ {Ptrs3, Length3, _, _, _} = couch_stream:close(Stream3),
+
+ % 4095 because of 5 * 4096 rem 5 (last write before exceeding threshold)
+ % + 5 puts us over the threshold
+ % + 4 bytes for the term_to_binary adding a length header
+ % + 1 byte every 4K for tail append headers
+ SecondPtr = ExpPtr2 + 4095 + 5 + 4 + 1,
+ etap:is(Ptrs3, [ExpPtr2, SecondPtr], "Pointers every 4K bytes."),
+ etap:is(Length3, 5120, "Wrote the expected 5K bytes."),
+
+ couch_file:close(Fd),
+ ok.
diff --git a/apps/couch/test/etap/060-kt-merging.t b/apps/couch/test/etap/060-kt-merging.t
new file mode 100755
index 00000000..0e481a52
--- /dev/null
+++ b/apps/couch/test/etap/060-kt-merging.t
@@ -0,0 +1,115 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(12),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ One = {0, {"1","foo",[]}},
+ TwoSibs = [{0, {"1","foo",[]}},
+ {0, {"2","foo",[]}}],
+ OneChild = {0, {"1","foo",[{"1a", "bar", []}]}},
+ TwoChild = {0, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}},
+ TwoChildSibs = {0, {"1","foo", [{"1a", "bar", []},
+ {"1b", "bar", []}]}},
+ TwoChildSibs2 = {0, {"1","foo", [{"1a", "bar", []},
+ {"1b", "bar", [{"1bb", "boo", []}]}]}},
+ Stemmed1b = {1, {"1a", "bar", []}},
+ Stemmed1a = {1, {"1a", "bar", [{"1aa", "bar", []}]}},
+ Stemmed1aa = {2, {"1aa", "bar", []}},
+ Stemmed1bb = {2, {"1bb", "boo", []}},
+
+ etap:is(
+ {[One], no_conflicts},
+ couch_key_tree:merge([], One, 10),
+ "The empty tree is the identity for merge."
+ ),
+
+ etap:is(
+ {TwoSibs, no_conflicts},
+ couch_key_tree:merge(TwoSibs, One, 10),
+ "Merging a prefix of a tree with the tree yields the tree."
+ ),
+
+ etap:is(
+ {[One], no_conflicts},
+ couch_key_tree:merge([One], One, 10),
+ "Merging is reflexive."
+ ),
+
+ etap:is(
+ {[TwoChild], no_conflicts},
+ couch_key_tree:merge([TwoChild], TwoChild, 10),
+ "Merging two children is still reflexive."
+ ),
+
+ etap:is(
+ {[TwoChildSibs], no_conflicts},
+ couch_key_tree:merge([TwoChildSibs], TwoChildSibs, 10),
+ "Merging a tree to itself is itself."),
+
+ etap:is(
+ {[TwoChildSibs], no_conflicts},
+ couch_key_tree:merge([TwoChildSibs], Stemmed1b, 10),
+ "Merging a tree with a stem."
+ ),
+
+ etap:is(
+ {[TwoChildSibs2], no_conflicts},
+ couch_key_tree:merge([TwoChildSibs2], Stemmed1bb, 10),
+ "Merging a stem at a deeper level."
+ ),
+
+ etap:is(
+ {[TwoChild], no_conflicts},
+ couch_key_tree:merge([TwoChild], Stemmed1aa, 10),
+ "Merging a single tree with a deeper stem."
+ ),
+
+ etap:is(
+ {[TwoChild], no_conflicts},
+ couch_key_tree:merge([TwoChild], Stemmed1a, 10),
+ "Merging a larger stem."
+ ),
+
+ etap:is(
+ {[Stemmed1a], no_conflicts},
+ couch_key_tree:merge([Stemmed1a], Stemmed1aa, 10),
+ "More merging."
+ ),
+
+ Expect1 = [OneChild, Stemmed1aa],
+ etap:is(
+ {Expect1, conflicts},
+ couch_key_tree:merge([OneChild], Stemmed1aa, 10),
+ "Merging should create conflicts."
+ ),
+
+ etap:is(
+ {[TwoChild], no_conflicts},
+ couch_key_tree:merge(Expect1, TwoChild, 10),
+ "Merge should have no conflicts."
+ ),
+
+ ok.
diff --git a/apps/couch/test/etap/061-kt-missing-leaves.t b/apps/couch/test/etap/061-kt-missing-leaves.t
new file mode 100755
index 00000000..d60b4db8
--- /dev/null
+++ b/apps/couch/test/etap/061-kt-missing-leaves.t
@@ -0,0 +1,65 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(4),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}],
+ Stemmed1 = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}],
+ Stemmed2 = [{2, {"1aa", "bar", []}}],
+
+ etap:is(
+ [],
+ couch_key_tree:find_missing(TwoChildSibs, [{0,"1"}, {1,"1a"}]),
+ "Look for missing keys."
+ ),
+
+ etap:is(
+ [{0, "10"}, {100, "x"}],
+ couch_key_tree:find_missing(
+ TwoChildSibs,
+ [{0,"1"}, {0, "10"}, {1,"1a"}, {100, "x"}]
+ ),
+ "Look for missing keys."
+ ),
+
+ etap:is(
+ [{0, "1"}, {100, "x"}],
+ couch_key_tree:find_missing(
+ Stemmed1,
+ [{0,"1"}, {1,"1a"}, {100, "x"}]
+ ),
+ "Look for missing keys."
+ ),
+ etap:is(
+ [{0, "1"}, {1,"1a"}, {100, "x"}],
+ couch_key_tree:find_missing(
+ Stemmed2,
+ [{0,"1"}, {1,"1a"}, {100, "x"}]
+ ),
+ "Look for missing keys."
+ ),
+
+ ok.
diff --git a/apps/couch/test/etap/062-kt-remove-leaves.t b/apps/couch/test/etap/062-kt-remove-leaves.t
new file mode 100755
index 00000000..745a00be
--- /dev/null
+++ b/apps/couch/test/etap/062-kt-remove-leaves.t
@@ -0,0 +1,69 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(6),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ OneChild = [{0, {"1","foo",[{"1a", "bar", []}]}}],
+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}],
+ Stemmed = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}],
+
+ etap:is(
+ {TwoChildSibs, []},
+ couch_key_tree:remove_leafs(TwoChildSibs, []),
+ "Removing no leaves has no effect on the tree."
+ ),
+
+ etap:is(
+ {TwoChildSibs, []},
+ couch_key_tree:remove_leafs(TwoChildSibs, [{0, "1"}]),
+ "Removing a non-existant branch has no effect."
+ ),
+
+ etap:is(
+ {OneChild, [{1, "1b"}]},
+ couch_key_tree:remove_leafs(TwoChildSibs, [{1, "1b"}]),
+ "Removing a leaf removes the leaf."
+ ),
+
+ etap:is(
+ {[], [{1, "1b"},{1, "1a"}]},
+ couch_key_tree:remove_leafs(TwoChildSibs, [{1, "1a"}, {1, "1b"}]),
+ "Removing all leaves returns an empty tree."
+ ),
+
+ etap:is(
+ {Stemmed, []},
+ couch_key_tree:remove_leafs(Stemmed, [{1, "1a"}]),
+ "Removing a non-existant node has no effect."
+ ),
+
+ etap:is(
+ {[], [{2, "1aa"}]},
+ couch_key_tree:remove_leafs(Stemmed, [{2, "1aa"}]),
+ "Removing the last leaf returns an empty tree."
+ ),
+
+ ok.
diff --git a/apps/couch/test/etap/063-kt-get-leaves.t b/apps/couch/test/etap/063-kt-get-leaves.t
new file mode 100755
index 00000000..6d4e8007
--- /dev/null
+++ b/apps/couch/test/etap/063-kt-get-leaves.t
@@ -0,0 +1,98 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(11),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}],
+ Stemmed = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}],
+
+ etap:is(
+ {[{"foo", {0, ["1"]}}],[]},
+ couch_key_tree:get(TwoChildSibs, [{0, "1"}]),
+ "extract a subtree."
+ ),
+
+ etap:is(
+ {[{"bar", {1, ["1a", "1"]}}],[]},
+ couch_key_tree:get(TwoChildSibs, [{1, "1a"}]),
+ "extract a subtree."
+ ),
+
+ etap:is(
+ {[],[{0,"x"}]},
+ couch_key_tree:get_key_leafs(TwoChildSibs, [{0, "x"}]),
+ "gather up the leaves."
+ ),
+
+ etap:is(
+ {[{"bar", {1, ["1a","1"]}}],[]},
+ couch_key_tree:get_key_leafs(TwoChildSibs, [{1, "1a"}]),
+ "gather up the leaves."
+ ),
+
+ etap:is(
+ {[{"bar", {1, ["1a","1"]}},{"bar",{1, ["1b","1"]}}],[]},
+ couch_key_tree:get_key_leafs(TwoChildSibs, [{0, "1"}]),
+ "gather up the leaves."
+ ),
+
+ etap:is(
+ {[{0,[{"1", "foo"}]}],[]},
+ couch_key_tree:get_full_key_paths(TwoChildSibs, [{0, "1"}]),
+ "retrieve full key paths."
+ ),
+
+ etap:is(
+ {[{1,[{"1a", "bar"},{"1", "foo"}]}],[]},
+ couch_key_tree:get_full_key_paths(TwoChildSibs, [{1, "1a"}]),
+ "retrieve full key paths."
+ ),
+
+ etap:is(
+ [{2, [{"1aa", "bar"},{"1a", "bar"}]}],
+ couch_key_tree:get_all_leafs_full(Stemmed),
+ "retrieve all leaves."
+ ),
+
+ etap:is(
+ [{1, [{"1a", "bar"},{"1", "foo"}]}, {1, [{"1b", "bar"},{"1", "foo"}]}],
+ couch_key_tree:get_all_leafs_full(TwoChildSibs),
+ "retrieve all the leaves."
+ ),
+
+ etap:is(
+ [{"bar", {2, ["1aa","1a"]}}],
+ couch_key_tree:get_all_leafs(Stemmed),
+ "retrieve all leaves."
+ ),
+
+ etap:is(
+ [{"bar", {1, ["1a", "1"]}}, {"bar", {1, ["1b","1"]}}],
+ couch_key_tree:get_all_leafs(TwoChildSibs),
+ "retrieve all the leaves."
+ ),
+
+ ok.
diff --git a/apps/couch/test/etap/064-kt-counting.t b/apps/couch/test/etap/064-kt-counting.t
new file mode 100755
index 00000000..f182d287
--- /dev/null
+++ b/apps/couch/test/etap/064-kt-counting.t
@@ -0,0 +1,46 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(4),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ EmptyTree = [],
+ One = [{0, {"1","foo",[]}}],
+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}],
+ Stemmed = [{2, {"1bb", "boo", []}}],
+
+ etap:is(0, couch_key_tree:count_leafs(EmptyTree),
+ "Empty trees have no leaves."),
+
+ etap:is(1, couch_key_tree:count_leafs(One),
+ "Single node trees have a single leaf."),
+
+ etap:is(2, couch_key_tree:count_leafs(TwoChildSibs),
+ "Two children siblings counted as two leaves."),
+
+ etap:is(1, couch_key_tree:count_leafs(Stemmed),
+ "Stemming does not affect leaf counting."),
+
+ ok.
diff --git a/apps/couch/test/etap/065-kt-stemming.t b/apps/couch/test/etap/065-kt-stemming.t
new file mode 100755
index 00000000..6e781c1d
--- /dev/null
+++ b/apps/couch/test/etap/065-kt-stemming.t
@@ -0,0 +1,42 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(3),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ TwoChild = [{0, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}],
+ Stemmed1 = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}],
+ Stemmed2 = [{2, {"1aa", "bar", []}}],
+
+ etap:is(TwoChild, couch_key_tree:stem(TwoChild, 3),
+ "Stemming more levels than what exists does nothing."),
+
+ etap:is(Stemmed1, couch_key_tree:stem(TwoChild, 2),
+ "Stemming with a depth of two returns the deepest two nodes."),
+
+ etap:is(Stemmed2, couch_key_tree:stem(TwoChild, 1),
+ "Stemming to a depth of one returns the deepest node."),
+
+ ok.
diff --git a/apps/couch/test/etap/080-config-get-set.ini b/apps/couch/test/etap/080-config-get-set.ini
new file mode 100644
index 00000000..e72e08db
--- /dev/null
+++ b/apps/couch/test/etap/080-config-get-set.ini
@@ -0,0 +1,8 @@
+[daemons]
+something=somevalue
+
+[httpd_design_handlers]
+_view = {couch_httpd_view, handle_view_req}
+
+[httpd]
+port = 5984
diff --git a/apps/couch/test/etap/080-config-get-set.t b/apps/couch/test/etap/080-config-get-set.t
new file mode 100755
index 00000000..c457c7f0
--- /dev/null
+++ b/apps/couch/test/etap/080-config-get-set.t
@@ -0,0 +1,129 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+default_config() ->
+ filename:join([code:lib_dir(couch, test), "etap/080-config-get-set.ini"]).
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(12),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ % start couch_config with default
+ couch_config_event:start_link(),
+ couch_config:start_link([default_config()]),
+
+
+ % Check that we can get values
+
+
+ etap:fun_is(
+ fun(List) -> length(List) > 0 end,
+ couch_config:all(),
+ "Data was loaded from the INI file."
+ ),
+
+ etap:fun_is(
+ fun(List) -> length(List) > 0 end,
+ couch_config:get("daemons"),
+ "There are settings in the [daemons] section of the INI file."
+ ),
+
+ etap:is(
+ couch_config:get("httpd_design_handlers", "_view"),
+ "{couch_httpd_view, handle_view_req}",
+ "The {httpd_design_handlers, view} is the expected default."
+ ),
+
+ etap:is(
+ couch_config:get("httpd", "foo", "bar"),
+ "bar",
+ "Returns the default when key doesn't exist in config."
+ ),
+
+ etap:is(
+ couch_config:get("httpd", "foo"),
+ undefined,
+ "The default default is the atom 'undefined'."
+ ),
+
+ etap:is(
+ couch_config:get("httpd", "port", "bar"),
+ "5984",
+ "Only returns the default when the config setting does not exist."
+ ),
+
+
+ % Check that setting values works.
+
+
+ ok = couch_config:set("log", "level", "severe", false),
+
+ etap:is(
+ couch_config:get("log", "level"),
+ "severe",
+ "Non persisted changes take effect."
+ ),
+
+ etap:is(
+ couch_config:get("new_section", "bizzle"),
+ undefined,
+ "Section 'new_section' does not exist."
+ ),
+
+ ok = couch_config:set("new_section", "bizzle", "bang", false),
+
+ etap:is(
+ couch_config:get("new_section", "bizzle"),
+ "bang",
+ "New section 'new_section' was created for a new key/value pair."
+ ),
+
+
+ % Check that deleting works
+
+
+ ok = couch_config:delete("new_section", "bizzle", false),
+ etap:is(
+ couch_config:get("new_section", "bizzle"),
+ undefined,
+ "Deleting sets the value to \"\""
+ ),
+
+
+ % Check ge/set/delete binary strings
+
+ ok = couch_config:set(<<"foo">>, <<"bar">>, <<"baz">>, false),
+ etap:is(
+ couch_config:get(<<"foo">>, <<"bar">>),
+ <<"baz">>,
+ "Can get and set with binary section and key values."
+ ),
+ ok = couch_config:delete(<<"foo">>, <<"bar">>, false),
+ etap:is(
+ couch_config:get(<<"foo">>, <<"bar">>),
+ undefined,
+ "Deleting with binary section/key pairs sets the value to \"\""
+ ),
+
+ ok.
diff --git a/apps/couch/test/etap/081-config-default.ini b/apps/couch/test/etap/081-config-default.ini
new file mode 100644
index 00000000..abdf79bc
--- /dev/null
+++ b/apps/couch/test/etap/081-config-default.ini
@@ -0,0 +1,6 @@
+[couchdb]
+max_dbs_open=100
+
+[httpd]
+port=5984
+bind_address = 127.0.0.1
diff --git a/apps/couch/test/etap/081-config-override.1.ini b/apps/couch/test/etap/081-config-override.1.ini
new file mode 100644
index 00000000..55451dad
--- /dev/null
+++ b/apps/couch/test/etap/081-config-override.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/apps/couch/test/etap/081-config-override.2.ini b/apps/couch/test/etap/081-config-override.2.ini
new file mode 100644
index 00000000..5f46357f
--- /dev/null
+++ b/apps/couch/test/etap/081-config-override.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/apps/couch/test/etap/081-config-override.t b/apps/couch/test/etap/081-config-override.t
new file mode 100755
index 00000000..16a6b6f6
--- /dev/null
+++ b/apps/couch/test/etap/081-config-override.t
@@ -0,0 +1,213 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+default_config() ->
+ filename:join([code:lib_dir(couch, test),"etap/081-config-default.ini"]).
+
+local_config_1() ->
+ filename:join([code:lib_dir(couch, test),"etap/081-config-override.1.ini"]).
+
+local_config_2() ->
+ filename:join([code:lib_dir(couch, test),"etap/081-config-override.2.ini"]).
+
+local_config_write() ->
+ test_util:build_file("test/etap/temp.081").
+
+% Run tests and wait for the config gen_server to shutdown.
+run_tests(IniFiles, Tests) ->
+ couch_config_event:start_link(),
+ {ok, Pid} = couch_config:start_link(IniFiles),
+ erlang:monitor(process, Pid),
+ Tests(),
+ couch_config:stop(),
+ receive
+ {'DOWN', _, _, Pid, _} -> ok;
+ _Other -> etap:diag("OTHER: ~p~n", [_Other])
+ after
+ 1000 -> throw({timeout_error, config_stop})
+ end.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(17),
+
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+
+ CheckStartStop = fun() -> ok end,
+ run_tests([default_config()], CheckStartStop),
+
+ CheckDefaults = fun() ->
+ etap:is(
+ couch_config:get("couchdb", "max_dbs_open"),
+ "100",
+ "{couchdb, max_dbs_open} is 100 by defualt."
+ ),
+
+ etap:is(
+ couch_config:get("httpd","port"),
+ "5984",
+ "{httpd, port} is 5984 by default"
+ ),
+
+ etap:is(
+ couch_config:get("fizbang", "unicode"),
+ undefined,
+ "{fizbang, unicode} is undefined by default"
+ )
+ end,
+
+ run_tests([default_config()], CheckDefaults),
+
+
+ % Check that subsequent files override values appropriately
+
+ CheckOverride = fun() ->
+ etap:is(
+ couch_config:get("couchdb", "max_dbs_open"),
+ "10",
+ "{couchdb, max_dbs_open} was overriden with the value 10"
+ ),
+
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "4895",
+ "{httpd, port} was overriden with the value 4895"
+ )
+ end,
+
+ run_tests([default_config(), local_config_1()], CheckOverride),
+
+
+ % Check that overrides can create new sections
+
+ CheckOverride2 = fun() ->
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "80",
+ "{httpd, port} is overriden with the value 80"
+ ),
+
+ etap:is(
+ couch_config:get("fizbang", "unicode"),
+ "normalized",
+ "{fizbang, unicode} was created by override INI file"
+ )
+ end,
+
+ run_tests([default_config(), local_config_2()], CheckOverride2),
+
+
+ % Check that values can be overriden multiple times
+
+ CheckOverride3 = fun() ->
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "80",
+ "{httpd, port} value was taken from the last specified INI file."
+ )
+ end,
+
+ run_tests(
+ [default_config(), local_config_1(), local_config_2()],
+ CheckOverride3
+ ),
+
+ % Check persistence to last file.
+
+ % Empty the file in case it exists.
+ {ok, Fd} = file:open(local_config_write(), write),
+ ok = file:truncate(Fd),
+ ok = file:close(Fd),
+
+ % Open and write a value
+ CheckCanWrite = fun() ->
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "5984",
+ "{httpd, port} is still 5984 by default"
+ ),
+
+ etap:is(
+ couch_config:set("httpd", "port", "8080"),
+ ok,
+ "Writing {httpd, port} is kosher."
+ ),
+
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "8080",
+ "{httpd, port} was updated to 8080 successfully."
+ ),
+
+ etap:is(
+ couch_config:delete("httpd", "bind_address"),
+ ok,
+ "Deleting {httpd, bind_address} succeeds"
+ ),
+
+ etap:is(
+ couch_config:get("httpd", "bind_address"),
+ undefined,
+ "{httpd, bind_address} was actually deleted."
+ )
+ end,
+
+ run_tests([default_config(), local_config_write()], CheckCanWrite),
+
+ % Open and check where we don't expect persistence.
+
+ CheckDidntWrite = fun() ->
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "5984",
+ "{httpd, port} was not persisted to the primary INI file."
+ ),
+
+ etap:is(
+ couch_config:get("httpd", "bind_address"),
+ "127.0.0.1",
+ "{httpd, bind_address} was not deleted form the primary INI file."
+ )
+ end,
+
+ run_tests([default_config()], CheckDidntWrite),
+
+ % Open and check we have only the persistence we expect.
+ CheckDidWrite = fun() ->
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "8080",
+ "{httpd, port} is still 8080 after reopening the config."
+ ),
+
+ etap:is(
+ couch_config:get("httpd", "bind_address"),
+ undefined,
+ "{httpd, bind_address} is still \"\" after reopening."
+ )
+ end,
+
+ run_tests([local_config_write()], CheckDidWrite),
+
+ ok.
diff --git a/apps/couch/test/etap/082-config-default.ini b/apps/couch/test/etap/082-config-default.ini
new file mode 100644
index 00000000..e46f158f
--- /dev/null
+++ b/apps/couch/test/etap/082-config-default.ini
@@ -0,0 +1,6 @@
+[couchdb]
+max_dbs_open=500
+
+[httpd]
+port=5984
+bind_address = 127.0.0.1
diff --git a/apps/couch/test/etap/082-config-register.t b/apps/couch/test/etap/082-config-register.t
new file mode 100755
index 00000000..36b48cf8
--- /dev/null
+++ b/apps/couch/test/etap/082-config-register.t
@@ -0,0 +1,95 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+default_config() ->
+ filename:join([code:lib_dir(couch, test), "etap/082-config-default.ini"]).
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(5),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ couch_config_event:start_link(),
+ couch_config:start_link([default_config()]),
+
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "5984",
+ "{httpd, port} is 5984 by default."
+ ),
+
+ ok = couch_config:set("httpd", "port", "4895", false),
+
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "4895",
+ "{httpd, port} changed to 4895"
+ ),
+
+ SentinelFunc = fun() ->
+ % Ping/Pong to make sure we wait for this
+ % process to die
+ receive {ping, From} -> From ! pong end
+ end,
+ SentinelPid = spawn(SentinelFunc),
+
+ couch_config:register(
+ fun("httpd", "port", Value) ->
+ etap:is(Value, "8080", "Registered function got notification.")
+ end,
+ SentinelPid
+ ),
+
+ ok = couch_config:set("httpd", "port", "8080", false),
+
+ % Implicitly checking that we *don't* call the function
+ etap:is(
+ couch_config:get("httpd", "bind_address"),
+ "127.0.0.1",
+ "{httpd, bind_address} is not '0.0.0.0'"
+ ),
+ ok = couch_config:set("httpd", "bind_address", "0.0.0.0", false),
+
+ % Ping-Pong kill process
+ SentinelPid ! {ping, self()},
+ receive
+ _Any -> ok
+ after 1000 ->
+ throw({timeout_error, registered_pid})
+ end,
+
+ ok = couch_config:set("httpd", "port", "80", false),
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "80",
+ "Implicitly test that the function got de-registered"
+ ),
+
+ % test passing of Persist flag
+ couch_config:register(
+ fun("httpd", _, _, Persist) ->
+ etap:is(Persist, false)
+ end),
+ ok = couch_config:set("httpd", "port", "80", false),
+
+ ok.
diff --git a/apps/couch/test/etap/083-config-no-files.t b/apps/couch/test/etap/083-config-no-files.t
new file mode 100755
index 00000000..edf8315d
--- /dev/null
+++ b/apps/couch/test/etap/083-config-no-files.t
@@ -0,0 +1,56 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+default_config() ->
+ "rel/overlay/etc/default.ini".
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(3),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+test() ->
+ couch_config_event:start_link(),
+ couch_config:start_link([]),
+
+ etap:fun_is(
+ fun(KVPairs) -> length(KVPairs) == 0 end,
+ couch_config:all(),
+ "No INI files specified returns 0 key/value pairs."
+ ),
+
+ ok = couch_config:set("httpd", "port", "80", false),
+
+ etap:is(
+ couch_config:get("httpd", "port"),
+ "80",
+ "Created a new non-persisted k/v pair."
+ ),
+
+ ok = couch_config:set("httpd", "bind_address", "127.0.0.1"),
+ etap:is(
+ couch_config:get("httpd", "bind_address"),
+ "127.0.0.1",
+ "Asking for a persistent key/value pair doesn't choke."
+ ),
+
+ ok.
diff --git a/apps/couch/test/etap/090-task-status.t b/apps/couch/test/etap/090-task-status.t
new file mode 100755
index 00000000..b278de7f
--- /dev/null
+++ b/apps/couch/test/etap/090-task-status.t
@@ -0,0 +1,209 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(16),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+check_status(Pid,ListPropLists) ->
+ From = list_to_binary(pid_to_list(Pid)),
+ Element = lists:foldl(
+ fun(PropList,Acc) ->
+ case couch_util:get_value(pid,PropList) of
+ From ->
+ [PropList | Acc];
+ _ ->
+ []
+ end
+ end,
+ [], ListPropLists
+ ),
+ couch_util:get_value(status,hd(Element)).
+
+loop() ->
+ receive
+ {add, From} ->
+ Resp = couch_task_status:add_task("type", "task", "init"),
+ From ! {ok, self(), Resp},
+ loop();
+ {update, Status, From} ->
+ Resp = couch_task_status:update(Status),
+ From ! {ok, self(), Resp},
+ loop();
+ {update_frequency, Msecs, From} ->
+ Resp = couch_task_status:set_update_frequency(Msecs),
+ From ! {ok, self(), Resp},
+ loop();
+ {done, From} ->
+ From ! {ok, self(), ok}
+ end.
+
+call(Pid, Command) ->
+ Pid ! {Command, self()},
+ wait(Pid).
+
+call(Pid, Command, Arg) ->
+ Pid ! {Command, Arg, self()},
+ wait(Pid).
+
+wait(Pid) ->
+ receive
+ {ok, Pid, Msg} -> Msg
+ after 1000 ->
+ throw(timeout_error)
+ end.
+
+test() ->
+ {ok, TaskStatusPid} = couch_task_status:start_link(),
+
+ TaskUpdater = fun() -> loop() end,
+ % create three updaters
+ Pid1 = spawn(TaskUpdater),
+ Pid2 = spawn(TaskUpdater),
+ Pid3 = spawn(TaskUpdater),
+
+ ok = call(Pid1, add),
+ etap:is(
+ length(couch_task_status:all()),
+ 1,
+ "Started a task"
+ ),
+
+ etap:is(
+ call(Pid1, add),
+ {add_task_error, already_registered},
+ "Unable to register multiple tasks for a single Pid."
+ ),
+
+ etap:is(
+ check_status(Pid1, couch_task_status:all()),
+ <<"init">>,
+ "Task status was set to 'init'."
+ ),
+
+ call(Pid1,update,"running"),
+ etap:is(
+ check_status(Pid1,couch_task_status:all()),
+ <<"running">>,
+ "Status updated to 'running'."
+ ),
+
+
+ call(Pid2,add),
+ etap:is(
+ length(couch_task_status:all()),
+ 2,
+ "Started a second task."
+ ),
+
+ etap:is(
+ check_status(Pid2, couch_task_status:all()),
+ <<"init">>,
+ "Second tasks's status was set to 'init'."
+ ),
+
+ call(Pid2, update, "running"),
+ etap:is(
+ check_status(Pid2, couch_task_status:all()),
+ <<"running">>,
+ "Second task's status updated to 'running'."
+ ),
+
+
+ call(Pid3, add),
+ etap:is(
+ length(couch_task_status:all()),
+ 3,
+ "Registered a third task."
+ ),
+
+ etap:is(
+ check_status(Pid3, couch_task_status:all()),
+ <<"init">>,
+ "Third tasks's status was set to 'init'."
+ ),
+
+ call(Pid3, update, "running"),
+ etap:is(
+ check_status(Pid3, couch_task_status:all()),
+ <<"running">>,
+ "Third task's status updated to 'running'."
+ ),
+
+
+ call(Pid3, update_frequency, 500),
+ call(Pid3, update, "still running"),
+ etap:is(
+ check_status(Pid3, couch_task_status:all()),
+ <<"still running">>,
+ "Third task's status updated to 'still running'."
+ ),
+
+ call(Pid3, update, "skip this update"),
+ etap:is(
+ check_status(Pid3, couch_task_status:all()),
+ <<"still running">>,
+ "Status update dropped because of frequency limit."
+ ),
+
+ call(Pid3, update_frequency, 0),
+ call(Pid3, update, "don't skip"),
+ etap:is(
+ check_status(Pid3, couch_task_status:all()),
+ <<"don't skip">>,
+ "Status updated after reseting frequency limit."
+ ),
+
+
+ call(Pid1, done),
+ etap:is(
+ length(couch_task_status:all()),
+ 2,
+ "First task finished."
+ ),
+
+ call(Pid2, done),
+ etap:is(
+ length(couch_task_status:all()),
+ 1,
+ "Second task finished."
+ ),
+
+ call(Pid3, done),
+ etap:is(
+ length(couch_task_status:all()),
+ 0,
+ "Third task finished."
+ ),
+
+ erlang:monitor(process, TaskStatusPid),
+ couch_task_status:stop(),
+ receive
+ {'DOWN', _, _, TaskStatusPid, _} ->
+ ok
+ after
+ 1000 ->
+ throw(timeout_error)
+ end,
+
+ ok.
diff --git a/apps/couch/test/etap/100-ref-counter.t b/apps/couch/test/etap/100-ref-counter.t
new file mode 100755
index 00000000..8f996d04
--- /dev/null
+++ b/apps/couch/test/etap/100-ref-counter.t
@@ -0,0 +1,114 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(8),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+loop() ->
+ receive
+ close -> ok
+ end.
+
+wait() ->
+ receive
+ {'DOWN', _, _, _, _} -> ok
+ after 1000 ->
+ throw(timeout_error)
+ end.
+
+test() ->
+ {ok, RefCtr} = couch_ref_counter:start([]),
+
+ etap:is(
+ couch_ref_counter:count(RefCtr),
+ 1,
+ "A ref_counter is initialized with the calling process as a referer."
+ ),
+
+ ChildPid1 = spawn(fun() -> loop() end),
+
+ % This is largely implicit in that nothing else breaks
+ % as ok is just returned from gen_server:cast()
+ etap:is(
+ couch_ref_counter:drop(RefCtr, ChildPid1),
+ ok,
+ "Dropping an unknown Pid is ignored."
+ ),
+
+ couch_ref_counter:add(RefCtr, ChildPid1),
+ etap:is(
+ couch_ref_counter:count(RefCtr),
+ 2,
+ "Adding a Pid to the ref_counter increases it's count."
+ ),
+
+ couch_ref_counter:add(RefCtr, ChildPid1),
+ etap:is(
+ couch_ref_counter:count(RefCtr),
+ 2,
+ "Readding the same Pid maintains the count but increments it's refs."
+ ),
+
+ couch_ref_counter:drop(RefCtr, ChildPid1),
+ etap:is(
+ couch_ref_counter:count(RefCtr),
+ 2,
+ "Droping the doubly added Pid only removes a ref, not a referer."
+ ),
+
+ couch_ref_counter:drop(RefCtr, ChildPid1),
+ etap:is(
+ couch_ref_counter:count(RefCtr),
+ 1,
+ "Dropping the second ref drops the referer."
+ ),
+
+ couch_ref_counter:add(RefCtr, ChildPid1),
+ etap:is(
+ couch_ref_counter:count(RefCtr),
+ 2,
+ "Sanity checking that the Pid was re-added."
+ ),
+
+ erlang:monitor(process, ChildPid1),
+ ChildPid1 ! close,
+ wait(),
+
+ CheckFun = fun
+ (Iter, nil) ->
+ case couch_ref_counter:count(RefCtr) of
+ 1 -> Iter;
+ _ -> nil
+ end;
+ (_, Acc) ->
+ Acc
+ end,
+ Result = lists:foldl(CheckFun, nil, lists:seq(1, 10000)),
+ etap:isnt(
+ Result,
+ nil,
+ "The referer count was decremented automatically on process exit."
+ ),
+
+ ok.
diff --git a/apps/couch/test/etap/120-stats-collect.inactive b/apps/couch/test/etap/120-stats-collect.inactive
new file mode 100755
index 00000000..dee88765
--- /dev/null
+++ b/apps/couch/test/etap/120-stats-collect.inactive
@@ -0,0 +1,150 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(11),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail()
+ end,
+ ok.
+
+test() ->
+ couch_stats_collector:start(),
+ ok = test_counters(),
+ ok = test_abs_values(),
+ ok = test_proc_counting(),
+ ok = test_all(),
+ ok.
+
+test_counters() ->
+ AddCount = fun() -> couch_stats_collector:increment(foo) end,
+ RemCount = fun() -> couch_stats_collector:decrement(foo) end,
+ repeat(AddCount, 100),
+ repeat(RemCount, 25),
+ repeat(AddCount, 10),
+ repeat(RemCount, 5),
+ etap:is(
+ couch_stats_collector:get(foo),
+ 80,
+ "Incrememnt tracks correctly."
+ ),
+
+ repeat(RemCount, 80),
+ etap:is(
+ couch_stats_collector:get(foo),
+ 0,
+ "Decremented to zaro."
+ ),
+ ok.
+
+test_abs_values() ->
+ lists:map(fun(Val) ->
+ couch_stats_collector:record(bar, Val)
+ end, lists:seq(1, 15)),
+ etap:is(
+ couch_stats_collector:get(bar),
+ lists:seq(1, 15),
+ "Absolute values are recorded correctly."
+ ),
+
+ couch_stats_collector:clear(bar),
+ etap:is(
+ couch_stats_collector:get(bar),
+ nil,
+ "Absolute values are cleared correctly."
+ ),
+ ok.
+
+test_proc_counting() ->
+ Self = self(),
+ OnePid = spawn(fun() ->
+ couch_stats_collector:track_process_count(hoopla),
+ Self ! reporting,
+ receive sepuku -> ok end
+ end),
+ R1 = erlang:monitor(process, OnePid),
+ receive reporting -> ok end,
+ etap:is(
+ couch_stats_collector:get(hoopla),
+ 1,
+ "track_process_count incrememnts the counter."
+ ),
+
+ TwicePid = spawn(fun() ->
+ couch_stats_collector:track_process_count(hoopla),
+ couch_stats_collector:track_process_count(hoopla),
+ Self ! reporting,
+ receive sepuku -> ok end
+ end),
+ R2 = erlang:monitor(process, TwicePid),
+ receive reporting -> ok end,
+ etap:is(
+ couch_stats_collector:get(hoopla),
+ 3,
+ "track_process_count allows more than one incrememnt per Pid"
+ ),
+
+ OnePid ! sepuku,
+ receive {'DOWN', R1, _, _, _} -> ok end,
+ timer:sleep(250),
+ etap:is(
+ couch_stats_collector:get(hoopla),
+ 2,
+ "Process count is decremented when process exits."
+ ),
+
+ TwicePid ! sepuku,
+ receive {'DOWN', R2, _, _, _} -> ok end,
+ timer:sleep(250),
+ etap:is(
+ couch_stats_collector:get(hoopla),
+ 0,
+ "Process count is decremented for each call to track_process_count."
+ ),
+ ok.
+
+test_all() ->
+ couch_stats_collector:record(bar, 0.0),
+ couch_stats_collector:record(bar, 1.0),
+ etap:is(
+ couch_stats_collector:all(),
+ [{foo, 0}, {hoopla, 0}, {bar, [1.0, 0.0]}],
+ "all/0 returns all counters and absolute values."
+ ),
+
+ etap:is(
+ couch_stats_collector:all(incremental),
+ [{foo, 0}, {hoopla, 0}],
+ "all/1 returns only the specified type."
+ ),
+
+ couch_stats_collector:record(zing, 90),
+ etap:is(
+ couch_stats_collector:all(absolute),
+ [{zing, [90]}, {bar, [1.0, 0.0]}],
+ "all/1 returns only the specified type."
+ ),
+ ok.
+
+repeat(_, 0) ->
+ ok;
+repeat(Fun, Count) ->
+ Fun(),
+ repeat(Fun, Count-1).
diff --git a/apps/couch/test/etap/121-stats-aggregates.cfg b/apps/couch/test/etap/121-stats-aggregates.cfg
new file mode 100644
index 00000000..30e475da
--- /dev/null
+++ b/apps/couch/test/etap/121-stats-aggregates.cfg
@@ -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.
+
+{testing, stuff, "yay description"}.
+{number, '11', "randomosity"}.
diff --git a/apps/couch/test/etap/121-stats-aggregates.inactive b/apps/couch/test/etap/121-stats-aggregates.inactive
new file mode 100755
index 00000000..d678aa9d
--- /dev/null
+++ b/apps/couch/test/etap/121-stats-aggregates.inactive
@@ -0,0 +1,171 @@
+#!/usr/bin/env escript
+%% -*- erlang -*-
+
+% 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.
+
+ini_file() ->
+ test_util:source_file("test/etap/121-stats-aggregates.ini").
+
+cfg_file() ->
+ test_util:source_file("test/etap/121-stats-aggregates.cfg").
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(17),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail()
+ end,
+ ok.
+
+test() ->
+ couch_config:start_link([ini_file()]),
+ couch_stats_collector:start(),
+ couch_stats_aggregator:start(cfg_file()),
+ ok = test_all_empty(),
+ ok = test_get_empty(),
+ ok = test_count_stats(),
+ ok = test_abs_stats(),
+ ok.
+
+test_all_empty() ->
+ {Aggs} = couch_stats_aggregator:all(),
+
+ etap:is(length(Aggs), 2, "There are only two aggregate types in testing."),
+ etap:is(
+ couch_util:get_value(testing, Aggs),
+ {[{stuff, make_agg(<<"yay description">>,
+ null, null, null, null, null)}]},
+ "{testing, stuff} is empty at start."
+ ),
+ etap:is(
+ couch_util:get_value(number, Aggs),
+ {[{'11', make_agg(<<"randomosity">>,
+ null, null, null, null, null)}]},
+ "{number, '11'} is empty at start."
+ ),
+ ok.
+
+test_get_empty() ->
+ etap:is(
+ couch_stats_aggregator:get_json({testing, stuff}),
+ make_agg(<<"yay description">>, null, null, null, null, null),
+ "Getting {testing, stuff} returns an empty aggregate."
+ ),
+ etap:is(
+ couch_stats_aggregator:get_json({number, '11'}),
+ make_agg(<<"randomosity">>, null, null, null, null, null),
+ "Getting {number, '11'} returns an empty aggregate."
+ ),
+ ok.
+
+test_count_stats() ->
+ lists:foreach(fun(_) ->
+ couch_stats_collector:increment({testing, stuff})
+ end, lists:seq(1, 100)),
+ couch_stats_aggregator:collect_sample(),
+ etap:is(
+ couch_stats_aggregator:get_json({testing, stuff}),
+ make_agg(<<"yay description">>, 100, 100, null, 100, 100),
+ "COUNT: Adding values changes the stats."
+ ),
+ etap:is(
+ couch_stats_aggregator:get_json({testing, stuff}, 1),
+ make_agg(<<"yay description">>, 100, 100, null, 100, 100),
+ "COUNT: Adding values changes stats for all times."
+ ),
+
+ timer:sleep(500),
+ couch_stats_aggregator:collect_sample(),
+ etap:is(
+ couch_stats_aggregator:get_json({testing, stuff}),
+ make_agg(<<"yay description">>, 100, 50, 70.711, 0, 100),
+ "COUNT: Removing values changes stats."
+ ),
+ etap:is(
+ couch_stats_aggregator:get_json({testing, stuff}, 1),
+ make_agg(<<"yay description">>, 100, 50, 70.711, 0, 100),
+ "COUNT: Removing values changes stats for all times."
+ ),
+
+ timer:sleep(600),
+ couch_stats_aggregator:collect_sample(),
+ etap:is(
+ couch_stats_aggregator:get_json({testing, stuff}),
+ make_agg(<<"yay description">>, 100, 33.333, 57.735, 0, 100),
+ "COUNT: Letting time passes doesn't remove data from time 0 aggregates"
+ ),
+ etap:is(
+ couch_stats_aggregator:get_json({testing, stuff}, 1),
+ make_agg(<<"yay description">>, 0, 0, 0, 0, 0),
+ "COUNT: Letting time pass removes data from other time aggregates."
+ ),
+ ok.
+
+test_abs_stats() ->
+ lists:foreach(fun(X) ->
+ couch_stats_collector:record({number, 11}, X)
+ end, lists:seq(0, 10)),
+ couch_stats_aggregator:collect_sample(),
+ etap:is(
+ couch_stats_aggregator:get_json({number, 11}),
+ make_agg(<<"randomosity">>, 5, 5, null, 5, 5),
+ "ABS: Adding values changes the stats."
+ ),
+ etap:is(
+ couch_stats_aggregator:get_json({number, 11}, 1),
+ make_agg(<<"randomosity">>, 5, 5, null, 5, 5),
+ "ABS: Adding values changes stats for all times."
+ ),
+
+ timer:sleep(500),
+ couch_stats_collector:record({number, 11}, 15),
+ couch_stats_aggregator:collect_sample(),
+ etap:is(
+ couch_stats_aggregator:get_json({number, 11}),
+ make_agg(<<"randomosity">>, 20, 10, 7.071, 5, 15),
+ "ABS: New values changes stats"
+ ),
+ etap:is(
+ couch_stats_aggregator:get_json({number, 11}, 1),
+ make_agg(<<"randomosity">>, 20, 10, 7.071, 5, 15),
+ "ABS: Removing values changes stats for all times."
+ ),
+
+ timer:sleep(600),
+ couch_stats_aggregator:collect_sample(),
+ etap:is(
+ couch_stats_aggregator:get_json({number, 11}),
+ make_agg(<<"randomosity">>, 20, 10, 7.071, 5, 15),
+ "ABS: Letting time passes doesn't remove data from time 0 aggregates"
+ ),
+ etap:is(
+ couch_stats_aggregator:get_json({number, 11}, 1),
+ make_agg(<<"randomosity">>, 15, 15, null, 15, 15),
+ "ABS: Letting time pass removes data from other time aggregates."
+ ),
+ ok.
+
+make_agg(Desc, Sum, Mean, StdDev, Min, Max) ->
+ {[
+ {description, Desc},
+ {current, Sum},
+ {sum, Sum},
+ {mean, Mean},
+ {stddev, StdDev},
+ {min, Min},
+ {max, Max}
+ ]}.
diff --git a/apps/couch/test/etap/121-stats-aggregates.ini b/apps/couch/test/etap/121-stats-aggregates.ini
new file mode 100644
index 00000000..cc5cd218
--- /dev/null
+++ b/apps/couch/test/etap/121-stats-aggregates.ini
@@ -0,0 +1,20 @@
+; 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.
+
+[stats]
+rate = 10000000 ; We call collect_sample in testing
+samples = [0, 1]
diff --git a/apps/couch/test/etap/etap.beam b/apps/couch/test/etap/etap.beam
new file mode 100644
index 00000000..da77f1d6
--- /dev/null
+++ b/apps/couch/test/etap/etap.beam
Binary files differ
diff --git a/apps/couch/test/etap/run.tpl b/apps/couch/test/etap/run.tpl
new file mode 100644
index 00000000..faf0f456
--- /dev/null
+++ b/apps/couch/test/etap/run.tpl
@@ -0,0 +1,27 @@
+#!/bin/sh -e
+
+# 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.
+
+SRCDIR="%abs_top_srcdir%"
+BUILDIR="%abs_top_builddir%"
+
+export ERL_FLAGS="$ERL_FLAGS -pa $BUILDIR/test/etap/"
+
+if test $# -gt 0; then
+ while [ $# -gt 0 ]; do
+ $1
+ shift
+ done
+else
+ prove $SRCDIR/test/etap/*.t
+fi