diff options
Diffstat (limited to 'apps/couch/test/etap')
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 Binary files differnew file mode 100644 index 00000000..da77f1d6 --- /dev/null +++ b/apps/couch/test/etap/etap.beam 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 |